feat(architect): host-mode agent bootstrap (M18-4) #180

Merged
code-lead merged 1 commit from boss/165 into main 2026-04-20 18:57:28 +00:00
Collaborator

Summary

Bootstraps the architect agent type per M18-4: a host-mode, singleton
planning + spec-writing agent that runs inside the claude-hooks
service process
(no container) and is driven through a chat-style
HTTP API rather than webhook dispatch.

  • Adds types.architect in config/agents.json (container.enabled: false, plugins: [], default_model: "claude-opus-4-7[1m]").
  • Gates container.enabled: false behind a hard-coded HOST_MODE_TYPES
    allowlist in webhook-config.ts — any other type trying to opt out
    of container isolation fails loadWebhookConfig at boot.
  • Derives ResolvedAgent.host_mode at merge time; runAgentTask
    branches on the flag to skip worktree / container / docker-exec
    plumbing and invoke the SDK inline with cwd: process.cwd().
  • Adds apps/server/src/architect.tsarchitect_sessions SQLite
    table + CRUD + runArchitectTurn that resumes the claude session
    id between chat turns, and the HTTP handlers for the four new
    endpoints.
  • Wires routes: POST /architect/chat, GET /architect/sessions,
    GET /architect/sessions/:id, DELETE /architect/sessions/:id,
    GET /architect/stream/:task_id. The SSE stream reuses the global
    broadcastSSE feed with a per-subscriber filter on task_id, so
    events match the existing envelope.
  • Blocks webhook dispatch to the architect: resolveAgentByUser /
    resolveAgentByType now skip host-mode types, and a new
    NON_DISPATCHABLE_TYPES set in webhook-routing.ts carries the
    explicit allowlist check used by tests.
  • Enforces singleton: handleAgentsCreate refuses a second
    architect row with 409 Conflict; db.ensureDefaultForTypes
    seeds architect-default on first boot after upgrade.
  • Security rails: canUseTool denies git push origin main/master
    patterns and mcp__forgejo__merge_pull_request outright — the
    architect delegates implementation instead of performing it.
  • Docs: CLAUDE.md Roles table + Host-mode section, README Architect
    Agent section with chat API reference + provisioning runbook.
  • Tests: architect.test.ts (22 tests — SDK options, SQLite CRUD,
    HTTP handlers with a fake worker), host-mode validation in
    webhook-config.test.ts, routing sanity in
    webhook-routing.test.ts. Full suite: 668 pass / 0 fail.
  • scripts/smoke-creds.sh learns to probe host-mode agents:
    filesystem + token-file check, skipping every docker exec path.

Closes #165

Test plan

  • just qa — typecheck, lint, format, tests all green.
  • Architect routes present in main.ts (grep confirms).
  • Manual: on deploy, run just agent-env-sync architect and
    verify ~/.config/claude-hooks/agent-env/architect/ populated.
  • Manual: curl POST /architect/chat returns 202 with session_id
    + task_id once architect token is provisioned.
  • Manual: curl GET /architect/stream/:task_id streams SSE
    events matching the /events envelope shape.
## Summary Bootstraps the `architect` agent type per M18-4: a host-mode, singleton planning + spec-writing agent that runs **inside the claude-hooks service process** (no container) and is driven through a chat-style HTTP API rather than webhook dispatch. - Adds `types.architect` in `config/agents.json` (`container.enabled: false`, `plugins: []`, `default_model: "claude-opus-4-7[1m]"`). - Gates `container.enabled: false` behind a hard-coded `HOST_MODE_TYPES` allowlist in `webhook-config.ts` — any other type trying to opt out of container isolation fails `loadWebhookConfig` at boot. - Derives `ResolvedAgent.host_mode` at merge time; `runAgentTask` branches on the flag to skip worktree / container / docker-exec plumbing and invoke the SDK inline with `cwd: process.cwd()`. - Adds `apps/server/src/architect.ts` — `architect_sessions` SQLite table + CRUD + `runArchitectTurn` that resumes the claude session id between chat turns, and the HTTP handlers for the four new endpoints. - Wires routes: `POST /architect/chat`, `GET /architect/sessions`, `GET /architect/sessions/:id`, `DELETE /architect/sessions/:id`, `GET /architect/stream/:task_id`. The SSE stream reuses the global `broadcastSSE` feed with a per-subscriber filter on `task_id`, so events match the existing envelope. - Blocks webhook dispatch to the architect: `resolveAgentByUser` / `resolveAgentByType` now skip host-mode types, and a new `NON_DISPATCHABLE_TYPES` set in `webhook-routing.ts` carries the explicit allowlist check used by tests. - Enforces singleton: `handleAgentsCreate` refuses a second `architect` row with `409 Conflict`; `db.ensureDefaultForTypes` seeds `architect-default` on first boot after upgrade. - Security rails: `canUseTool` denies `git push origin main/master` patterns and `mcp__forgejo__merge_pull_request` outright — the architect delegates implementation instead of performing it. - Docs: CLAUDE.md Roles table + Host-mode section, README Architect Agent section with chat API reference + provisioning runbook. - Tests: `architect.test.ts` (22 tests — SDK options, SQLite CRUD, HTTP handlers with a fake worker), host-mode validation in `webhook-config.test.ts`, routing sanity in `webhook-routing.test.ts`. Full suite: **668 pass / 0 fail**. - `scripts/smoke-creds.sh` learns to probe host-mode agents: filesystem + token-file check, skipping every `docker exec` path. Closes #165 ## Test plan - [x] `just qa` — typecheck, lint, format, tests all green. - [x] Architect routes present in main.ts (`grep` confirms). - [ ] Manual: on deploy, run `just agent-env-sync architect` and verify `~/.config/claude-hooks/agent-env/architect/` populated. - [ ] Manual: `curl POST /architect/chat` returns 202 with session_id + task_id once architect token is provisioned. - [ ] Manual: `curl GET /architect/stream/:task_id` streams SSE events matching the `/events` envelope shape.
feat(architect): host-mode agent bootstrap (M18-4 / #165)
All checks were successful
qa / qa (pull_request) Successful in 3m49s
qa / dockerfile (pull_request) Successful in 8s
839c7aed85
Architect runs in the claude-hooks service process (no container),
singleton, driven via `/architect/chat` + session endpoints. Wired
through the existing Worker queue so cancel / task history / SSE work
the same way as the code-flow agents. Webhook dispatch paths are
explicitly blocked from reaching the architect; `container.enabled:
false` is gated behind a type allowlist so only the architect can opt
out of the container isolation boundary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
reviewer approved these changes 2026-04-20 18:56:40 +00:00
reviewer left a comment

Review: feat(architect): host-mode agent bootstrap (M18-4)

CI: Green (run #1770, sha 839c7ae, 3m59s)

Acceptance criteria — all met

Criterion Status
types.architect in agents.json (container.enabled: false, plugins: [], model 1M)
Singleton: ensureDefaultForTypes seeds architect-default; POST /agents 409 on second
ResolvedAgent.host_mode derived from container.enabled === false at merge time
runAgentTask branches on host_mode: skips worktree/container, uses process.cwd()
HOST_MODE_TYPES allowlist gate — other types with enabled: false fail boot loud
Architect loads isolated creds dir via hostModeConfigDir(); fallback to deterministic path
canUseTool denies git push origin main/master and mcp__forgejo__merge_pull_request
resolveAgentByUser / resolveAgentByType skip host-mode types
NON_DISPATCHABLE_TYPES carries explicit webhook block
Chat API: POST /architect/chat, GET/DELETE /architect/sessions, stream endpoint
architect_sessions SQLite table with id/created_at/updated_at/title/messages/claude_session_id
SSE stream reuses global broadcastSSE feed filtered on task_id
claude_session_id hidden from the GET transcript response
CLAUDE.md Roles table + Host-mode section
architect.test.ts — 22 tests; webhook-config + webhook-routing tests
smoke-creds.sh probes host-mode path (filesystem + token file, no docker exec)

Code notes (non-blocking)

apps/server/src/architect.tsmerge_pull_request in advertised surface

FORGEJO_TOOLS_ALLOWLIST includes merge_pull_request, so buildArchitectSdkOptions hands it to Claude in allowedTools. The canUseTool callback correctly denies it with a message at execution time, which is the right approach for a deny-with-explanation (vs. silent capability hiding). Functionally sound — the denial is SDK-enforced, Claude can't bypass it. A future hardening pass could additionally filter the entry out of allowedTools for the architect for defense-in-depth, but it's not a correctness issue here.

apps/server/src/architect.ts::runArchitectTurn — no unit test for the session-capture + message-append path

The session resume / setClaudeSessionId / appendMessage logic inside runArchitectTurn is exercised only through the real SDK in production, not in tests. The HTTP-handler tests use a fake worker so runArchitectTurn is never entered. The SQLite CRUD tests cover the DB primitives in isolation. This is an acceptable tradeoff given SDK calls can't be easily mocked, and the individual building blocks are all tested directly.

apps/server/src/db.ts::ensureDefaultForTypes — clean upgrade path for the architect-default row on existing non-empty deployments. Idempotent on every boot. Exactly right.

Summary

Clean, well-scoped implementation. The host-mode isolation boundary is properly gated, the singleton invariant is enforced at both boot (DB seed) and runtime (409 guard), security rails are in place, and the chat API plumbing is well-separated from the regular webhook-dispatch path. Test coverage is solid for a feature at this complexity level. Merging.

## Review: feat(architect): host-mode agent bootstrap (M18-4) **CI:** ✅ Green (run #1770, sha `839c7ae`, 3m59s) ### Acceptance criteria — all met | Criterion | Status | |---|---| | `types.architect` in agents.json (`container.enabled: false`, `plugins: []`, model 1M) | ✅ | | Singleton: `ensureDefaultForTypes` seeds `architect-default`; `POST /agents` 409 on second | ✅ | | `ResolvedAgent.host_mode` derived from `container.enabled === false` at merge time | ✅ | | `runAgentTask` branches on `host_mode`: skips worktree/container, uses `process.cwd()` | ✅ | | `HOST_MODE_TYPES` allowlist gate — other types with `enabled: false` fail boot loud | ✅ | | Architect loads isolated creds dir via `hostModeConfigDir()`; fallback to deterministic path | ✅ | | `canUseTool` denies `git push origin main/master` and `mcp__forgejo__merge_pull_request` | ✅ | | `resolveAgentByUser` / `resolveAgentByType` skip host-mode types | ✅ | | `NON_DISPATCHABLE_TYPES` carries explicit webhook block | ✅ | | Chat API: `POST /architect/chat`, `GET/DELETE /architect/sessions`, stream endpoint | ✅ | | `architect_sessions` SQLite table with id/created_at/updated_at/title/messages/claude_session_id | ✅ | | SSE stream reuses global `broadcastSSE` feed filtered on `task_id` | ✅ | | `claude_session_id` hidden from the GET transcript response | ✅ | | CLAUDE.md Roles table + Host-mode section | ✅ | | `architect.test.ts` — 22 tests; webhook-config + webhook-routing tests | ✅ | | `smoke-creds.sh` probes host-mode path (filesystem + token file, no docker exec) | ✅ | ### Code notes (non-blocking) **`apps/server/src/architect.ts` — `merge_pull_request` in advertised surface** `FORGEJO_TOOLS_ALLOWLIST` includes `merge_pull_request`, so `buildArchitectSdkOptions` hands it to Claude in `allowedTools`. The `canUseTool` callback correctly denies it with a message at execution time, which is the right approach for a deny-with-explanation (vs. silent capability hiding). Functionally sound — the denial is SDK-enforced, Claude can't bypass it. A future hardening pass could additionally filter the entry out of `allowedTools` for the architect for defense-in-depth, but it's not a correctness issue here. **`apps/server/src/architect.ts::runArchitectTurn` — no unit test for the session-capture + message-append path** The session resume / `setClaudeSessionId` / `appendMessage` logic inside `runArchitectTurn` is exercised only through the real SDK in production, not in tests. The HTTP-handler tests use a fake worker so `runArchitectTurn` is never entered. The SQLite CRUD tests cover the DB primitives in isolation. This is an acceptable tradeoff given SDK calls can't be easily mocked, and the individual building blocks are all tested directly. **`apps/server/src/db.ts::ensureDefaultForTypes`** — clean upgrade path for the architect-default row on existing non-empty deployments. Idempotent on every boot. Exactly right. ### Summary Clean, well-scoped implementation. The host-mode isolation boundary is properly gated, the singleton invariant is enforced at both boot (DB seed) and runtime (409 guard), security rails are in place, and the chat API plumbing is well-separated from the regular webhook-dispatch path. Test coverage is solid for a feature at this complexity level. Merging.
code-lead deleted branch boss/165 2026-04-20 18:57:29 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/claude-hooks!180
No description provided.