feat(examples): E9-T3 adversarial denial demo (#115) - #164
jingbofeng wants to merge 1 commit into
Conversation
Add examples/adversarial: submits a schema-valid engineer ClaimRequest under an out-of-band, unauthorized Team B principal and evaluates it through the merged E2 authorization path against the reference default-deny bundle. Prints [DENIED] with real Decision evidence (principal, policy ID/version, decision ID, reason) before any claim is created, and exits non-zero. Malformed input exits non-zero without panic. Focused tests assert the claim store stays empty on denial. The Team B fixture ships demo-local at testdata/team-b-unauthorized.yaml rather than the shared E1-T1 contract manifest: a Team B request is schema-valid (principal is out-of-band), so it cannot be an `invalid` manifest case, and the merged E1-T1 contract requires every `valid` claim-request fixture to be the canonical Team A request. Placing it in the manifest would reshape a merged, out-of-scope contract. Flagged for reviewer confirmation. Ticket: #115 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
👋 Reviewers — one decision needs your explicit sign-off before merge: Team B fixture placement. The packet's Target/Scope named the shared E1-T1 contract manifest ( Why: a Team B ClaimRequest is schema-valid (principal is out-of-band, never in the request), so it can't be an If you'd rather the shared contract grow an authorization-outcome fixture class, I think that's a dedicated E1 ticket. Happy to file it. Otherwise this keeps 0115 in scope with identical acceptance behavior. Decision is recorded in the packet's Decisions section (2026-09-17). |
Linked ticket
Closes #115
Review context
work/0115-adversarial-denial-demo/task.mdexamples/adversarial/testdata/team-b-unauthorized.yamlinstead of the shared E1-T1 contract manifest the packet's Target/Scope named. Please confirm this is acceptable (rationale below).MVP-path outcome
Realizes the PRD's Declarative request + authorization resolution denial path: a teammate can run the Team B scenario locally and watch the governance boundary reject an unauthorized principal, with inspectable evidence, before any claim is created — no backend, no credentials.
Changes
examples/adversarial/main.go: reads a ClaimRequest (YAML or JSON) via--task, composes the unauthorized Team B principal out-of-band, and evaluates it through the merged E2 authorization path (internal/authorization+ a fixturepolicy.ReferenceBundle). Prints[DENIED]with realDecisionevidence — principal subject,policy=<id>@<version>, decision ID, denial reason — before any claim exists; exits non-zero on denial and on malformed input; never panics. Output carries a[FIXTURE MODE]label.examples/adversarial/main_test.go: denial case (asserts the injected claim store stays empty), malformed-input case (non-zero, no panic, no policy evaluation), and missing---taskusage guard.examples/adversarial/testdata/team-b-unauthorized.yaml: demo-local, schema-valid engineer request (identical shape to what Team A may run).README.md,docs/project-status.md: quickstart entry and delivered-status note.Scope and deferrals
internal/app,internal/authorization, andinternal/policyread-only. The shared E1-T1 contract manifest is unchanged.Fixture-placement rationale (for reviewers): The packet originally targeted
harness/fixtures/contract/v0/.../invalid-team-b-unauthorized.yaml+ amanifest.jsoncase. That merged contract cannot represent this case: a Team B ClaimRequest is schema-valid (the principal is supplied out-of-band, never in the request), so it is not aninvalidcase, and there is no authorization error category — only schema ones. The contract also asserts everyvalidclaim-request fixture is the canonical Team A request (api/v1alpha1/claim_request_test.go) and hard-codes fixture counts there and inui/contractgen/main_test.go. Landing it in the manifest would reshape a merged contract and editapi/v1alpha1/tests, which the packet lists as out of scope. Owner approved demo-local placement; recorded in the packet's Decisions.Verification
go test -count=1 -v ./examples/adversarial/...go run ./examples/adversarial --task examples/adversarial/testdata/team-b-unauthorized.yaml→[DENIED],policy=reference-default-deny@1,claimsCreated=0, exit 1go run ./examples/adversarial --task <missing-fields>.yaml→required-field: spec.templateRef, exit 1, no panicgofmt -l,go vet ./...,go mod tidy(clean),go test -count=1 ./..../scripts/check.ps1 -All./scripts/check.ps1 -Allis blocked locally becausepwshis not installed on this dev host; the Go portion of that gate was reproduced by hand (row above) and CI runs the full gate.Backend neutrality
Risks and blockers