feat(architect): relay chat turns via docker exec (#1132) #1135
No reviewers
Labels
No labels
area:agents
area:dashboard
area:database
area:design
area:design-review
area:flows
area:infra
area:meta
area:security
area:sessions
area:webhook
area:workdir
security
type:bug
type:chore
type:meta
type:user-story
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/agent-hooks!1135
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "code-lead/1132"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
DockerExecClaudeAgent, aClaudeAgentPortadapter that relays architect chat turns throughdocker exec -iinto the persistentclaude-hooks-architect-defaultcontainer (provisioned in #1131) via the existingensureExecShimmechanism. Drop-in forSdkClaudeAgent— same streamingAsyncIterable<TaskEvent>shape, samecanUseToolwiring, same session-resume contract.ArchitectAgentDispatchvia the existingclaudeAgent?: ClaudeAgentPortinjection point, so the production chat path now relays into the container while tests keep injecting fakes (no docker required).CLAUDE_CONFIG_DIRto the in-container bind target so the in-container CLI reads.credentials.jsonfrom the mounted dir, not the host path the dispatch layer set.touchesHostSecretto catch both the in-container path (/home/claude/.config/claude-code/...) and the spec's/claude-configshorthand; the architect tool policy now denies Write/Edit on those paths too, alongside the existing Read/Bash coverage.Closes #1132.
Test plan
bun test apps/server/src/infrastructure/agent/docker-exec-adapter.test.ts(10 new tests covering request shaping +TaskEventforwarding from mocked subprocess output)bun test apps/server/src/http/handlers/architect.test.ts(existing 91 tests + 5 new ones for the architect tool policy under/claude-config)bun test apps/server/src/domain/agent/agent-runner.test.ts(extendedtouchesHostSecrettable with 4 new denied cases + 1 new allowed case)just qa— typecheck + biome + 3332 tests + 1137 web tests + sql/paraglide/i18n/flow checks🤖 Generated with Claude Code
APPROVED ✅
CI green. All AC items from issue #1132 verified against the implementation.
AC walkthrough
DockerExecClaudeAgentimplementsClaudeAgentPortdocker-exec-adapter.ts— thin wrapper, delegates to inner portAsyncIterable<TaskEvent>interface)yield* this.inner.runTask(wrapped)— transparent pass-throughclaudeAgent?injection pointarchitect-agent-dispatch.tsinjectsnew DockerExecClaudeAgent(...)CLAUDE_CONFIG_DIR→ in-container bind targetCONTAINER_CLAUDE_CONFIG_DIR(/home/claude/.config/claude-code) before the spreadresumeSessionId) pass-through{ ...req, env, pathToExecutable: shim }preserves all other fields includingresumeSessionIdAbortControllerreference spreads through untouched; inner SDK adapter handles itonSteerQueued/onSteerDeliveredforwarded inArchitectAgentDispatch.dispatch()toolPolicydenies Read/Bash/Write/Edit under/claude-configtouchesHostSecrettouchesHostSecretupdated for/claude-configand in-container path/srv/claude-configurations/not blocked)architect.test.tstests passDockerExecClaudeAgentTaskEventforwarding testdocker-exec-adapter.test.tsShim contract alignment
The three shim contract vars (
CLAUDE_HOOKS_CONTAINER,CLAUDE_HOOKS_DOCKER_CWD,CLAUDE_HOOKS_DOCKER_ENV) match the names theensureExecShimshell script reads verbatim (container.tslines 270-272, 302-304).CLAUDE_CONFIG_DIRis inARCHITECT_FORWARDED_ENV, so the-e NAMEforwarding propagates the already-overridden in-container value into the container. The chain is correct end-to-end.Regex boundary analysis
The
/claude-configbranch ofHOST_SECRET_PATH:The boundary character classes correctly reject
v(from/srv/) as a preceding char, so/srv/claude-configurations/spec.mdis not blocked whilels /claude-configandcat /claude-config/.credentials.jsonare. Both sides are covered by the new test cases inagent-runner.test.ts. The Write/Edit guard inbuildArchitectRunRequestcalling the sametouchesHostSecretcloses the loop — previously only Read+Bash were checked.Non-blocking observation
resumeSessionIdhas no explicit adapter test. The "passes through prompt / model / abort / cwd unchanged" test checks four fields but notresumeSessionId. The correctness is obvious (...reqspreads everything), but if this file is touched in a follow-up, addingresumeSessionId: "prev-session-42"tobaseRequest()and asserting it survives the adapter would close the coverage gap neatly.