Skip to content

feat(examples): E9-T3 adversarial denial demo (#115) - #164

Open
jingbofeng wants to merge 1 commit into
mainfrom
claude/0115-adversarial-denial-demo
Open

jingbofeng wants to merge 1 commit into
mainfrom
claude/0115-adversarial-denial-demo

Conversation

@jingbofeng

Copy link
Copy Markdown
Collaborator

Linked ticket

Closes #115

Review context

  • Task packet: work/0115-adversarial-denial-demo/task.md
  • Review focus: Fixture placement decision — the Team B fixture ships demo-local at examples/adversarial/testdata/team-b-unauthorized.yaml instead 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 fixture policy.ReferenceBundle). Prints [DENIED] with real Decision evidence — 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---task usage 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

  • Contract or boundary changed: None. The demo consumes internal/app, internal/authorization, and internal/policy read-only. The shared E1-T1 contract manifest is unchanged.
  • Deferred / non-goal: Live-gateway integration stage (post-E4-T2, recorded in [E9-T3] Demonstrate adversarial denial before claim creation #115); adding an authorization-outcome fixture class to the shared E1-T1 contract (would be its own E1 ticket — see rationale).

Fixture-placement rationale (for reviewers): The packet originally targeted harness/fixtures/contract/v0/.../invalid-team-b-unauthorized.yaml + a manifest.json case. 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 an invalid case, and there is no authorization error category — only schema ones. The contract also asserts every valid claim-request fixture is the canonical Team A request (api/v1alpha1/claim_request_test.go) and hard-codes fixture counts there and in ui/contractgen/main_test.go. Landing it in the manifest would reshape a merged contract and edit api/v1alpha1/ tests, which the packet lists as out of scope. Owner approved demo-local placement; recorded in the packet's Decisions.

Verification

Gate Exact command or artifact Result
focused tests go test -count=1 -v ./examples/adversarial/... pass
denial run go run ./examples/adversarial --task examples/adversarial/testdata/team-b-unauthorized.yaml[DENIED], policy=reference-default-deny@1, claimsCreated=0, exit 1 pass
malformed run go run ./examples/adversarial --task <missing-fields>.yamlrequired-field: spec.templateRef, exit 1, no panic pass
repository baseline (Go) gofmt -l, go vet ./..., go mod tidy (clean), go test -count=1 ./... pass
./scripts/check.ps1 -All full PowerShell gate blocked

./scripts/check.ps1 -All is blocked locally because pwsh is 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

  • Not applicable: this change does not touch runtime or application-facing contracts; it consumes the merged authorization/policy paths read-only and imports no provider SDK.

Risks and blockers

  • Risks: The demo-local fixture placement deviates from the packet's literal Target; behavior and acceptance criteria are unchanged. Flagged above for reviewer confirmation. If a shared authorization-outcome fixture class is wanted, it should be a dedicated E1 ticket.
  • Blockers: None.

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>
@jingbofeng

Copy link
Copy Markdown
Collaborator Author

👋 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 (harness/fixtures/contract/v0/.../invalid-team-b-unauthorized.yaml + a manifest.json case). I placed it demo-local at examples/adversarial/testdata/team-b-unauthorized.yaml instead.

Why: a Team B ClaimRequest is schema-valid (principal is out-of-band, never in the request), so it can't be an invalid manifest case, and there's no authorization error category — only schema ones. The merged contract also asserts every valid claim-request fixture is the canonical Team A request (api/v1alpha1/claim_request_test.go) and hard-codes fixture counts there and in ui/contractgen/main_test.go. Landing it in the manifest would reshape a merged contract and edit api/v1alpha1/ — which the packet lists as out of scope.

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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[E9-T3] Demonstrate adversarial denial before claim creation

1 participant