ADR 0018: aidokit verify facet umbrella
| Field | Value |
|---|---|
| Status | Accepted |
| Date | 2026-05-26 |
| Deciders | Project maintainers |
| Supersedes | — |
| Superseded by | — |
| Related ADRs | ADR-0011 (Schema library / Zod), ADR-0017 (Strict-tier capability artifact) |
Summary
aidokit verify is extended from a narrow install-integrity check into
a pluggable facet runner. Facets implement the VerifyFacet
interface in @aidokit/core/verify. Five facets are reserved at v1.x:
secrets, budget, deps, license, loop-cap. Only secrets
ships fully implemented; the other four are reserved stubs that emit
FACET_NOT_IMPLEMENTED warnings when enabled.
The umbrella gives the user one discoverable surface
(aidokit verify --secrets, --budget, --deps, --license,
--loop-cap, --all-facets), one shared policy file
(.aidokit/policy.json), one output formatter set (human, --json,
--sarif), and one aggregated exit code.
Context
The 25-pain-point analysis surfaces several independent failure modes that a structured AI workflow needs CI-side gates for:
- #19 Secrets / credential leakage through agents.
- #22 Unbounded token / cost budget.
- #4 Dependency / supply-chain drift.
- #24 License contamination from suggested code.
- #18 Runaway autonomy / loop blow-up.
Each is a static or quasi-static scan operating on the working tree (or staged diff) and emitting findings with a severity. They share the same shape: configurable rules, allowlist, severity, output formatters, exit-code semantics.
The existing aidokit verify [--capabilities] already owned the
"verify install integrity" surface. Two design pressures shape this
ADR:
- Surface multiplication risk. Shipping
aidokit secrets,aidokit budget,aidokit deps-check,aidokit license,aidokit loop-capas five top-level commands grows the CLI surface 5× for what is semantically one capability ("scan and report"). Discoverability suffers; documentation duplicates. - Policy sharing. Each facet needs project-local configuration (allowlists, severity overrides, paths to skip). Five separate config files is worse than one.
Decision
1. One umbrella command, N facets
aidokit verify keeps its existing --capabilities mode and grows
five new facet flags:
aidokit verify --secrets [--all] [--sarif] [--json]
aidokit verify --budget # stub, emits FACET_NOT_IMPLEMENTED
aidokit verify --deps # stub
aidokit verify --license # stub
aidokit verify --loop-cap # stub
aidokit verify --all-facets # run every enabled facet
--all selects scan scope (full tree vs staged diff; default staged).
--sarif emits SARIF 2.1.0 for GitHub code-scanning ingestion.
--json emits the standard CLI envelope.
2. Facets implement VerifyFacet in @aidokit/core/verify
export interface VerifyFacet {
id: 'secrets' | 'budget' | 'deps' | 'license' | 'loop-cap';
run(ctx: VerifyContext, policy: VerifyPolicy): Promise<VerifyFinding[]>;
}
A facet returns findings with { severity: 'error' | 'warn', code,
file, line?, message, rule? }. The umbrella aggregates findings
across enabled facets, formats them, and computes exit code:
- Exit
0when noerrorfindings (warn-only is non-fatal). - Exit
1when anyerrorfinding present.
3. Single policy file: .aidokit/policy.json
Emitted at init across all three adapters, byte-identical fixture. Carries per-facet enable/severity/allowlist settings:
{
"specVersion": "2.0",
"facets": {
"secrets": { "enabled": true, "severity": "error", "allowPaths": [] },
"budget": { "enabled": false, "severity": "warn", "limits": {} },
"deps": { "enabled": false, "severity": "warn" },
"license": { "enabled": false, "severity": "warn", "allowed": ["MIT","Apache-2.0","BSD-*"] },
"loop-cap":{ "enabled": false, "severity": "warn", "maxIterations": 50 }
}
}
4. Cross-adapter, CLI-owned
All facet logic lives in @aidokit/core/verify and the aidokit
CLI. No adapter package is touched. .aidokit/policy.json is part
of the cross-adapter .aidokit/ directory (alongside state.json,
capabilities.json, model.lock) — byte-identical across the three
adapter fixtures.
5. Stub facets are real entries, not silent no-ops
budget, deps, license, and loop-cap are registered in the
facet runner. Invoking them emits a FACET_NOT_IMPLEMENTED warn
finding with a link to the tracking issue, so users get an explicit
signal rather than a silent success. The reserved CLI flags document
forward intent and let early-adopters pin their policy.json against
the eventual schema without breakage.
Consequences
Positive
- One discoverable surface. Users learn
aidokit verifyand get every scan facet via one help page. - Shared infrastructure. Output formatters (human, JSON, SARIF), exit-code aggregation, severity model, allowlist parser — written once, reused N times.
- Single policy file. Auditors and reviewers read one file to understand what the project gates on.
- Forward-compatible. Stub facets reserve namespace; later
implementation is a
run()body change, no CLI surface change. - Adapter-neutral. Lives in
@aidokit/core+aidokit— no adapter rewrite to add a facet later.
Negative
- Help text grows.
aidokit verify --helplists six modes (--capabilitiesplus the five facets). Mitigated by grouping in the help output. - Stubs are user-visible.
--budgetetc. exist and warn rather than erroring "unknown flag". Some users may file bugs assuming the facet is broken; theFACET_NOT_IMPLEMENTEDcode is the documented signal.
Neutral
.aidokit/policy.jsonlifecycle joins.aidokit/state.jsonand.aidokit/capabilities.json. All three are CLI-emitted artifacts consumed by downstream tooling — the pattern is established.
Alternatives considered
Alternative 1: Five top-level commands
aidokit secrets, aidokit budget, aidokit deps, aidokit license,
aidokit loop-cap as siblings of aidokit verify.
Pros
- No flag explosion under one command; each command's
--helpis focused. - Per-command discoverability in shell completions.
Cons
- Surface multiplies 5× for what is semantically one capability.
- Each command would re-invent output formatters, exit-code semantics, policy loading.
--all-facetshas no natural home — running all of them becomes a bash one-liner.
Reason rejected: the surface-multiplication cost outweighs the discoverability win, especially given the shared infrastructure.
Alternative 2: Two commands split by failure-class
aidokit scan for static checks (secrets, license, deps) and
aidokit limit for budget / loop-cap (runtime-quasi-static).
Pros
- Semantically honest — the two classes have slightly different failure modes.
- Smaller per-command help text than the umbrella.
Cons
- Still grows the top-level surface.
- The split is leaky: license is partly runtime (dependencies pulled in), budget is partly static (max iterations declared in policy).
- Users have to remember which command owns which facet.
Reason rejected: the boundary is not crisp enough to justify the extra command.
Alternative 3: Push facets into aidokit doctor
Re-use the existing doctor command as the scan surface.
Pros
- One fewer command.
Cons
doctoris install-health, not project-scan. Conflating the two makes both jobs muddier.doctorruns implicitly during other commands (sync, add adapter); scan facets are explicit CI gates.
Reason rejected: ownership semantics differ — keep doctor for install health, verify for project gates.
Implementation notes
- Facet registry lives in
packages/core/src/verify/facets.ts. secretsfacet implementation: 11 bundled patterns (AWS, GCP, Stripe, GitHub PAT, generic JWT, private-key PEM headers, etc.) plus Shannon entropy scoring on long opaque strings, plus path allowlist frompolicy.json, plus inline// aidokit-ignore-secretoverride (line-scope).- SARIF emitter:
packages/core/src/verify/sarif.tsproduces SARIF 2.1.0 with oneruns[]entry per facet. .aidokit/policy.jsonschema defined with Zod in@aidokit/core/schema(ADR-0011). Validation errors emitPOLICY_INVALIDin the standard envelope.
Validation
This decision is validated when:
- [ ]
aidokit verify --secretsdetects all 11 bundled-pattern positives in a smoke fixture and respects inline overrides. - [ ]
aidokit verify --budget(et al.) emits a singleFACET_NOT_IMPLEMENTEDwarn finding and exit code 0. - [ ]
.aidokit/policy.jsonis byte-identical across the three adapter reference fixtures. - [ ]
--sarifoutput validates against the SARIF 2.1.0 schema. - [ ] Exit-code aggregation: warn-only across all enabled facets produces exit 0; any error finding produces exit 1.
References
Internal
- ADR-0011 — Schema library (Zod) — used for
policy.jsonschema. - ADR-0017 — Strict-tier capability artifact — same CLI-emits-artifact
pattern, same
.aidokit/directory. docs/specs/cli-reference.md—verifysurface (frozen at v1.0).docs/specs/security-model.md— secrets-leakage threat (T-prefix).
External
- SARIF 2.1.0 specification — OASIS Committee Specification.
- GitHub code-scanning — SARIF ingestion endpoint.