* feat(#2008): add generic command-exit-zero gate-predicate evaluator Third-party capability gates declared via check.predicate were rendered for display but never evaluated (only built-in check.query gates fired; the security capability's gate worked solely via a hard-coded ship.md branch). Add a generic, deps-injected gate-predicate evaluator (src/gate-predicate-evaluator.cts) that dispatches by predicate.kind. Built-in kind: command-exit-zero — runs a bounded sh -c command at the project root (via shell-command-projection.execTool), inherits env, exit 0 => pass, non-zero => block, timeout => block, fail-closed. Wire a 'check predicate' subcommand into check-command-router.cts and extend the three generic workflow gate-dispatch sites (execute:wave:post, execute:post, plan:post) to route check.predicate gates to the new evaluator. The two-step gate contract (command-failure => onError; block => halt) is unchanged. - src/gate-predicate-evaluator.cts: pure leaf, KIND_TABLE extensible - src/check-command-router.cts: cmdCheckPredicate + buildPredicateDeps + parsePredicateFlags - docs/adr/2008-*, docs/reference/gate-predicates.md, docs/how-to/command-exit-zero-gate.md - tests: 38 unit + integration tests (exit mapping, timeout, interpolation, property-based bijection, malformed-predicate fail-closed, real subprocess e2e) Closes #2008 * docs(#2008): backfill changeset pr number 2011
4.5 KiB
Gate predicates (reference)
Diátaxis quadrant: Reference. This is the canonical specification of the capability gate
check.predicateevaluation path. For a step-by-step authoring guide, see How-to: add a command-exit-zero gate.
A capability gate's check block carries exactly one of three shapes
(query, predicate, agentVerdict), enforced by the registry validator
(capability-validator.cjs:validateGate). This page documents the
predicate shape and the kinds the built-in evaluator recognises.
Declaration
"gates": [
{
"point": "<loop-point>",
"check": {
"predicate": {
"kind": "<kind>",
"<kind-specific fields>"
}
},
"when": "<config-key>",
"blocking": true,
"onError": "halt"
}
]
The gate envelope (point, when, blocking, onError) follows the standard
contract documented in ADR-0894 (capability declaration format) and the
Loop Host Contract glossary entry in CONTEXT.md. This page covers only
check.predicate.
Evaluation path
- The loop-resolver (
gsd-tools loop render-hooks <point>) renders the active gate hook (including itscheck.predicatedeclaration) to the workflow. - The workflow gate-dispatch reads the hook in-context and, when the
checkshape ispredicate, runs:gsd_run check predicate --predicate '<predicate JSON>' [--phase-dir …] [--phase-number …] [--phase-req-ids …] --raw check-command-router.cts:cmdCheckPredicateparses the predicate, builds the production subprocess binding, and callsgate-predicate-evaluator.cjs:evaluatePredicate, which dispatches bypredicate.kind.- The evaluator returns the standard gate envelope:
{ "block": <bool>, "message": "<string>", "details": { … } } - The workflow applies the two-step gate contract unchanged:
- Step 1 — if the check command itself failed (non-zero exit, e.g. a
malformed predicate / unknown kind), route per
onError(haltorskip). - Step 2 — if the command succeeded, a
blocking: truegate halts onblock: true; an advisory gate showsmessageand continues.
- Step 1 — if the check command itself failed (non-zero exit, e.g. a
malformed predicate / unknown kind), route per
Built-in kinds
command-exit-zero
Runs a declared command in a bounded sh -c subprocess; exit 0 → pass,
non-zero → block, timeout → block. See ADR-2008 for the full sandbox
contract.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
kind |
string | yes | — | Must be "command-exit-zero" |
command |
string | yes | — | The shell command. Non-empty, ≤ 4096 chars |
timeout |
number | no | 30 |
Positive finite number, seconds |
Interpolation. Before execution, three placeholders are substituted from
the gate context; all others are left untouched for sh to interpret:
| Placeholder | Source | Workflow flag |
|---|---|---|
${PHASE_NUMBER} |
the active phase number | --phase-number |
${PHASE_DIR} |
the active phase directory | --phase-dir |
${PHASE_REQ_IDS} |
the phase's requirement ids | --phase-req-ids |
An undefined placeholder interpolates to the empty string.
Sandbox. cwd = project root; env = inherited from the GSD process; killed (SIGTERM) on timeout. The command runs as the user, on the user's machine — there is no sandbox boundary vs. the user's own shell. See ADR-2008 "Trust model".
Result mapping.
| Command outcome | block |
message |
|---|---|---|
| exit 0 | false |
command exited 0 |
| exit N (non-zero) | true |
command exited N: <stderr/stdout tail, ≤2000 chars> |
| timeout (SIGTERM) | true |
command timed out after <s>s: <tail> |
sh missing (ENOENT, exit 127) |
true |
command exited 127: sh: not found |
Validation errors (throw → check-command failure → Step-1 / onError).
- Missing, non-string, empty, or whitespace-only
command. commandlonger than 4096 chars.timeoutpresent but not a positive finite number.- Unknown
kind.
Extensibility
The evaluator dispatches through a KIND_TABLE. Adding a new built-in kind is
a one-line registration in gate-predicate-evaluator.cts — no workflow changes
required, since the workflow dispatches any check.predicate to the same
gsd_run check predicate subcommand.
Related
- ADR-2008 — full decision record.
- How-to: add a command-exit-zero gate.
- ADR-0894 — capability declaration format.
src/gate-predicate-evaluator.cts,src/check-command-router.cts.