feat(execverb): allow inspect-list passthrough sugar (N read-only binaries per wrap) #157
Labels
No labels
burndown-2026-06
sunday-sprint
coherence-core
consult
headless
interactive
P0
P1
P2
P3
P4
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
coilyco-flight-deck/cli-guard#157
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?
Summary
Add an
allow <bin...>node to the execverb passthrough dialect: a singlewrapblock that opens a read-only passthrough funnel for each binary in a flat list, mounting each as a sibling subcommand. This is the "inspect tier" of the passthrough concept — the mega-list of read-only OS primitives (ls grep tail cat jq ps df ...) that ward should always let through without a hand-written verb.It complements the host tier already shipped on
feat/passthrough-ssh-dialect:passthrough <bin>wraps one mutating primitive whole (systemctl,launchctl), one binary per file. Inspect is the same idea for read-only binaries, but collapsed into one flat list so we don't carry 30 near-identical single-binary wrap files.Tiering (the organizing principle, decided in discussion):
grep,cat,ps,df,jq). One mega-file, oneallowline, grows freely, never reviewed for safety.systemctl,launchctl,mkdir,chmod). Onepassthroughfile per binary, reviewed when it grows.git,kubectl,aws) stays a declared verb. A binary only enters passthrough if its whole surface fits one tier; that rule is what letsallowstay a dumb, unreviewed flat list.Why a new keyword (not overloaded
passthrough)passthrough <bin> [prefix...]already assigns trailing tokens to the fixed argv prefix (passthrough tailscale sshexecstailscale ssh <args...>). Sopassthrough ls grepwould mean "execlswith a fixedgrepprefix", not "two binaries". The list form must therefore be its own node. Keyword:allow(verb of admission; no quantifier ambiguity likeevery/all; distinct from the existing grant-bodyallow-flag, which is a different scope — call this out in the doc to avoid reader confusion).Grammar
allow <bin...>— wrap-body node, exec/passthrough dialect. For each<bin>, mount a leaf equivalent to a nestedwrap ward-kdl <bin> { passthrough <bin> }: exec that binary, implicitcan run "*"funnel, mounted as the sibling subcommandward-kdl <bin>. Names are bare binaries only — no fixed argv prefix (that is what singlepassthroughis for); reject a name containing a path separator or shell metachar, fail-closed.exec,passthrough, andcan runin the same wrap. A wrap is single-binary or an allow-list, never both. Emit the same fail-closed error style as the existingexec/passthroughexclusivity atcli/execverb/guardfile.go:141-143.allowwith no args mounts nothing — fail closed, mirrorguardfile.go:131-133).never pass <token>andonly pass when <selector> is <glob>(guardfile.go:157-165) attach to every generated leaf, same as they do for a singlepassthrough. Inspect won't usually need a host gate (read-only), but the composition must hold so e.g. anever passspeed-bump can scope the whole list.wrap ward-kdlwith no subpath. The wrap arg list is just["ward-kdl"](the binary/group); theallownames are the leaves. The parser currently requires a singleBin— see Implementation.Mount / runtime semantics
Each allowed binary is an independent open-passthrough surface (
can run "*"funnel) — identical engine path to a singlepassthrough <bin>:actionForenforces wrap-level guards then execs (cli/execverb/execverb.go:197-208). Auditing, the sandbox jail, and grandchild re-entry are inherited unchanged from the wildcard funnel —allowadds no new engine behavior, only a fan-out over names.Implementation
Repo:
forgejo.coilysiren.me/coilyco-flight-deck/cli-guard, branch offfeat/passthrough-ssh-dialect(where the passthrough dialect lives;maindoes not have it yet).The crux:
Parsereturns one*Guardfilewith a single scalarBin(cli/execverb/guardfile.go:13-24,:128-130).allowis N binaries from one wrap, so the single-Binassumption has to flex on this path. Two viable shapes, implementer's choice:allowparses into a slice the driver expands into one passthrough*Guardfileper name (Group["ward-kdl", <bin>],Bin=<bin>,passthrough=true, sharing the wrap'sWhens). Cleanest: reuses the existing single-binary mount path verbatim, the fan-out is purely a parse-time desugar.Allow []stringfield, and havemountWildcard(cli/execverb/execverb.go:88-127) loop, building one wildcard leaf per name with its own bin. Requires threading a per-leaf bin whereBinis read today.Prefer (1) — it keeps the engine's single-Bin invariant and makes
allowa pure front-end desugar over the already-tested passthrough path.Touch points (branch
feat/passthrough-ssh-dialect):cli/execverb/guardfile.go— addcase "allow":toapplyNode(:138-168), aparseAllowthat validates bare-binary names and exclusivity. Relax theBin == ""check (:128-130) for the allow path (it has no single Bin), or set up the desugar so each synthesized member still satisfies it.cli/execverb/execverb.go— if going with shape (1), the desugar likely lands where members are built (driver side,http/specdrv/specdrv.go:83-112reads members); if shape (2), extendmountWildcard.cli/execverb/describe.go— render each allow leaf in the describe surface (:122open-passthrough rendering already exists; fan it out per name).docs/passthrough.md— add an## Inspect lists (allow)section: grammar, the bare-binary constraint, the why-not-overload-passthroughreasoning, and theallowvsallow-flagscope distinction.Tests (cli/execverb/*_test.go)
allow a b c→ three passthrough leaves named a/b/c, eachWildcard/funnel.allow+execin one wrap;allow+can run; emptyallow; a name with/or a shell metachar.ward-kdl ls,ward-kdl grepare each callable and exec the right binary; args pass through.never pass rmandonly pass when shell hostname is ...apply to every allow leaf (reuse the host-gate test harness with the injectableHostResolver,execverb.go:25-27).Done condition
allow <bin...>parses, mounts one funnel per name, composes with wrap-level guards, rejects the exclusivity/empty/bad-name cases, is documented indocs/passthrough.md, and is covered by the tests above. Then cut a cli-guard release tag so ward can bump its dep (Phase 2).Phase 2 (separate ward issue — blocked on the cli-guard release above)
Filed against
coilyco-flight-deck/infrastructure's consumer is actuallycoilyco-flight-deck/ward(cmd/ward-kdl/). Not part of this issue; outline so nothing is lost:ward/go.modto the release carryingallow.cmd/ward-kdl/ward-kdl.inspect.guardfile.kdl— the megaallowlist (read-only set above; seed from the infrastructure script audit:cat grep find stat jq ps df ...).passthroughper binary (Kai's call: own file each, ~3 lines):systemctl,launchctl,journalctl,service, and the mutating coreutils kept as host primitives (mkdir mv cp chmod chown ln touch install). Discovery is automatic — the*.guardfile.kdlglob (http/specdrv/specdrv.go:127) merges them into theward-kdlbinary byGroup[0]; no registry edit.make build-ward-kdlto regen and move reference docs. Decide docs shape up front: onedocs/ward-kdl.passthrough.mdcovering the convention + the inspect file, rather than a 3-line doc stub per host binary.ward/docs/FEATURES.md.Context
Discussion origin: ward-kdl passthrough design, inspect-vs-host tiering. Host-tier grammar shipped on cli-guard
feat/passthrough-ssh-dialect(a476b69), designed in cli-guard#130 / #123. The untracked wardssh/tailscale-sshguardfiles already consume the host tier; this issue adds the inspect tier they're missing.🔒 Reserved by
ward agent claude— containerward-cli-guard-issue-157-claude-4c18e3b8on hostkais-macbook-pro-2.localis carrying this issue (reserved 2026-06-19T07:18:45Z). Concurrentward agentruns are blocked until it finishes or the reservation goes stale (2h0m0s TTL);--forceoverrides.— Claude (she/her), via
ward agent🔒 Reserved by
ward agent claude— containerward-cli-guard-issue-157-claude-567a60d6on hostkais-macbook-pro-2.localis carrying this issue (reserved 2026-06-24T04:36:21Z). Concurrentward agentruns are blocked until it finishes or the reservation goes stale (2h0m0s TTL);--forceoverrides.— Claude (she/her), via
ward agent