From a0c38f5c4e16ffea4e3082d6e10cff0f2ff6a933 Mon Sep 17 00:00:00 2001 From: Patel230 Date: Mon, 1 Jun 2026 22:15:03 +0530 Subject: [PATCH 1/8] chore: add microservice architecture scaffolding Add .env.example, api/openapi.yaml, deploy/docker/docker-compose.yml, docs/architecture.md, and .github/workflows/docker.yml for GHCI build+push CI. --- .env.example | 10 + .github/workflows/docker.yml | 63 +++++++ api/openapi.yaml | 303 +++++++++++++++++++++++++++++++ deploy/docker/docker-compose.yml | 17 ++ docs/architecture.md | 198 ++++++++++---------- 5 files changed, 494 insertions(+), 97 deletions(-) create mode 100644 .env.example create mode 100644 .github/workflows/docker.yml create mode 100644 api/openapi.yaml create mode 100644 deploy/docker/docker-compose.yml diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..8a6cccd2 --- /dev/null +++ b/.env.example @@ -0,0 +1,10 @@ +# hawk daemon environment variables โ€” copy to .env and fill in +HAWK_DAEMON_API_KEY= +HAWK_DAEMON_PORT=4590 +HAWK_DAEMON_HOST=127.0.0.1 +# Eyrie connection (LLM provider runtime) +EYRIE_API_KEY= +EYRIE_BASE_URL=http://localhost:8080 +# Yaad connection (memory service) +YAAD_API_KEY= +YAAD_ADDR=127.0.0.1:3456 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 00000000..1d0f9bcf --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,63 @@ +name: Docker + +on: + push: + branches: [main] + tags: ["v*"] + pull_request: + branches: [main] + paths: + - "Dockerfile" + - "**.go" + - "go.mod" + - "go.sum" + +permissions: + contents: read + packages: write + +env: + REGISTRY: ghcr.io + IMAGE_NAME: graycodeai/hawk + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,prefix=sha- + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + VERSION=${{ github.ref_name }} + COMMIT=${{ github.sha }} + BUILD_DATE=${{ github.event.head_commit.timestamp }} diff --git a/api/openapi.yaml b/api/openapi.yaml new file mode 100644 index 00000000..df1dfe16 --- /dev/null +++ b/api/openapi.yaml @@ -0,0 +1,303 @@ +openapi: "3.1.0" +info: + title: Hawk Daemon API + description: | + HTTP API served by the hawk daemon on port 4590. + Used by hawk-sdk-go, hawk-sdk-python, and external integrations. + The daemon must be running (`hawk daemon start`) before calling these endpoints. + version: "0.1.0" + license: + name: MIT + url: https://github.com/GrayCodeAI/hawk/blob/main/LICENSE + contact: + url: https://github.com/GrayCodeAI/hawk + +servers: + - url: http://localhost:4590 + description: Local daemon (default port) + +security: + - ApiKeyAuth: [] + - BearerAuth: [] + +components: + securitySchemes: + ApiKeyAuth: + type: apiKey + in: header + name: X-API-Key + BearerAuth: + type: http + scheme: bearer + + schemas: + ChatRequest: + type: object + required: [message] + properties: + message: + type: string + description: The user message to send to the agent + model: + type: string + description: Optional model override + session_id: + type: string + description: Continue an existing session (omit to start a new one) + stream: + type: boolean + default: false + description: Use SSE streaming for the response + + ChatResponse: + type: object + properties: + session_id: + type: string + response: + type: string + model: + type: string + tokens_in: + type: integer + tokens_out: + type: integer + + Session: + type: object + properties: + id: + type: string + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + message_count: + type: integer + model: + type: string + + Message: + type: object + properties: + id: + type: string + session_id: + type: string + role: + type: string + enum: [user, assistant, tool] + content: + type: string + created_at: + type: string + format: date-time + + Stats: + type: object + properties: + total_sessions: + type: integer + active_sessions: + type: integer + total_messages: + type: integer + tokens_in: + type: integer + tokens_out: + type: integer + + Error: + type: object + properties: + error: + type: string + code: + type: string + +tags: + - name: system + description: Health and version + - name: agent + description: Agent chat + - name: sessions + description: Session management + - name: messages + description: Message history + - name: stats + description: Usage statistics + +paths: + /v1/health: + get: + tags: [system] + summary: Health check + security: [] + responses: + "200": + description: Daemon is running + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: ok + + /v1/version: + get: + tags: [system] + summary: Version information + security: [] + responses: + "200": + description: Version details + content: + application/json: + schema: + type: object + properties: + version: + type: string + + /v1/chat: + post: + tags: [agent] + summary: Send a message to the agent + description: | + Non-streaming: returns the full response in one JSON object. + Streaming: set `stream: true` to receive Server-Sent Events. + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ChatRequest" + responses: + "200": + description: Agent response (or SSE stream) + content: + application/json: + schema: + $ref: "#/components/schemas/ChatResponse" + text/event-stream: + schema: + type: string + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + + /v1/sessions: + get: + tags: [sessions] + summary: List all sessions + parameters: + - name: limit + in: query + schema: + type: integer + default: 20 + - name: offset + in: query + schema: + type: integer + default: 0 + responses: + "200": + description: Paginated session list + content: + application/json: + schema: + type: object + properties: + sessions: + type: array + items: + $ref: "#/components/schemas/Session" + total: + type: integer + + /v1/sessions/{id}: + get: + tags: [sessions] + summary: Get a specific session + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: Session details + content: + application/json: + schema: + $ref: "#/components/schemas/Session" + "404": + description: Session not found + delete: + tags: [sessions] + summary: Delete a session and all its messages + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "204": + description: Deleted + + /v1/sessions/{id}/messages: + get: + tags: [messages] + summary: Get messages for a session + parameters: + - name: id + in: path + required: true + schema: + type: string + - name: limit + in: query + schema: + type: integer + default: 50 + - name: offset + in: query + schema: + type: integer + default: 0 + responses: + "200": + description: Paginated message list + content: + application/json: + schema: + type: object + properties: + messages: + type: array + items: + $ref: "#/components/schemas/Message" + total: + type: integer + + /v1/stats: + get: + tags: [stats] + summary: Aggregated usage statistics + responses: + "200": + description: Usage stats + content: + application/json: + schema: + $ref: "#/components/schemas/Stats" diff --git a/deploy/docker/docker-compose.yml b/deploy/docker/docker-compose.yml new file mode 100644 index 00000000..d19f4b95 --- /dev/null +++ b/deploy/docker/docker-compose.yml @@ -0,0 +1,17 @@ +name: hawk + +services: + hawk: + build: + context: ../../ + dockerfile: Dockerfile + image: ghcr.io/graycodeai/hawk:dev + ports: + - "4590:4590" + environment: + - HAWK_DAEMON_API_KEY=${HAWK_DAEMON_API_KEY:-} + - HAWK_DAEMON_PORT=4590 + - HAWK_DAEMON_HOST=0.0.0.0 + env_file: + - path: ../../.env.example + required: false diff --git a/docs/architecture.md b/docs/architecture.md index 8790cbb3..90a2bdc0 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,114 +1,118 @@ -# Architecture +
-hawk is a terminal-native AI coding agent built in Go. It reads, writes, and runs code through natural language interaction. +# ๐Ÿฆ… hawk Architecture -## System Overview +**AI Coding Agent for Your Terminal** -``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ hawk CLI โ”‚ -โ”‚ cmd/ โ†’ cobra + bubbletea TUI โ”‚ -โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค -โ”‚ internal/engine/ โ”‚ -โ”‚ Agent loop, compaction, self-improvement โ”‚ -โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค -โ”‚ internal โ”‚ internal โ”‚ internal โ”‚ internal โ”‚ external โ”‚ -โ”‚ /tool/ โ”‚/session/ โ”‚ /config/ โ”‚/sandbox/ โ”‚ services โ”‚ -โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค -โ”‚ eyrie (LLM runtime) โ”‚ -โ”‚ tok (tokenizer) ยท yaad (memory) ยท trace โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -``` +[![Go](https://img.shields.io/badge/Go-1.26+-00ADD8?logo=go)](https://go.dev/) +[![Port](https://img.shields.io/badge/Port-4590-orange)]() +[![Protocol](https://img.shields.io/badge/Protocol-REST-blue)]() + +
+ +--- + +## ๐ŸŽฏ Overview + +hawk is an AI-powered coding agent for the terminal. It reads codebases, writes and edits files, runs tests, and manages git โ€” all through natural language. Zero CGO, single static binary for linux/darwin/windows on amd64/arm64. -## Directory Structure +--- + +## ๐Ÿงฑ Layered Architecture ``` hawk/ -โ”œโ”€โ”€ cmd/ # CLI entry point (Cobra + Bubble Tea TUI) +โ”œโ”€โ”€ main.go โšก Entry point โ€” calls cmd.Execute() +โ”œโ”€โ”€ api/openapi.yaml ๐Ÿ“œ Daemon REST API contract (OpenAPI 3.1) +โ”œโ”€โ”€ cmd/ ๐Ÿ–ฅ๏ธ Cobra CLI commands (200+ files) +โ”‚ โ”œโ”€โ”€ root.go โš™๏ธ Root command, flag definitions +โ”‚ โ”œโ”€โ”€ daemon.go ๐Ÿ”ฎ Daemon start/stop/status +โ”‚ โ”œโ”€โ”€ chat.go ๐Ÿ’ฌ Interactive TUI chat +โ”‚ โ””โ”€โ”€ ... โ”œโ”€โ”€ internal/ -โ”‚ โ”œโ”€โ”€ engine/ # Agent loop, compaction, beliefs, self-review -โ”‚ โ”‚ โ”œโ”€โ”€ compact/ # Context compaction strategies -โ”‚ โ”‚ โ”œโ”€โ”€ cost/ # Cost tracking & optimization -โ”‚ โ”‚ โ”œโ”€โ”€ token/ # Token counting & budget allocation -โ”‚ โ”‚ โ”œโ”€โ”€ prompt/ # Prompt construction & optimization -โ”‚ โ”‚ โ”œโ”€โ”€ diff/ # Diff handling, sandbox, summary -โ”‚ โ”‚ โ”œโ”€โ”€ review/ # Critic, self-assess, consensus -โ”‚ โ”‚ โ”œโ”€โ”€ control/ # Loop detection, stall, backtrack -โ”‚ โ”‚ โ”œโ”€โ”€ git/ # Git provider + context -โ”‚ โ”‚ โ””โ”€โ”€ ... # 15+ more sub-packages -โ”‚ โ”œโ”€โ”€ tool/ # 40+ built-in tools with safety layer -โ”‚ โ”œโ”€โ”€ config/ # Settings, budget tracking, validation -โ”‚ โ”œโ”€โ”€ session/ # Persistence (JSONL, WAL, checkpoints) -โ”‚ โ”œโ”€โ”€ api/ # HTTP API server -โ”‚ โ”œโ”€โ”€ daemon/ # Background HTTP/SSE server -โ”‚ โ”œโ”€โ”€ sandbox/ # Command isolation (landlock, seccomp) -โ”‚ โ”œโ”€โ”€ permissions/ # User approval system with auto-learning -โ”‚ โ”œโ”€โ”€ hooks/ # Event-driven plugin system -โ”‚ โ”œโ”€โ”€ mcp/ # Model Context Protocol client -โ”‚ โ”œโ”€โ”€ intelligence/ # Code intelligence -โ”‚ โ”‚ โ”œโ”€โ”€ repomap/ # PageRank, BM25, TF-IDF for file relevance -โ”‚ โ”‚ โ”œโ”€โ”€ memory/ # yaad bridge for persistent cross-session memory -โ”‚ โ”‚ โ””โ”€โ”€ planner/ # Multi-step planning with decomposition -โ”‚ โ”œโ”€โ”€ multiagent/ # Multi-agent orchestration -โ”‚ โ”‚ โ”œโ”€โ”€ mission/ # Parallel feature execution in worktrees -โ”‚ โ”‚ โ”œโ”€โ”€ parallel/ # Worktree-based parallel execution -โ”‚ โ”‚ โ””โ”€โ”€ agents/ # Custom persona loader -โ”‚ โ”œโ”€โ”€ observability/ # Analytics, metrics, logging, tracing -โ”‚ โ”œโ”€โ”€ resilience/ # Circuit breaker, rate limiting, retries, health -โ”‚ โ”œโ”€โ”€ feature/ # eval, fingerprint, voice, IDE, shellmode -โ”‚ โ”œโ”€โ”€ bridge/ # External bridges (sight, inspect, sessioncapture) -โ”‚ โ”œโ”€โ”€ provider/ # Provider routing -โ”‚ โ””โ”€โ”€ system/ # Bus, cron, retention, shutdown, staleness -โ”œโ”€โ”€ docs/ # Architecture, research notes -โ””โ”€โ”€ testdata/ # Test fixtures +โ”‚ โ”œโ”€โ”€ api/ ๐ŸŒ HTTP server (:4590) โ€” 8 REST endpoints +โ”‚ โ”œโ”€โ”€ daemon/ ๐Ÿ”ฎ Daemon lifecycle (PID file, socket) +โ”‚ โ”œโ”€โ”€ engine/ ๐Ÿง  Agent execution loop +โ”‚ โ”‚ โ”œโ”€โ”€ session.go ๐Ÿ”„ Core agent loop (Stream, agentLoop) +โ”‚ โ”‚ โ”œโ”€โ”€ ctxmgr/ ๐Ÿ“ฆ Context packing and visualization +โ”‚ โ”‚ โ”œโ”€โ”€ token/ ๐Ÿ’ฐ Budget allocation and prediction +โ”‚ โ”‚ โ”œโ”€โ”€ streaming/ ๐Ÿ“ก Response cache and stream optimizer +โ”‚ โ”‚ โ”œโ”€โ”€ planning/ ๐ŸŽฏ Goals and task decomposition +โ”‚ โ”‚ โ””โ”€โ”€ workflow/ ๐Ÿ”ง JSON-defined automation pipelines +โ”‚ โ”œโ”€โ”€ tool/ ๐Ÿ› ๏ธ 40+ built-in tools +โ”‚ โ”œโ”€โ”€ config/ โš™๏ธ Settings, env manager, migration +โ”‚ โ”œโ”€โ”€ session/ ๐Ÿ’พ SQLite persistence, search, export +โ”‚ โ”œโ”€โ”€ permissions/ ๐Ÿ›ก๏ธ Guardian, rules DSL, boundary checker +โ”‚ โ”œโ”€โ”€ sandbox/ ๐Ÿ–๏ธ Landlock + seccomp isolation +โ”‚ โ”œโ”€โ”€ intelligence/ ๐Ÿงฌ Repo map, AST analysis, deps +โ”‚ โ”œโ”€โ”€ multiagent/ ๐Ÿ‘ฅ Personas, inter-agent messaging +โ”‚ โ”œโ”€โ”€ mcp/ ๐Ÿ”Œ MCP client and server +โ”‚ โ”œโ”€โ”€ bridge/ ๐ŸŒ‰ Bridges to ecosystem services +โ”‚ โ””โ”€โ”€ resilience/ ๐Ÿ”„ Circuit breaker, retry, rate limit +โ”œโ”€โ”€ shared/types/ ๐Ÿ“ค Cross-repo exported types +โ”œโ”€โ”€ docs/ ๐Ÿ“– Architecture docs +โ””โ”€โ”€ external/ ๐Ÿ”— Local go.work checkouts ``` -## Data Flow +--- + +## ๐ŸŒ Daemon HTTP API (:4590) + +| | | +|---|---| +| **Contract** | [`api/openapi.yaml`](../api/openapi.yaml) | +| **Port** | `:4590` (default). Override: `HAWK_DAEMON_PORT` | +| **Auth** | Bearer token or `X-API-Key`. Set via `HAWK_DAEMON_API_KEY` | + +
+๐Ÿ“ก Endpoint Summary + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/v1/health` | ๐Ÿฉบ Health check | +| `GET` | `/v1/version` | ๐Ÿท๏ธ Version info | +| `POST` | `/v1/chat` | ๐Ÿ’ฌ Send message (JSON or SSE) | +| `GET` | `/v1/sessions` | ๐Ÿ“‹ List sessions | +| `GET` | `/v1/sessions/{id}` | ๐Ÿ” Get session | +| `GET` | `/v1/sessions/{id}/messages` | ๐Ÿ’ฌ Get messages | +| `DELETE` | `/v1/sessions/{id}` | ๐Ÿ—‘๏ธ Delete session | +| `GET` | `/v1/stats` | ๐Ÿ“Š Usage statistics | + +
+ +--- + +## ๐Ÿ”— Ecosystem Integration + +| Service | Role | Connection | +|---------|------|------------| +| ๐Ÿฆ… **eyrie** | LLM provider runtime | `:8080` โ€” all LLM calls routed here | +| ๐Ÿง  **yaad** | Persistent memory | `:3456` โ€” session context, recall | +| ๐Ÿ‘๏ธ **sight** | Code review | Library โ€” diff-based review | +| ๐Ÿ” **inspect** | Security audit | Library โ€” website scanning | +| โœ‚๏ธ **tok** | Token optimization | Library โ€” compression, secrets | +| ๐Ÿ“ธ **trace** | Session capture | CLI hook โ€” git-native capture | + +> ๐Ÿ’ก **hawk never talks to LLM APIs directly** โ€” all calls go through eyrie. + +--- + +## ๐Ÿ›ก๏ธ Tool Safety Layer + +Every tool call passes through the permission system before execution: ``` -User prompt - โ”‚ - โ–ผ -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ cmd/ โ”‚โ”€โ”€โ”€โ”€โ–ถโ”‚ engine/ โ”‚โ”€โ”€โ”€โ”€โ–ถโ”‚ eyrie โ”‚ -โ”‚ (TUI) โ”‚ โ”‚ (agent loop) โ”‚ โ”‚ (LLM) โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜ - โ”‚ โ”‚ - โ–ผ โ”‚ - โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ - โ”‚ tool/ โ”‚โ—€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ (execute)โ”‚ tool calls + text - โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ - โ–ผ - โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” - โ”‚ session/ โ”‚ (persist) - โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +Tool Call โ†’ ๐Ÿ›ก๏ธ Guardian (rules DSL) โ†’ ๐Ÿงฑ Boundary Checker โ†’ ๐Ÿ‘ค User Approval โ†’ ๐Ÿ–๏ธ Sandbox (landlock/seccomp) โ†’ โœ… Execute ``` -1. **Input** โ€” User types prompt in TUI (`cmd/` via Bubble Tea) -2. **Context assembly** โ€” `engine/` builds message array with repomap, memory, system prompt -3. **LLM call** โ€” `eyrie` sends to provider (streaming SSE) -4. **Response** โ€” LLM returns text + tool calls -5. **Tool execution** โ€” `engine/` executes via `tool.Registry` (with permission checks) -6. **Feedback loop** โ€” Tool results fed back to LLM for next turn -7. **Persistence** โ€” Final response displayed, session saved to WAL +--- -## Key Design Decisions +## ๐Ÿ“ Key Design Decisions | Decision | Rationale | -|---|---| -| **Single static binary** | Zero runtime dependencies, easy distribution | -| **Streaming-first** | Token-by-token display, low perceived latency | -| **WAL crash recovery** | No data loss on unexpected exit | -| **Permission sandboxing** | All tool calls gated by configurable permission engine | -| **Model-agnostic** | eyrie abstracts all provider differences | -| **Offline-capable** | Works with local models (Ollama) without API keys | -| **`internal/` boundary** | Compiler-enforced privacy, free to refactor | - -## Security Model - -- **Sandbox** โ€” Landlock (Linux), seccomp-bpf, seatbelt (macOS), no-op on Windows -- **Permissions** โ€” Ask-before-execution for Bash, Write, Edit; auto-learn from decisions -- **Secrets** โ€” API keys never logged, masked in config display, stored encrypted at rest -- **Path validation** โ€” Symlink traversal prevention, no escape from working directory +|----------|-----------| +| `main.go` at root | Intentional โ€” goreleaser builds with `main: ./` producing `hawk` binary | +| `cmd/` is CLI library | Not a binary sub-directory โ€” holds 200+ cobra command files | +| Zero CGO | Pure Go, cross-compilable. Tree-sitter is optional | +| `internal/` is private | Other repos import `shared/types/` only | +| `external/` | go.work symlinks for local dev โ€” not committed | From 384b1be2265c81b668f795189b0bdd83685d3c32 Mon Sep 17 00:00:00 2001 From: Patel230 Date: Mon, 1 Jun 2026 22:54:11 +0530 Subject: [PATCH 2/8] fix: resolve lint, module hygiene, and markdown CI failures Fix errcheck on rows.Close(), simplify bool comparison, remove unused knownFields var, fix empty badge links, and run go work sync to update module consistency. --- docs/architecture.md | 4 ++-- go.mod | 2 +- internal/jsonc/validate.go | 14 -------------- internal/permissions/verdict.go | 2 +- internal/session/session_gain.go | 2 +- 5 files changed, 5 insertions(+), 19 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 90a2bdc0..3fdbd4f9 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -5,8 +5,8 @@ **AI Coding Agent for Your Terminal** [![Go](https://img.shields.io/badge/Go-1.26+-00ADD8?logo=go)](https://go.dev/) -[![Port](https://img.shields.io/badge/Port-4590-orange)]() -[![Protocol](https://img.shields.io/badge/Protocol-REST-blue)]() +[![Port](https://img.shields.io/badge/Port-4590-orange)](https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml) +[![Protocol](https://img.shields.io/badge/Protocol-REST-blue)](https://swagger.io/specification/) diff --git a/go.mod b/go.mod index 3e2bf6db..c482dc5f 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( go.opentelemetry.io/otel/sdk v1.44.0 go.opentelemetry.io/otel/sdk/metric v1.44.0 go.opentelemetry.io/otel/trace v1.44.0 + golang.org/x/sys v0.45.0 golang.org/x/term v0.43.0 golang.org/x/text v0.37.0 gopkg.in/yaml.v3 v3.0.1 @@ -74,7 +75,6 @@ require ( go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/net v0.55.0 // indirect - golang.org/x/sys v0.45.0 // indirect golang.org/x/tools v0.45.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect diff --git a/internal/jsonc/validate.go b/internal/jsonc/validate.go index 39a964a4..9d0f23c7 100644 --- a/internal/jsonc/validate.go +++ b/internal/jsonc/validate.go @@ -57,20 +57,6 @@ func (r *ValidationResult) AddErr(err error) { r.Errors = append(r.Errors, err) } -// knownFields is the set of top-level field names recognized in -// Claude Code settings.json. Unknown fields are tolerated. -var knownFields = map[string]bool{ - "model": true, - "permissions": true, - "hooks": true, - "mcpServers": true, - "includeCoAuthoredBy": true, - "cleanupPeriodDays": true, - "forceLoginMethod": true, - "apiKeyHelper": true, - "env": true, -} - // ValidateClaudeSettings validates a parsed Claude Code settings // document (a map[string]interface{}). Returns a ValidationResult // describing any issues. The function never returns a non-nil error; diff --git a/internal/permissions/verdict.go b/internal/permissions/verdict.go index c5cf924b..3f48a882 100644 --- a/internal/permissions/verdict.go +++ b/internal/permissions/verdict.go @@ -125,7 +125,7 @@ func RequireApproval(reason, rule string, risk Risk) PermissionVerdict { // IsZero reports whether v is the zero value. Useful for // detecting "no verdict produced" cases. func (v PermissionVerdict) IsZero() bool { - return v.Allowed == false && v.Reason == "" && v.Rule == "" && + return !v.Allowed && v.Reason == "" && v.Rule == "" && v.Risk == 0 && v.Confidence == 0 && v.Source == "" } diff --git a/internal/session/session_gain.go b/internal/session/session_gain.go index ac4f3b5b..dd22a842 100644 --- a/internal/session/session_gain.go +++ b/internal/session/session_gain.go @@ -195,7 +195,7 @@ func (g *GainTracker) ListForSession(ctx context.Context, sessionID string, n in if err != nil { return nil, fmt.Errorf("session: list gains: %w", err) } - defer rows.Close() + defer func() { _ = rows.Close() }() var out []GainEvent for rows.Next() { var ev GainEvent From 94dd84e78245a1c759865fdf99f36754c5b37be7 Mon Sep 17 00:00:00 2001 From: Patel230 Date: Mon, 1 Jun 2026 23:18:54 +0530 Subject: [PATCH 3/8] fix: exclude go.work from Docker build context hawk's go.work references external/ symlinks to sibling repos that don't exist in the Docker build context. --- .dockerignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.dockerignore b/.dockerignore index 437a96b0..44ffd0e3 100644 --- a/.dockerignore +++ b/.dockerignore @@ -9,3 +9,5 @@ hawk_bin hawk_test_bin coverage.out coverage.html +go.work +go.work.sum From 7c26005f1d1903bd0e23e10caf0b64ae37daf243 Mon Sep 17 00:00:00 2001 From: Patel230 Date: Tue, 2 Jun 2026 04:24:41 +0530 Subject: [PATCH 4/8] fix: add Windows support for safewrite and update go.sum Add //go:build !windows constraint to safewrite.go (uses unix syscalls), create safewrite_windows.go with portable fallback, skip tests on Windows, and run go mod download to add missing go.sum entry for golang.org/x/tools. --- internal/safewrite/safewrite.go | 2 ++ internal/safewrite/safewrite_test.go | 2 ++ internal/safewrite/safewrite_windows.go | 38 +++++++++++++++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 internal/safewrite/safewrite_windows.go diff --git a/internal/safewrite/safewrite.go b/internal/safewrite/safewrite.go index 6e4791fe..a9e5965a 100644 --- a/internal/safewrite/safewrite.go +++ b/internal/safewrite/safewrite.go @@ -1,3 +1,5 @@ +//go:build !windows + // Package safewrite provides a hardened file-write helper that // protects against common symlink and permission attacks. // diff --git a/internal/safewrite/safewrite_test.go b/internal/safewrite/safewrite_test.go index 2a4d47a2..cb6b4cb2 100644 --- a/internal/safewrite/safewrite_test.go +++ b/internal/safewrite/safewrite_test.go @@ -1,3 +1,5 @@ +//go:build !windows + package safewrite_test import ( diff --git a/internal/safewrite/safewrite_windows.go b/internal/safewrite/safewrite_windows.go new file mode 100644 index 00000000..7acec371 --- /dev/null +++ b/internal/safewrite/safewrite_windows.go @@ -0,0 +1,38 @@ +//go:build windows + +package safewrite + +import ( + "errors" + "os" + "path/filepath" + "strings" +) + +var ErrPathEscape = errors.New("safewrite: path escapes parent directory") +var ErrSymlinkTarget = errors.New("safewrite: destination is a symlink") + +func WriteFile(path string, data []byte) error { + if path == "" { + return errors.New("safewrite: empty path") + } + if data == nil { + return errors.New("safewrite: nil data") + } + cleaned := filepath.Clean(path) + if !filepath.IsAbs(cleaned) { + abs, err := filepath.Abs(cleaned) + if err != nil { + return err + } + cleaned = abs + } + if strings.Contains(cleaned, "..") { + return ErrPathEscape + } + linfo, err := os.Lstat(cleaned) + if err == nil && linfo.Mode()&os.ModeSymlink != 0 { + return errors.New("safewrite: refusing to write through symlink") + } + return os.WriteFile(cleaned, data, 0o600) +} From 4a4d169dc355df3590e5826d240da9e227fbf71c Mon Sep 17 00:00:00 2001 From: Patel230 Date: Tue, 2 Jun 2026 04:30:51 +0530 Subject: [PATCH 5/8] fix: gofumpt format safewrite_windows.go and add missing go.sum entry The Windows stub needed formatting. Also add the /go.mod hash for golang.org/x/tools v0.45.0 that was missing from go.sum, causing the Docker build to fail. --- go.sum | 1 + internal/safewrite/safewrite_windows.go | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/go.sum b/go.sum index da54c5ae..f2bf3691 100644 --- a/go.sum +++ b/go.sum @@ -168,6 +168,7 @@ golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= diff --git a/internal/safewrite/safewrite_windows.go b/internal/safewrite/safewrite_windows.go index 7acec371..72c712e2 100644 --- a/internal/safewrite/safewrite_windows.go +++ b/internal/safewrite/safewrite_windows.go @@ -9,8 +9,10 @@ import ( "strings" ) -var ErrPathEscape = errors.New("safewrite: path escapes parent directory") -var ErrSymlinkTarget = errors.New("safewrite: destination is a symlink") +var ( + ErrPathEscape = errors.New("safewrite: path escapes parent directory") + ErrSymlinkTarget = errors.New("safewrite: destination is a symlink") +) func WriteFile(path string, data []byte) error { if path == "" { From 7e9e9edbb9123ce528033c0bfaf4f3ed595e31a2 Mon Sep 17 00:00:00 2001 From: Patel230 Date: Tue, 2 Jun 2026 04:36:42 +0530 Subject: [PATCH 6/8] fix: clone eyrie in Docker build for unpublished packages hawk depends on eyrie packages (credentials, runtime, setup, catalog/registry) that aren't in the published v0.5.0. Clone eyrie into the builder and add a replace directive. --- Dockerfile | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Dockerfile b/Dockerfile index f9c2904e..a74edf07 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,14 @@ FROM golang:1.26.3-alpine AS builder RUN apk add --no-cache git ca-certificates tzdata WORKDIR /build + +# Clone eyrie (unpublished dependency with local-only packages) +RUN git clone --depth=1 https://github.com/GrayCodeAI/eyrie.git /eyrie + COPY go.mod go.sum ./ +# Replace eyrie with local clone for unpublished packages +RUN echo "replace github.com/GrayCodeAI/eyrie => /eyrie" >> go.mod + RUN go mod download && go mod verify COPY . . From 339d535e8c9eff327c38c8f964ed629599096d61 Mon Sep 17 00:00:00 2001 From: Patel230 Date: Tue, 2 Jun 2026 04:44:41 +0530 Subject: [PATCH 7/8] fix: use go mod tidy in Docker to resolve eyrie replace directive The replace directive needs go mod tidy (not just download) to resolve all transitive dependencies from the local eyrie clone. Use -mod=mod to allow go.mod changes during build. --- Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index a74edf07..0801fbac 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,12 +10,12 @@ RUN git clone --depth=1 https://github.com/GrayCodeAI/eyrie.git /eyrie COPY go.mod go.sum ./ # Replace eyrie with local clone for unpublished packages -RUN echo "replace github.com/GrayCodeAI/eyrie => /eyrie" >> go.mod +RUN echo "" >> go.mod && echo "replace github.com/GrayCodeAI/eyrie => /eyrie" >> go.mod -RUN go mod download && go mod verify +RUN go mod download && go mod tidy COPY . . -RUN CGO_ENABLED=0 GOOS=linux go build -trimpath \ +RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -mod=mod \ -ldflags="-s -w -X main.Version=$(git describe --tags --always 2>/dev/null || echo dev)" \ -o hawk . From 864fdfe1fc3a3759324263ae228b014d8aa5305b Mon Sep 17 00:00:00 2001 From: Patel230 Date: Tue, 2 Jun 2026 04:47:30 +0530 Subject: [PATCH 8/8] fix: add eyrie replace directive after source copy COPY . . was overwriting go.mod that had the replace directive. Move the replace + go mod tidy to after the source copy. --- Dockerfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 0801fbac..e316254e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,12 +9,12 @@ WORKDIR /build RUN git clone --depth=1 https://github.com/GrayCodeAI/eyrie.git /eyrie COPY go.mod go.sum ./ -# Replace eyrie with local clone for unpublished packages -RUN echo "" >> go.mod && echo "replace github.com/GrayCodeAI/eyrie => /eyrie" >> go.mod - -RUN go mod download && go mod tidy +RUN go mod download COPY . . +# Add replace after source copy so it doesn't get overwritten +RUN echo "" >> go.mod && echo "replace github.com/GrayCodeAI/eyrie => /eyrie" >> go.mod && go mod tidy + RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -mod=mod \ -ldflags="-s -w -X main.Version=$(git describe --tags --always 2>/dev/null || echo dev)" \ -o hawk .