feat(secrets): per-repo secrets — vault to labctl broker exec (tracer bullet) #104
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/coding-lab#104
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
Per-repo secrets: named, operator-managed values an agent can use in commands without ever holding them in ambient env. End-to-end slice: operator creates a secret in the repo's settings UI → an instance uses it via
labctl secret exec→ the wrapped command sees it as an env var, nothing else does.Threat model (from the 2026-07-10 grill): accidental leakage by a trusted-but-sloppy agent — not hard isolation. This slice is the foundation; the harness layers (redaction, git guard, body sanitization, exposure detection) are separate follow-up issues.
Design:
name(env-var-shaped,^[A-Z][A-Z0-9_]*$, unique per repo),description(operator-written, shown to the agent), value encrypted at rest with the existing master key (AES-256-GCM, ADR-0006 style,nonce||ciphertext). Secrets are NOT vault credential kinds and have no cross-repo sharing.labctl secret list→ names + descriptions only, never values.labctl secret exec <NAME...> -- <cmd>→ fetches the named values viaLAB_TOKEN, injects each as$NAMEinto the child process env only (never argv, never the session env, never files), execs the command. Rotation is live: the broker fetches at exec time, no respawn.labctl secret exec), the-- sh -c '... $NAME ...'single-quote pattern (double quotes expand in the parent shell, to empty), spawn-time names + descriptions, and a pointer tolabctl secret listfor live inventory. Repos with zero secrets get no section.internal/secrets) that detects a secret value in a byte stream as its exact form plus base64, hex, and URL-encoded forms. Landed here (with tests, no consumer yet) as the shared kernel so the four harness follow-ups can proceed in parallel.Acceptance criteria
labctl secret listshows names + descriptions;labctl secret exec NAME -- sh -c 'echo $NAME'works inside an instance;envin the session shows no secret; value appears in no argvsecret execinjects, without respawnBlocked by
None - can start immediately
Agent Brief
Category: enhancement
Summary: Per-repo, operator-managed secrets that an agent can use only through
labctl secret exec(broker injection into the child process env) — the foundation slice, including the shared multi-encoding secret matcher.Current behavior:
Lab has a credential vault for git auth (ADR-0006: write-only, master-key AES-256-GCM encryption), but no way for an operator to hand an instance a named secret such as an API key. There is no ambient-env mechanism for repo secrets, no
labctl secretsurface, no secrets section in repo settings, and no secret-value matcher anywhere in the codebase.Desired behavior:
An operator creates a named secret in the repo's settings; an instance discovers it (
labctl secret list— names and descriptions only) and uses it (labctl secret exec <NAME...> -- <cmd>— value injected into the wrapped child's env as$NAME, nothing else). The value is never readable back through any API or UI, never appears in argv, the session env, or on disk. Rotation is live: the broker fetches values at exec time, so the nextsecret execpicks up a rotated value without respawn. Threat model is accidental leakage by a trusted-but-sloppy agent, not hard isolation.Key interfaces:
name(env-var-shaped,^[A-Z][A-Z0-9_]*$, unique per repo),description(operator-written, shown to agents), value encrypted at rest with the existing master key (AES-256-GCM,nonce||ciphertext, ADR-0006 style). Not a vault credential kind; no cross-repo sharing.secret exec, scoped so a run token can fetch only its own repo's secrets.labctl secret exec— fetches named values viaLAB_TOKENat exec time, injects each as$NAMEinto the child process env only, execs the command, preserves exit code.secret exec), the-- sh -c '... $NAME ...'single-quote pattern (double quotes expand in the parent shell, to empty), spawn-time names + descriptions, and a pointer tolabctl secret list. Repos with zero secrets get no section.internalsecrets package) that detects a secret value in a byte stream in its exact form plus base64, hex, and URL-encoded forms, correct across chunk boundaries (rolling window). Ships here with tests but no consumer — it is the shared kernel the four harness follow-ups (#105–#108) build on.Acceptance criteria:
labctl secret listshows names + descriptions;labctl secret exec NAME -- sh -c 'echo $NAME'works inside an instance;envin the session shows no secret; the value appears in no argvsecret execinjects, without respawnOut of scope:
Landing audit — PR #112 → PASS
Validated
feat(secrets): per-repo secrets — vault to labctl broker exec (tracer bullet)(afk/104).Signals relied on
ci / native:success(Forgejo Actions run — relied on, not re-run).origin/main: no conflict.Checks
Closes #104(AFK contract).repoSecretColumns) never selectencrypted_value; onlyRepoSecretValuesdoes, keyed by name — never embedded in theRepoSecretstruct.repoSecretJSONemits id/name/description/timestamps only; rotate takes a fresh value wholesale, never reads the old one back; cross-repo{sid}→ 404.info.RepoID, never client-supplied; partial miss → 404 naming the missing, zero values returned; decrypt failure → opaque 500 logging name+repo only.labctl secret exec: values land only incmd.Env— never argv,os.Setenv, or disk; fetched at exec time (live rotation); child exit code passes through.maxLen−1+ strictend > thresholdgive exactly-once detection across arbitrary chunk boundaries; encodings (base64 ×4, hex, url) covered.UNIQUE(repo_id, name),ON DELETE CASCADE.Independent review + a deep second-pass review each found no correctness bugs. Awaiting free-text merge go-ahead.