feat(secrets): broker output redaction in labctl secret exec #105
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/coding-lab#105
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
What to build
labctl secret execpipes the wrapped command's stdout and stderr through a redactor: any occurrence of an injected secret's value — exact, base64, hex, or URL-encoded form (the matcher from #104) — is replaced with[REDACTED:NAME]before the bytes reach the terminal, and therefore before they can reach the transcript or chat. This kills the two biggest accidental-leak paths at the source: an API echoing the key back in an error response, and a casualsecret exec FOO -- printenv.Implementation notes:
Acceptance criteria
[REDACTED:NAME]secret exec NAME -- printenv NAMEshows only the redaction placeholderBlocked by
Agent Brief
Category: enhancement
Summary:
labctl secret execpipes the wrapped command's stdout and stderr through a redactor so any injected secret value — exact, base64, hex, or URL-encoded form — reaches the terminal (and thus the transcript and chat) only as[REDACTED:NAME].Current behavior:
labctl secret exec <NAME...> -- <cmd>(landed in #104) fetches the named values at exec time, injects them into the child process env, and streams the child's stdout/stderr through untouched. A command that echoes a value — an API error response quoting the key, or a casualprintenv— puts the plaintext on the terminal. The streaming matcher (secrets.NewMatcher/Matcher.Feedin the shared secrets package) exists with tests but has no consumer yet.Desired behavior:
The broker scans both child streams for the values it injected for THIS exec (it already holds them — no extra fetch) and replaces every occurrence, in any of the four derived forms, with
[REDACTED:NAME]before the bytes are written out. A value split across write chunks is still caught (rolling buffer; held-back bytes flushed safely on stream close). Exit code is preserved; stdout stays stdout and stderr stays stderr; non-secret bytes pass through byte-identical. The child runs on pipes, not a TTY — acceptable and documented (agents don't run interactive commands through the broker). Redaction hits are quiet: no operator alert, no log entry — this is the harness working, not an incident.Key interfaces:
secret execcode path in the labctl broker — wrap the child's stdout/stderr pipes with a redacting writer/reader before forwardingsecrets.NewMatcher(map[string]string)andMatcher.Feed(chunk) []Match— matches report name, form, and position; the redactor must hold back a tail window so a match straddling a chunk boundary can still be rewritten before emission. AMatcheris not safe for concurrent use — one matcher (or serialised access) per stream[REDACTED:NAME]where NAME is the secret's env-var nameAcceptance criteria:
[REDACTED:NAME]secret exec NAME -- printenv NAMEshows only the redaction placeholderOut of scope:
PR #116 landing — BLOCKED (needs rework), not merged.
#116 passed CI in isolation, but it was tested against a
mainthat did not yet contain #108 (PR #118, merged just now). The two collide and cannot coexist as-is:internal/secrets/redactor.go+redactor_test.go— add/add conflict. Both #116 and #108 add a new file at this path, each declaringtype Redactor/func NewRedactorin packageinternal/secretswith incompatible signatures:NewRedactor(dst io.Writer, m *Matcher)— a streamingio.Writerredactor (Write/Flush) for broker stdout/stderr.NewRedactor(values map[string]string)— a whole-stringRedact(s string) (string, []string)for chat render.Even if the file conflict were force-resolved, the two definitions duplicate-declare
Redactor/NewRedactor→ compile break. This is a design reconciliation, not a mechanical merge.internal/labctl/labctl.go— content conflict in thesecretsubcommand help text (#116 annotatesexecwith redaction; #106/PR #119, also now on main, added ascanline). Trivial union, but it must be redone on top of the new main.Rework needed before #116 can land: rebase
afk/105onto currentmainand reconcile the two redactors — e.g. rename this streaming one toStreamRedactor(or fold both into one type exposing both a streaming and a whole-string entry point), then resolve thelabctl.gohelp-text union. Re-run CI after the rebase (its green predates #118/#119 and no longer reflects main).Migration note: the
0008→0009 run_titlerenumber this PR carries is now redundant —mainalready renumbered it (and #108 added0010_repo_secret_exposure); the current sequence is a gapless0001–0010with no duplicates. The rebase should drop the redundant renfor a clean diff.#116 stays open in this blocked state for an AFK re-run or manual rebase.