Adds three checks to host::agent::live_tests against a real container:
SigIgn/SigBlk match docker exec, re-opening /dev/stdout and /dev/stderr as
claude works through both, and a tty session ends when its client goes
away. Run against the pre-fix agent, it fails on SigIgn (SIGHUP ignored).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Execs reset every catchable signal to SIG_DFL and clear the signal mask
in pre_exec (raw rt_sigaction/rt_sigprocmask, so libc-reserved RT
signals are covered too). The supervisor's `trap '' HUP` no longer
leaks into execs, so a dropped tty attach hangs up its session again.
- runc's fixStdioPermissions: fds 0-2 are fchown'd to the exec user
(except /dev/null and EINVAL/EPERM/EROFS), so /dev/stdout, /dev/stderr
and a tty slave are usable by a non-root exec user.
- Own accept loop on hyper's http1 builder with a TokioTimer: a 10s
header-read timeout and a 256-connection semaphore (wait for a slot,
not drop). WebSocket upgrades still served; Config gains
header_read_timeout and max_connections.
- Attach/tunnel WebSockets accept messages and frames of at most 1 MiB.
- The disconnect SIGHUP is sent by the pump, and only while the exec is
unreaped, so a reused pgid is never signalled.
- Docs: removing TRIPLEC_AGENT_TOKEN does not scrub /proc/<pid>/environ;
recommend TRIPLEC_AGENT_TOKEN_FILE.
- Tests: inherited_signals.rs (agent with SIGHUP ignored), slowloris and
connection-cap tests, root-only stdio ownership test; fix the stale
$USER assertion in the existing root test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- entrypoint.sh: the agent token goes to a root-only file on /dev/shm
(tmpfs) instead of /run, so it never lands in the writable layer and
cannot ride `docker commit` into a snapshot image; the exec env snapshot
(which holds every API key in the container env) stays in memory.
- host/agent.rs: per-request timeouts on detached start, status polls,
resize and small-body reads; an overall bound on wait_exec_exit; uploads
get a size-proportional timeout. One-shot exec, attach and archive stay
unbounded on purpose (long commands, as with Docker).
- Also: Authorization marked sensitive; tokens validated as header-safe up
front; a 413 is passed through only if it is the OUTPUT_LIMIT message;
WebSocket messages capped at 1 MiB with stdin split into 64 KiB frames.
Tests: a hostile agent that accepts and never answers cannot hang the small
calls; bad tokens refused; a spoofed 413 is not passed through.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Crate moves to container/triple-c-agent (inside the image build context);
the Dockerfile's agent-build stage cross-compiles it on $BUILDPLATFORM for
$TARGETARCH (verified: buildx arm64 and the classic builder with the app's
build args both produce static binaries). The app's local build context
embeds the crate's Cargo.toml, Cargo.lock and src/ only.
- entrypoint.sh starts the agent from an env/cwd snapshot taken before the
script exports or cd's anything (minus bash's own PWD/SHLVL/_), so execs
see what `docker exec` would.
- Agent: no TRIPLEC_AGENT_* setting reaches an exec; a token file others can
read is refused (security review); upload symlink handling documented as
deliberate parity (claude has passwordless sudo, so no boundary).
- New ignored live test host::agent::live_tests runs every operation through
ContainerTarget::Docker and ::Agent on the same real container and requires
identical results. It caught a real divergence, now fixed in agent and
spec: docker exec sets only HOME, never USER/LOGNAME.
- CI runs the agent crate's tests in the preview test job.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A static musl binary (agent/, standalone crate) implementing
docs/superpowers/specs/2026-11-13-triple-c-agent-protocol.md: one-shot,
detached and attached execs (pipes or pty), exec status and resize, file
upload, archive download, TCP tunnels and activity counters, all behind a
constant-time bearer token. Matches docker exec: same user/initgroups,
working directory, env merge (PATH/TERM defaults, HOME/USER/LOGNAME from
passwd), 126/127 for commands that cannot start, 128+n for signals, the
exact OUTPUT_LIMIT message, and a process group per exec so the limit kill
and disconnect SIGHUP reach children.
Builds statically for x86_64 and aarch64 musl with no C toolchain
(aarch64 via rust-lld); commands in agent/README.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The agent runs inside the container, which is not trusted with the host.
- The shared HTTP client no longer follows redirects, so an agent cannot
point the host app's requests at another URL; a 3xx is an error.
- Error, exec-id and exec-status bodies are read through a 64 KiB cap
before parsing, and error text passed to the UI is cut to 4 KiB.
Regression tests use a hostile fake agent; the redirect test fails with the
fix removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
host/agent.rs implements every row of the protocol spec's client mapping
over reqwest (HTTP) and tokio-tungstenite (attach WebSocket): one-shot
execs rebuilt into docker/exec.rs's own OneshotOutput (413 passed through
verbatim), detached execs and exit polling with Docker's budget, attach
with stdout/stderr channel mapping, exit-frame recording and close_stdin
on shutdown, resize, uploads, streamed archive download and health.
AgentEndpoint's Debug redacts the token, and no error carries it.
docker/exec.rs shares its host-file reader, uid probe, one-shot
reassembly and exit-poll budget instead of the agent duplicating them;
Docker behaviour is unchanged. Lifecycle stop/remove refuse an agent
target, and has_exited is false for one (only the host can confirm).
Tests run the client against a small fake agent built with axum.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- entrypoint.sh: a TRIPLEC_AGENT_TOKEN env var is moved to a root-only file
and unset before anything else runs; when a token is configured, the agent
starts last (after setup, so health == ready) under a root supervisor loop,
matching the keepalive's survival rules.
- The app's local image build was missing three COPY sources (skills,
triple-c-open, triple-c-playwright-heal), so "build locally" could not
succeed. Added, with a test that every Dockerfile COPY source is in the
context.
- Local builds pass BUILDPLATFORM/TARGETARCH (the daemon's own platform):
the classic builder does not predefine them, and the agent build stage
will be pinned to $BUILDPLATFORM.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>