feat(secrets): broker output redaction in labctl secret exec (#105) #116
No reviewers
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/coding-lab!116
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "afk/105"
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?
The broker now scans the wrapped command's stdout and stderr for the values it injected for THIS exec and rewrites every occurrence — exact, base64 (4 alphabets), hex (2 cases), or URL-encoded — to
[REDACTED:NAME]before the bytes reach the terminal, and therefore before they can reach the transcript or chat.What's in here
internal/secrets—Matchgains a chunking-invariantStartoffset; newRedactor, anio.Writerover the Matcher kernel. It holds back a tail window (longest-pattern−1 bytes) so a value split across write chunks is rewritten before emission, coalesces overlapping matches (no byte covered by any match is ever emitted — safety over fidelity), flushes the held tail safely on stream end, and poisons itself on underlying-writer errors. The type doc proves the window arithmetic and why Flush cannot leak.internal/labctl—runSecretExecwrapsenv.Stdout/env.Stderrin one matcher+redactor each (a Matcher is not concurrency-safe; os/exec copies each stream from its own goroutine), and flushes both unconditionally before deciding the return code, so a child that prints a secret and exits non-zero still gets its tail redacted. Child exit code passes through verbatim; stdout stays stdout, stderr stays stderr; non-secret bytes are byte-identical. The child now runs on pipes, never a TTY — documented as deliberate (agents don't run interactive commands through the broker). Contained hits are quiet: no alert, no log, no plaintext anywhere (grill decision).Acceptance criteria → proof
[REDACTED:NAME]:TestRedactor_EachForm(all 9 derived encodings), e2eTestSecretExecRedactsPrintedValue/TestSecretExecRedactsEncodedFormsTestRedactor_ChunkingInvariance— identical output for one Write, every 2-way split position, byte-at-a-time, and uneven cycles (pipes can't force chunk boundaries reliably, so this criterion is pinned at the unit layer)TestSecretExecRedactionPreservesExitCode(prints secret, exits 5),TestSecretExecStderrRedactedAndSeparate,TestSecretExecArgvIsExactCommandsecret exec NAME -- printenv NAME→ placeholder only:TestSecretExecPrintenvShowsPlaceholder(real coreutils printenv)Also verified against the real binary: a stub agent API +
bin/labctl secret exec— plain echo,printenv, a genuinebase64pipe, stderr+exit 7, and clean passthrough all behave per contract.Also: fix(store) migration collision (#115)
Verifying this issue required a green suite, which surfaced that main is currently broken:
0008_repo_secrets.sql(#104) and0008_run_title.sql(#111) both claim goose version 8, sostore.Openfails everywhere (server cannot boot, store-backed tests red). First commit renumbersrun_title→0009in both dialects — the safe direction, since no DB can have applied run_title as 8 (a tree with both files fails before applying anything) while the main lineage applied 8 = repo_secrets. Filed and explained in #115.Verification:
go test ./...green (sole exception:internal/tmuxxintegration tests, which fail in this sandbox on pristine main too — package has zero in-repo deps, unrelated);go test -racegreen forinternal/secrets+internal/labctl;golangci-lint run0 issues;gofmtclean;CGO_ENABLED=0 go build ./...clean.Closes #105
Closes #115
🤖 Generated with Claude Code
https://claude.ai/code/session_01Lwh1BiNqYbLRWmTwCZBhtY
migrations/{sqlite,postgres}/0008_run_title.sql (#111) collided with 0008_repo_secrets.sql (#104): the #111 branch was cut before #104 merged, so both landed on main claiming goose version 8. goose rejects duplicate versions at provider construction, so store.Open failed for both dialects — the server could not boot on main and every store-backed test was red (TestMigrationParity catches it). The later-merged migration yields: run_title becomes 0009. Safe for every real lineage — goose records applied versions by number, any DB migrated before the collision has 8 = repo_secrets (#104 merged first), and no DB can have applied run_title as 8 because a tree with both files fails before applying anything. Closes #115 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lwh1BiNqYbLRWmTwCZBhtY