T68d Policy Hardening Validation
T68d Policy Hardening Validation
Scope
T68d thickens the boundary that decides whether an agent may act on the world.
It adds a declarative policy-test runner that evaluates
principal/action/resource/context cases against the production
CedarPolicyAdapter and fails on any mismatch; structured explanations that
trace a decision to its determining policy, layer, and source statement with
secret-shaped values redacted before they can enter evidence; and versioned,
signed policy bundles whose digests are recomputed from the sources on every
verification, referenced from the sealed Execution Package by content digest.
31 cases across two suites, against a declared minimum of 15.
| Suite | Cases |
|---|---|
tests/unit/policy-hardening.test.mjs |
20 |
tests/unit/execution-package-repair-policy.test.mjs |
11 |
Deterministic gates
Both gates ran on a clean checkout detached at the implementation revision,
dispatched through full-validation.yml; each run’s artifact records the
revision and the profile.
| Command | Result |
|---|---|
pnpm gate:quick |
PASS — run 30672257071 |
pnpm gate:security |
PASS — run 30672258537 |
| Profile | Stages |
|---|---|
gate:quick |
format:check, lint, typecheck, test:unit, test:agent-readiness |
gate:security |
format:check, lint, typecheck, build, test:unit, test:architecture, test:qualification, test:security, test:fault |
Adequacy matrix
Anchored in .specs/features/policy-hardening/spec.md.
| Criterion | Requirement | Assertion |
|---|---|---|
| POL-01 | Declarative cases evaluate against the workspace policy set and fail on mismatch, in gate:quick |
policy-hardening.test.mjs - a passing corpus, a per-case mismatch report carrying the actual decision and code, an expected-code mismatch failing even when the decision matches, and malformed cases rejected. The runner executes inside test:unit, which gate:quick runs. Deviation stated plainly: there is no vestra policy test CLI verb, because the installed manifest exposes only init and adding commands is composition-root work (#64); the criterion’s substance - declarative cases gating on mismatch - is enforced, its CLI spelling is not |
| POL-02 | Denials carry the determining policy, failing attribute, and expected-versus-actual, codes unchanged | policy-hardening.test.mjs - explanations name the determining policy id, layer, and source statement; the id is asserted to be the real compiled ${layer}.${id}, not a placeholder; codes pass through unchanged |
| POL-03 | The sealed package references the active bundle by content digest; bundles versioned and signed | execution-package-repair-policy.test.mjs - policyBundleDigest seals verbatim, is covered by the payload digest, and coexists with onGateFailure; policy-hardening.test.mjs - deterministic, order-independent bundle digests under the trust-root signer |
| POL-04 | Digest mismatch, unknown version, or invalid signature fail closed with distinct errors | policy-hardening.test.mjs - modified source, swapped digest, forged signature, unknown field, a digest that does not reproduce even with a valid signature, and a source that does not match its recorded digest all fail |
| POL-05 | Explanations pass the redaction boundary; secrets never leave | policy-hardening.test.mjs - secret-shaped values are replaced before an explanation can enter evidence, and the redaction count is reported rather than silent |
Discrimination sensor
| Mutation | Criterion | Result |
|---|---|---|
| A case with the wrong decision passes anyway | POL-01 | KILLED |
| Explanation redaction returns the original value | POL-05 | KILLED |
| Per-policy source digests stop being recomputed on verify | POL-04 | KILLED |
The package accepts a policyBundleDigest that is not a digest |
POL-03 | KILLED |
| The explanation entry carries a placeholder instead of the real policy id | POL-02 | KILLED |
Two of these began as survivors, and the suite was strengthened until they
died rather than the mutations being dropped. Removing the per-policy digest
recomputation was invisible because every tampering test also broke the
bundle-level digest; the new case re-signs a tampered source whose recorded
digest is stale, which only the recomputation can catch. And nothing asserted
the explanation’s policy id was real, so a placeholder survived; the assertion
now requires the compiled ${layer}.${id} present in the decision.
Non-shallow checks
- The runner evaluates through the production
CedarPolicyAdapter, not a reimplementation, so a case that passes here is evidence about the engine that runs in the product. - A bundle whose recorded digests do not reproduce is tampered whatever its signature says; signature validity is necessary, never sufficient.
- A determining policy the view cannot name is surfaced as such instead of hidden, because the decision and the view disagreeing is itself a finding.
- Explanations are redacted before they exist as strings anywhere evidence can reach, not at display time.
Verdict
T68d is complete for its declared scope, with the POL-01 CLI-spelling deviation stated above rather than papered over. Five of five acceptance criteria have file-and-assertion evidence, both declared gates pass on the implementation revision through external runs, and every sensor mutation was killed - two of them only after the suite was strengthened, which is the sensor doing its job.
What this report does not assert: independent verification, or recorded human
acceptance - docs/qualification/REPORT-CONTRACT.md deliberately has no field
for either, and docs/merge-governance.md states why independence is not
obtainable by configuration in a single-collaborator repository.
