Skip to content

fix(container-gateway): make the macOS path work end to end - #1304

Merged
potiuk merged 1 commit into
mainfrom
fix/container-gateway-macos-path
Sep 21, 2026
Merged

potiuk merged 1 commit into
mainfrom
fix/container-gateway-macos-path

Conversation

@potiuk

@potiuk potiuk commented Sep 21, 2026

Copy link
Copy Markdown
Member

Summary

  • The committed gateway env block could never have worked. CONTAINER_HOST / DOCKER_HOST shipped a project-relative unix://./… value. A unix:// URL's authority is parsed as a host component, so unix://./x dials /.//x, unix://x dials /x/, and unix:x dials //. Only unix:///abs/path connects (verified against podman 6.1.0). An absolute path is per-machine, so both variables move out of the committed baseline into the gitignored .claude/settings.local.json, beside the allowUnixSockets pair.
  • Backend discovery failed on macOS whenever the caller's TMPDIR differed from the machine's. podman machine inspect renders the socket path from the caller's TMPDIR, so a hook, agent, or launchd service is told a path that does not exist and discovery reports no backend — on a host where podman works fine by hand. The finders now return candidate lists and discover takes the first that exists, with getconf DARWIN_USER_TEMP_DIR as the second podman candidate.
  • The TMPDIR troubleshooting entry prescribed a fix that cannot work. Claude Code sets TMPDIR when it builds the sandbox and that assignment wins over env.TMPDIR from any settings file, so the setting is accepted and silently does nothing. Doctor probe 4 now reports a shared session root as a pass instead of sending the operator after it.

Found while doctoring a sandbox where podman worked by hand but not through the gateway. Each defect alone is enough to break the macOS path end to end.

Type of change

  • Skill change (.claude/skills/<name>/) — eval fixtures updated below
  • Tool / bridge contract (tools/<system>/*.md)
  • Python package (tools/*/ with pyproject.toml)
  • Groovy reference impl
  • Cross-cutting (RFC, AGENTS.md, sandbox, privacy-LLM)
  • Documentation (docs/, README.md, CONTRIBUTING.md)
  • Project template (projects/_template/)
  • CI / dev loop (prek, workflows, validators)
  • Other:

Test plan

  • prek run --all-files passes
  • For Python packages touched: uv run pytest / ruff check / mypy passes
  • For Groovy bridges touched: command-line invocation tested end-to-end
  • For skill changes: eval suite passes for the affected skill
  • For skill behaviour changes: a new or updated eval fixture is included in this PR
  • Other: verified live on macOS — the gateway now starts from the SessionStart hook with no TMPDIR override and serves both sockets, and podman info reaches it from inside the sandbox through the absolute CONTAINER_HOST.

Evidence for the URL claim, gathered against podman 6.1.0:

CONTAINER_HOST dials result
unix://./.apache-magpie-local/run/podman.sock /.//.apache-magpie-local/… fail
unix://.apache-magpie-local/run/podman.sock /.apache-magpie-local//run/… fail
unix:.apache-magpie-local/run/podman.sock // fail
unix:///Users/…/.apache-magpie-local/run/podman.sock the socket connects

RFC-AI-0004 compliance

  • HITL — any new mutation is gated on explicit user confirmation
  • Sandbox — no new unrestricted host access; network reach declared in the adapter
  • Vendor neutrality — placeholders used in all skill / tool prose
  • Conversational + correctable — agentic-override path documented if behaviour is adopter-tunable
  • Write-access discipline — no autonomous outbound messages; drafts only, sent on confirmation
  • Privacy LLM — private content does not reach a non-approved LLM; redactor invoked where needed

No sandbox surface widens. The change removes two committed env values and records that the socket allow-list entries were always per-machine. The privacy-LLM tick covers the new copilot-runtime.md, which records the GitHub-hosted-model boundary rather than changing it.

Linked issues

Refs #1298 (container gateway), #1287 (Copilot harness), #1299 and #1300 (the code-review fixes back-filled into the spec).

Notes for reviewers

Adopter migration — this removes committed settings. Anyone who installed the gateway before this PR has CONTAINER_HOST / DOCKER_HOST in their committed .claude/settings.json. After upgrading they must move both to .claude/settings.local.json with absolute paths; setup-isolated-setup-verify 12c now fails a relative value and quotes it, so the upgrade path surfaces rather than silently leaving a broken gateway.

A spec claim is reversed. specs/container-gateway.md recorded both open questions as resolved in favour of the relative form. That conclusion was drawn from how the URL parses, not from a connection. The spec now says so explicitly, because the next person to read it should know the claim was tested and failed rather than never considered.

One unrelated fix rode along, because it blocked the commit. agent-isolation's test_git_signs_a_commit_through_the_wrapper failed ~1 run in 3 on pristine main under the full suite and passed in isolation. The cause is real and not test-only: the watcher polled lsof -U -n — which enumerates every unix socket on the machine — before the O(1) signing_in_flight check, and on a loaded host that enumeration outlasts the signature it is meant to observe, so the touch overlay never appeared. Asking the cheap question first keeps lsof out of the hot loop entirely on the wrapped path. 5/5 green after, was 1-in-3 failing. Happy to split it out if you'd rather review it separately.

Spec sync. .last-sync was 10 commits behind. Most of those commits updated their own specs inline; the two genuine gaps were the pr-management-code-review fixes and the Copilot harness, both now written. The marker moves to the current tip, so the claim it makes is true.

Probe 3's new check earns its place. Stripping unix:// leaves a relative path that stats successfully — the socket really does exist relative to the cwd — so the old probe passed the file check and the real cause disappeared into the generic failure branch. The explicit shape check is what makes the misconfiguration legible.

🤖 Generated with Claude Code

Three defects, found while doctoring a sandbox where `podman` worked by
hand but not through the gateway.

Backend discovery trusted `podman machine inspect` for the socket path.
That command renders the path from the *caller's* TMPDIR, not the
machine's, so a hook, agent, or launchd service whose TMPDIR differs is
told a path that does not exist and discovery reports no backend at all.
Only the basename is stable, so the finders now return candidate lists
and `discover` takes the first that exists, with the per-user temp
directory from `getconf DARWIN_USER_TEMP_DIR` as the second podman
candidate. `_docker_socket` gets the same shape: the context result and
the `~/.docker` fallback are now both candidates rather than either/or.

`CONTAINER_HOST` / `DOCKER_HOST` were committed in the baseline with a
project-relative `unix://./…` value. That can never have worked: a
`unix://` URL's authority is parsed as a host component, so `unix://./x`
dials `/.//x`, `unix://x` dials `/x/`, and `unix:x` dials `//`. Only
`unix:///abs/path` connects, and an absolute path is per-machine, so
both variables move out of the committed baseline and join the
`allowUnixSockets` pair in the gitignored `.claude/settings.local.json`.
The spec had recorded this as a resolved open question in favour of the
relative form, concluded from URL parsing rather than a connection; it
now records what actually happens.

The TMPDIR troubleshooting entry prescribed `env.TMPDIR` in project
settings to get a per-project scratch directory. Claude Code sets TMPDIR
itself when it builds the sandbox and that assignment wins, so the
setting is accepted and silently does nothing. The entry now says so,
and the doctor's probe 4 reports a shared session root as a pass instead
of sending the operator after a fix that cannot work. Probe 3 gains an
explicit check for a non-absolute value, because stripping `unix://`
leaves a relative path that stats fine and hides the real cause.

Also fixes an unrelated blocker found on the way: the touch overlay
polled `lsof -U -n` before the O(1) `signing_in_flight` check, and on a
loaded host that enumeration outlasts the signature it is meant to
observe, so the overlay never appeared and its test failed one run in
three. Asking the cheap question first keeps `lsof` out of the hot loop
on the wrapped path entirely.

Specs are synced to main: the two pr-management-code-review fixes
(#1299, #1300) are recorded in the family contract, the GitHub Copilot
harness from #1287 gets copilot-runtime.md, and .last-sync moves to the
current tip.

Generated-by: Claude Opus 5
@potiuk
potiuk merged commit 0e7012b into main Sep 21, 2026
12 checks passed
@potiuk
potiuk deleted the fix/container-gateway-macos-path branch September 21, 2026 00:45
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