execverb: add a sealed clause to block trailing caller args (enable strict single-resource verbs) #173
Labels
No labels
burndown-2026-06
sunday-sprint
coherence-core
consult
headless
interactive
P0
P1
P2
P3
P4
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
coilyco-flight-deck/cli-guard#173
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?
Goal
Make a
can rungrant able to forbid trailing caller args, so a pinnedargvverb forwards exactly its fixed command and nothing else. Today every exec leaf appends the caller's args unconditionally, so even a fully-pinnedargvcan be widened by the caller. This blocks strictly-single-resource verbs. Motivating case: a ward-kdl verb that reads only the forgejo k8s secret and cannot be pivoted to any other secret.Current behavior (verified in
cli/execverb/)actionFor(cli/execverb/execverb.go, ~line 240) builds the child argv as:argsis the caller's trailing tokens, always appended. Preflight checks run first but none can require "zero trailing args":gateFuncgates run first, butgateRegistryis closed - only"aws-read"is registered (execverb.go ~line 209). No seam to add a custom or generic gate.checkFlagPolicy(execverb.go ~290) governs flags only (allow/deny-flags), not positional trailing args.checkWhens(deny-when/only pass/never pass) constrains arg values by glob, but cannot express "there must be no caller args at all."Concrete failure: a grant
can run read { argv get secret forgejo-runner-secrets -n forgejo -o "go-template={{.data.api-token | base64decode}}" }is widened by... read othersecret -n kube-system- the extra tokens append and kubectl reaches another secret. The pin is not a seal.Deliverable A (primary) - a
sealedclauseAdd a grant-level keyword
sealed(rejected alternatives:seal,no-args,exact) parsed inside thecan runblock:cli/execverb/guardfile.gowhereGrantis parsed; addSealed boolto theGrantstruct (alongsideArgv []string/ArgvSet bool, guardfile.go ~58-70).actionFor(execverb.go), wheng.Sealedis set, reject any non-emptyargsbefore the append, failing closed with a clear UserError (e.g."sealed verb takes no caller arguments"). The forwarded argv is exactlyArgvPrefix + ExecArgv().sealedrequiresArgvSet(a pinnedargv). A sealed verb with no pinned argv is a parse error - a sealed bare subcommand is meaningless and must not silently mean "run the bare binary."sealedwithoutargv.Example once shipped:
Deliverable B (optional, larger - scope separately if it grows)
Open the closed
gateRegistryso guardfiles can declare richer named gates beyondaws-read- e.g. a genericgate exact-argv, or a host-registerable gate seam viaConfig. This is the general form of "custom gating";sealedis the minimal special case of it. The missing "Config.Gatesseam" is explicitly called out in the ward repo atcmd/ward-kdl/ward-kdl.brew.guardfile.kdllines ~23-30 (the ward#92 / ward#95 decision that forced a hand-written-gated-Go workaround). Closing this seam retires that workaround class.Acceptance
can rungrant withsealed+ a pinnedargvforwards exactly the pinned command; any caller arg is refused, with a test proving the pivot is blocked.sealedis set withoutargv.docs/execverb.md(and the guardfile/passthrough docs) documentsealed.Files
cli/execverb/guardfile.go(Grant parse + struct),cli/execverb/execverb.go(actionForsealed check, before the argv append ~line 248),cli/execverb/execverb_test.go,docs/execverb.md. Precedent for the workaround this removes: ward repocmd/ward-kdl/ward-kdl.brew.guardfile.kdllines ~23-30.When done
Run the gate, commit, push to canonical
main, update docs/FEATURES inventory if cli-guard keeps one, and comment on the dependent ward forgejo-key issue that thesealedclause is available.execverb: add ato execverb: add asealclause to block trailing caller args (enable strict single-resource verbs)sealedclause to block trailing caller args (enable strict single-resource verbs)🔒 Reserved by
ward agent --driver claude— containerengineer-claude-cli-guard-173on hostkais-macbook-pro-2.localis carrying this issue (reserved 2026-07-01T04:43:37Z). Concurrentward agentruns are blocked until it finishes or the reservation goes stale (2h0m0s TTL);--forceoverrides.— Claude (she/her), via
ward agentWARD-OUTCOME: done -
sealedclause landed on main: anargvgrant can now forbid trailing caller args, forwarding its pinned command exactly.The core of this one was genuinely a one-liner in
actionForplus a struct field and a parse-time check - the deny-by-structure engine was already shaped sosealedslotted in beside the existingargvoverride with no friction. Thesealed-requires-argvinvariant fell out naturally next to the existingwildcard-forbids-argvguard, which was a nice sign the grammar was pointing the right way.What actually fought back was the docs, not the code.
docs/execverb.mdwas sitting at 3982 chars against a hard 4000-char pre-commit cap, so adding one honest grammar bullet blew the budget by ~600 chars and I spent more effort trimming redundant prose across unrelated bullets than writing the feature. It's under cap now, but that doc is one keyword away from needing a split (anargv/sealedsub-doc would be the clean move) - worth a follow-up before the next grammar addition. Merge to main also hit a conflict with the concurrentspecgen -> kdl-specsrename, resolved cleanly.Confidence is high: three targeted tests cover the three required behaviors (pinned-argv forwards, trailing arg refused, parse error without argv), plus a describe-surface test, and the full suite is green (the only failures are the pre-existing sandbox seccomp/ptrace tests that need privileges this container lacks). Deliverable B (opening
gateRegistryfor custom named gates) was optional and I left it out to keep the change tight - worth its own issue if a second consumer wants a custom gate.x
WARD-OUTCOME: done -
sealedclause landed on main; the retrospective is two comments up.Housekeeping: the bare
xcomment just above was an accidental artifact from a verification command, and comment deletion is denied by policy so I can't remove it. The real retrospective and status line are the earlierWARD-OUTCOME: donecomment. Nothing about the code or the merge changed.