Skip to content

fix: bypass DooD socat proxy so /afk agent turns stream#71

Merged
lsfera merged 1 commit into
mainfrom
fix/socat-exec-stream
Jun 28, 2026
Merged

fix: bypass DooD socat proxy so /afk agent turns stream#71
lsfera merged 1 commit into
mainfrom
fix/socat-exec-stream

Conversation

@lsfera

@lsfera lsfera commented Jun 28, 2026

Copy link
Copy Markdown
Owner

Problem

Every /afk claude-tier iteration came back as an empty "Agent started → Agent stopped" turn with zero commits, regardless of issue, image, or claude-code version (2.1.181/193/195 all failed identically).

Root cause

The docker-outside-of-docker devcontainer feature fronts the host socket with a socat proxy (to fix permissions):

socat UNIX-LISTEN:/var/run/docker.sock,fork,... UNIX-CONNECT:/var/run/docker-host.sock

socat tears down docker exec's hijacked bidirectional stream after the first data burst. sandcastle streams the agent's stream-json over docker exec, so the agent's system/init line arrives, then everything after — the actual turn — is dropped. The exec close handler's exitCode: code ?? 0 masks the severed stream as a clean exit, so it surfaces as a silent empty turn.

Evidence

Test Result
docker exec from the host streams fully (claude completes)
Same from inside the devcontainer only the init line, then dies
echo a; sleep 1; echo b; sleep 1; echo c via socat socket only a
Same via docker-host.sock (bypass socat) a b c

Fix

A guarded resolveDockerHost() called first thing in main() points the orchestrator's docker CLI at the real socket (docker-host.sock, which the feature also exposes and grants docker-group access to):

  • No-op when socat isn't in play — a bare docker compose up (the cockpit distribution path) has no docker-host.sock; its raw /var/run/docker.sock streams natively and is left untouched. (A static docker-compose.yml env can't express this condition — the image has no entrypoint, so docker-host.sock only exists under the devcontainer lifecycle — which is why the fix lives in TypeScript, not compose.)
  • No-op when DOCKER_HOST is already set — explicit choice wins.
  • Pure decider with 4 unit tests; propagates to sandcastle's child docker processes via process.env.

Verification

With the fix live, two full autonomous /afk cycles implemented and merged real PRs (#69 cockpit Slice A, #70 Slice B) — commits=1 completed=true each. Suite: 131 pass, typecheck clean.

🤖 Generated with Claude Code

The docker-outside-of-docker feature fronts the host socket with a socat
proxy (for permissions). socat tears down `docker exec`'s hijacked
bidirectional stream after the first data burst, and sandcastle streams the
agent's stream-json over `docker exec` — so the agent's init line arrives,
the rest of the turn is dropped, and every iteration ends as an empty
"started → stopped" turn with zero commits (the exec close handler's
`code ?? 0` even masks it as a clean exit).

Point the orchestrator's docker CLI at the real host socket
(docker-host.sock, which the feature also exposes and grants docker-group
access to) via a guarded resolveDockerHost() called first thing in main().
It is a no-op when socat isn't in play — a bare `docker compose up` has no
docker-host.sock and its raw socket streams natively — or when an explicit
DOCKER_HOST is already set. Pure decider with unit tests.
@lsfera lsfera merged commit 22fd564 into main Jun 28, 2026
3 checks passed
@lsfera lsfera deleted the fix/socat-exec-stream branch June 28, 2026 15:32
lsfera added a commit that referenced this pull request Jun 28, 2026
)

Switch SandboxRunner from run() to createSandbox + await using +
hooks.onSandboxReady + promptFile/promptArgs so the library owns
worktree creation and deterministic teardown instead of the adapter.

- SandboxRunner.runIssue uses `await using sandbox = await createSandbox()`
  then `sandbox.run()` with promptFile/promptArgs; scope-bound disposal
  replaces bespoke try/finally teardown
- buildAgentInput extended to accept IssueInput; returns the full config
  for both createSandbox() and sandbox.run() (agent, imageName, network,
  copyToWorktree, onSandboxReady, promptFile, promptArgs)
- In-sandbox setup (opencode.json relocation) moves to hooks.onSandboxReady
- Agent prompts move to prompt-claude.md / prompt-local.md templates with
  {{ISSUE_NUMBER}}/{{ISSUE_TITLE}}/{{ISSUE_BODY}} substitution via promptArgs
- buildPrompt/buildLocalPrompt string builders removed
- 6 new unit tests covering promptFile, promptArgs, and network forwarding
- ADR-0019 records the two asymmetries that stay imperative (MTU creation,
  socat host override) and their relation to ADR-0006/0013 and #71
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant