ts-sidecar: attach carries to the standing mac-proxy box over ward-tailnet #349

Closed
opened 2026-06-25 21:55:40 +00:00 by coilysiren · 2 comments
Owner

What

Replace the per-run, ephemeral --ts-sidecar tailscale sidecar with attachment to a standing, shared mac-proxy SOCKS5 box over a known docker network. Implements the ward half of agentic-os#291. The infrastructure half (naming the ward-tailnet network on the mac-proxy compose stack) is a sibling issue - the shared contract is the network name ward-tailnet and the proxy hostname mac-proxy.

Why

Today (cmd/ward/container.go, container_compute.go) every --ts-sidecar carry mints its own <carry>-ts userspace tailscale node (hostname mac-proxy, key from SSM), joins its netns, and tears it down on exit. That churns ephemeral tailnet nodes per run and re-pays key injection each launch. The standing mac-proxy box already runs restart: unless-stopped in the same daemon, on 0.0.0.0:1055, so a carry just needs to share a user-defined network with it and dial it by name. One stable tailnet node, no per-run mint/teardown.

Design (locked in agentic-os#291)

  • Reuse the existing standing mac-proxy box (one box, two consumers: Mac-host tools via the published 127.0.0.1:1055, carries via the docker network).
  • Carry reaches the box over a shared user-defined docker network named ward-tailnet (default bridge does no name resolution, so this is required). NOT a shared netns - shared netns across many carries collapses them into one loopback and collides ports.
  • Box convergence stays in ansible (authoring-vs-rollout doctrine). ward only attaches and preflights, never stands the box up at launch.

Changes

In cmd/ward/container_compute.go:

  • dockerArgvHead: for the p.TSSidecar case, replace --network=container:<sidecar> with --network=ward-tailnet (sole network - a user-defined bridge gives the carry both outbound egress and by-name reach to mac-proxy).
  • agentEnv: keep WARD_TOWER_OLLAMA=http://kai-tower-3026:11434, but change the proxy URL host from loopback to the box name: WARD_TS_SOCKS5=socks5h://mac-proxy:1055. Add a wardTailnetNetwork = "ward-tailnet" const and a proxyBoxHost = "mac-proxy:1055" const.
  • Delete tsSidecarRunArgv (and the tsSidecarName/tsSidecarSuffix/tsSidecarLabel/tsSidecarHostname machinery only the per-run sidecar used).

In cmd/ward/container.go:

  • Delete startTSSidecar, stopTSSidecar, sweepOrphanedSidecars, writeTSAuthEnvFile, and the tsAuthKeySSMPath const - key injection now belongs to the ansible-converged box, not ward.
  • Remove the orphan-sidecar sweep call in sweepStaleContainers.
  • Add a preflight on the tailnet path: verify docker network inspect ward-tailnet succeeds and the mac-proxy box is attached/running. On failure, error clearly: "standing tailnet proxy not found - converge the mac-proxy infra role (agentic-os#291)" rather than launching a carry that routes nowhere.

In cmd/ward/agent.go:

  • The flag no longer implies --aws: the key is no longer SSM-fetched by ward and the tower FQDN is a constant, so a tailnet carry has no SSM dependency. Drop the implied --aws for this flag and update the help text. (Confirm no other reason the flag needs ~/.aws.)
  • Keep the --ts-sidecar flag spelling for compatibility (it is now a slight misnomer - reframe the help to "attach to the standing tailnet proxy"); a --tailnet alias is optional and a nice-to-have, not required.
  • Update the --print output (the tsSidecarRunArgv echo at agent.go:1366 goes away; show the --network=ward-tailnet attach instead).

Tests + docs

  • Update cmd/ward/container_tssidecar_test.go and container_compute tests: assert the carry argv carries --network=ward-tailnet, env carries WARD_TS_SOCKS5=socks5h://mac-proxy:1055, and no per-run sidecar docker run is emitted.
  • Add a preflight test: missing ward-tailnet network produces the clear error.
  • Rewrite docs/agent-ts-sidecar.md to the standing-box model (one box, shared network, ansible-converged, ward attaches). Update docs/agent-host-net.md cross-references and docs/FEATURES.md.

Dependency / sequencing

The carry preflight passes only once the infra sibling has named ward-tailnet on the mac-proxy stack and the role has converged. The feature is opt-in (the flag), so landing ward first is safe - the preflight just errors until infra lands. Note this in the doc.

Done when

  • A --ts-sidecar carry on Docker Desktop attaches to ward-tailnet, and curl --proxy "$WARD_TS_SOCKS5" "$WARD_TOWER_OLLAMA/api/tags" reaches the tower's Ollama from inside the carry.
  • No <carry>-ts container is created, and pre-commit run --all-files plus the ward test suite are green.
## What Replace the **per-run, ephemeral** `--ts-sidecar` tailscale sidecar with attachment to a **standing, shared** mac-proxy SOCKS5 box over a known docker network. Implements the ward half of agentic-os#291. The infrastructure half (naming the `ward-tailnet` network on the mac-proxy compose stack) is a sibling issue - the shared contract is the network name `ward-tailnet` and the proxy hostname `mac-proxy`. ## Why Today (`cmd/ward/container.go`, `container_compute.go`) every `--ts-sidecar` carry mints its own `<carry>-ts` userspace tailscale node (hostname `mac-proxy`, key from SSM), joins its netns, and tears it down on exit. That churns ephemeral tailnet nodes per run and re-pays key injection each launch. The standing mac-proxy box already runs `restart: unless-stopped` in the same daemon, on `0.0.0.0:1055`, so a carry just needs to share a user-defined network with it and dial it by name. One stable tailnet node, no per-run mint/teardown. ## Design (locked in agentic-os#291) - Reuse the existing standing mac-proxy box (one box, two consumers: Mac-host tools via the published `127.0.0.1:1055`, carries via the docker network). - Carry reaches the box over a **shared user-defined docker network** named `ward-tailnet` (default bridge does no name resolution, so this is required). NOT a shared netns - shared netns across many carries collapses them into one loopback and collides ports. - Box convergence stays in ansible (authoring-vs-rollout doctrine). ward only **attaches and preflights**, never stands the box up at launch. ## Changes In `cmd/ward/container_compute.go`: - `dockerArgvHead`: for the `p.TSSidecar` case, replace `--network=container:<sidecar>` with `--network=ward-tailnet` (sole network - a user-defined bridge gives the carry both outbound egress and by-name reach to `mac-proxy`). - `agentEnv`: keep `WARD_TOWER_OLLAMA=http://kai-tower-3026:11434`, but change the proxy URL host from loopback to the box name: `WARD_TS_SOCKS5=socks5h://mac-proxy:1055`. Add a `wardTailnetNetwork = "ward-tailnet"` const and a `proxyBoxHost = "mac-proxy:1055"` const. - Delete `tsSidecarRunArgv` (and the `tsSidecarName`/`tsSidecarSuffix`/`tsSidecarLabel`/`tsSidecarHostname` machinery only the per-run sidecar used). In `cmd/ward/container.go`: - Delete `startTSSidecar`, `stopTSSidecar`, `sweepOrphanedSidecars`, `writeTSAuthEnvFile`, and the `tsAuthKeySSMPath` const - key injection now belongs to the ansible-converged box, not ward. - Remove the orphan-sidecar sweep call in `sweepStaleContainers`. - Add a **preflight** on the tailnet path: verify `docker network inspect ward-tailnet` succeeds and the `mac-proxy` box is attached/running. On failure, error clearly: "standing tailnet proxy not found - converge the mac-proxy infra role (agentic-os#291)" rather than launching a carry that routes nowhere. In `cmd/ward/agent.go`: - The flag no longer implies `--aws`: the key is no longer SSM-fetched by ward and the tower FQDN is a constant, so a tailnet carry has **no SSM dependency**. Drop the implied `--aws` for this flag and update the help text. (Confirm no other reason the flag needs `~/.aws`.) - Keep the `--ts-sidecar` flag spelling for compatibility (it is now a slight misnomer - reframe the help to "attach to the standing tailnet proxy"); a `--tailnet` alias is optional and a nice-to-have, not required. - Update the `--print` output (the `tsSidecarRunArgv` echo at agent.go:1366 goes away; show the `--network=ward-tailnet` attach instead). ## Tests + docs - Update `cmd/ward/container_tssidecar_test.go` and `container_compute` tests: assert the carry argv carries `--network=ward-tailnet`, env carries `WARD_TS_SOCKS5=socks5h://mac-proxy:1055`, and **no** per-run sidecar `docker run` is emitted. - Add a preflight test: missing `ward-tailnet` network produces the clear error. - Rewrite `docs/agent-ts-sidecar.md` to the standing-box model (one box, shared network, ansible-converged, ward attaches). Update `docs/agent-host-net.md` cross-references and `docs/FEATURES.md`. ## Dependency / sequencing The carry preflight passes only once the infra sibling has named `ward-tailnet` on the mac-proxy stack and the role has converged. The feature is opt-in (the flag), so landing ward first is safe - the preflight just errors until infra lands. Note this in the doc. ## Done when - A `--ts-sidecar` carry on Docker Desktop attaches to `ward-tailnet`, and `curl --proxy "$WARD_TS_SOCKS5" "$WARD_TOWER_OLLAMA/api/tags"` reaches the tower's Ollama from inside the carry. - No `<carry>-ts` container is created, and `pre-commit run --all-files` plus the ward test suite are green.
Author
Owner

🔒 Reserved by ward agent --driver claude — container ward-ward-issue-349-claude-0dba6943 on host docker-desktop is carrying this issue (reserved 2026-06-25T21:56:13Z). Concurrent ward agent runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); --force overrides.

— Claude (she/her), via ward agent

<!-- ward-agent-reservation --> 🔒 Reserved by `ward agent --driver claude` — container `ward-ward-issue-349-claude-0dba6943` on host `docker-desktop` is carrying this issue (reserved 2026-06-25T21:56:13Z). Concurrent `ward agent` runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); `--force` overrides. <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Author
Owner

Retro from the carry that landed this:

The issue read almost like a build sheet, so the Go itself was the easy part - swap the netns-join for a --network=ward-tailnet attach, repoint the proxy URL at mac-proxy:1055 by name, and rip out the per-run sidecar machinery (mint, teardown, orphan sweep, the SSM auth-key fetch). Dropping the --aws implication fell out naturally once nothing reached SSM anymore.

What actually fought back was everything around the code. The doc-size caps are brutal here: FEATURES.md and agent-flags.md both sit right at the 4000-char ceiling, so every sentence I added had to be paid for by trimming another - and then the merge with ward#348 landed on the exact same FEATURES.md line, so I got to re-fight that budget a second time. Also a surprise: trufflehog wasn't on PATH in this container, so the commit gate couldn't run at all until I fetched the release binary myself. For a "just rewire a flag" change that was an unexpected detour.

Confidence is high on the wiring - the attach argv, the by-name socks5h env, and the preflight (network exists + box attached) are all unit-tested, including a fake-docker test for the missing-network error. The honest gap is the same one #337 had: no live end-to-end, since that needs the ansible-converged box in hand. I flagged it in the doc as a follow-up.

Two rough edges worth a future ticket: the preflight only confirms mac-proxy is attached, not that SOCKS5 actually answers on :1055 - a real liveness probe would catch a wedged box. And the flag is still spelled --ts-sidecar though there's no sidecar anymore; kept for compat, but the name now slightly lies.

Retro from the carry that landed this: The issue read almost like a build sheet, so the Go itself was the easy part - swap the netns-join for a `--network=ward-tailnet` attach, repoint the proxy URL at `mac-proxy:1055` by name, and rip out the per-run sidecar machinery (mint, teardown, orphan sweep, the SSM auth-key fetch). Dropping the `--aws` implication fell out naturally once nothing reached SSM anymore. What actually fought back was everything _around_ the code. The doc-size caps are brutal here: `FEATURES.md` and `agent-flags.md` both sit right at the 4000-char ceiling, so every sentence I added had to be paid for by trimming another - and then the merge with ward#348 landed on the exact same `FEATURES.md` line, so I got to re-fight that budget a second time. Also a surprise: trufflehog wasn't on PATH in this container, so the commit gate couldn't run at all until I fetched the release binary myself. For a "just rewire a flag" change that was an unexpected detour. Confidence is high on the wiring - the attach argv, the by-name socks5h env, and the preflight (network exists + box attached) are all unit-tested, including a fake-docker test for the missing-network error. The honest gap is the same one #337 had: no live end-to-end, since that needs the ansible-converged box in hand. I flagged it in the doc as a follow-up. Two rough edges worth a future ticket: the preflight only confirms `mac-proxy` is _attached_, not that SOCKS5 actually answers on :1055 - a real liveness probe would catch a wedged box. And the flag is still spelled `--ts-sidecar` though there's no sidecar anymore; kept for compat, but the name now slightly lies.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
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
coilyco-flight-deck/ward#349
No description provided.