NK-WP-0056-T19 done: approval exercise receipt (16 checks against KeyCape 9e082dca, approval-engine f41a9ca)
All checks were successful
Build and Publish bounded-executor / build-and-push (push) Successful in 12s
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Identity provider journey acceptance / provider (push) Successful in 37s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 523928@bnt-lap001
Assistant-Session: 6c737afb-9c14-4152-9f02-477214b9c350
This commit is contained in:
tegwick 2026-11-03 23:16:06 +01:00
parent e1ef4bf6b1
commit 66dc397193
4 changed files with 146 additions and 3 deletions

View file

@ -361,7 +361,7 @@
| task | NK-WP-0056-T16 | wait | — | workplans/NK-WP-0056-bounded-executor-path.md |
| task | NK-WP-0056-T17 | done | — | workplans/NK-WP-0056-bounded-executor-path.md |
| task | NK-WP-0056-T18 | done | — | workplans/NK-WP-0056-bounded-executor-path.md |
| task | NK-WP-0056-T19 | todo | — | workplans/NK-WP-0056-bounded-executor-path.md |
| task | NK-WP-0056-T19 | done | — | workplans/NK-WP-0056-bounded-executor-path.md |
| task | NK-WP-0056-T20 | done | — | workplans/NK-WP-0056-bounded-executor-path.md |
| task | NK-WP-0056-T21 | done | — | workplans/NK-WP-0056-bounded-executor-path.md |
| intake | NK-IN-0001 | closed | blue | docs/intakes/activity-core-ops-sso-operators.md |

View file

@ -0,0 +1,68 @@
# NK-WP-0056-T19: executor approval exercise against the real services (2026-11-03)
Agent-run, local only, synthetic keys, everything removed afterwards:
`python3 bounded-executor/exercise_approval_identity.py --receipt FILE`. It follows secrets-engine's
`tools/exercise_approval_identity.py`.
**Setup:**
- **KeyCape:** a disposable container on the pinned image `9e082dca` (`sso-mfa/k8s/keycape/deployment.yaml`). It is
configured with two clients:
- the `client-from-case` output for case `nk-bind-executor-approval`;
- key-cape's example `approval-engine-operator`, which acts as the revoker and as a service that tries to approve.
- **approval-engine:** source `f41a9ca`, in-process, with its `JWTAuthenticator` against KeyCape's JWKS.
- **Executor:** the real code. That covers `cli.run_request`, `KeyCapeClientCredentials`, `ApprovalEngine`, and
`executor.Run.preflight` and `consume`. Only the OpenBao lane is replaced: the generated client secret is handed to
the client directly. The T18 tests cover that lane.
Receipt: [`receipt.json`](receipt.json). It is value-free; the harness asserts that no generated secret appears in it.
It records the image, the source commits, the executed files' hashes and the case hash. The commits are
`issuer_contract_commit` (key-cape), approval-engine, and net-kingdom `e1ef4bf` (executor code from `625ad65`).
| Check | Result |
| --- | --- |
| The request phase creates the approval as `service:nk-bind-executor` through a verified KeyCape JWT | passed |
| Create with another actor | 403 |
| Preflight before approval | refused, `approval_status` |
| A service (`approval-engine-operator`) approves a human-control approval | 403, no entry |
| A human aal2 entry approves | `approved` |
| The executor's preflight accepts KeyCape's aal2 entry | passed (after the fix below) |
| Consume | `consumed`; the read-back shows the admission digest and this run's id |
| Retry with the same digest | idempotent |
| A second run on the consumed approval | refused before consume |
| A different digest | 409; the consumption is unchanged |
| A changed admission | refused, `approval_binding` |
| Revoke before consume | refused by the executor and by approval-engine (409). The executor's own token cannot revoke (403) |
| A human aal1 entry | accepted by approval-engine (no floor declared), refused by the executor (`approval_entry`) |
| The executor client asks for `approval:approve` | KeyCape refuses with `invalid_profile_usage` |
| A token for another scope | 403 on create and on consume |
| A wrong client secret | refused at the token endpoint |
**Limitations:**
- approval-engine runs from source, not from the deployed image.
- The founder's entry is seeded through approval-engine's store. It uses the exact assurance object KeyCape mints and
the API persists, but there is no human PKCE login.
- There is no informed-decision presentation; the T21 tests cover it.
- Plain HTTP on loopback.
## Finding that changed the executor
**Every real founder approval would have been refused.** approval-engine stores an entry's `assurance` as the
verified token's claim, serialized as JSON: `{"at", "level", "methods", "mfa", "source"}`. KeyCape mints that object
in `token.go` `assuranceClaim`, and approval-engine stores it in `api.py`. The executor compared it with the bare
string `"aal2"`, which the fakes supplied. Against the real store, preflight refused with `approval_entry`.
The fix (`625ad65`): `executor.entry_is_aal2` parses the object and requires level `aal2` or `aal3` with `mfa: true`.
Anything else fails closed, including a bare `"aal2"`. The fakes use the real shape, and a unit test covers the
refusals.
## Consequences
- **The pinned executor image needs rebuilding.** The image pinned in the rapp by T18 (`c67cb044`, `b90951a`) has the
defect. It must be rebuilt from `625ad65` or later and re-pinned before T06.
- **Open for the founder.** approval-engine accepts an aal1 human entry unless the approval declares
`required_assurance` (APPROVAL-WP-0007-T01). Today the executor is the only aal2 guard. The request phase could
declare `required_assurance: {"level": "aal2"}` so approval-engine also enforces the floor at approve and consume
time. That needs the deployed approval-engine to be at schema v7 or later, and a check that informed-decision's
present_existing accepts the field.

View file

@ -0,0 +1,59 @@
{
"schema": "netkingdom.bounded-executor.approval-exercise/v1",
"task": "NK-WP-0056-T19",
"target": "disposable local processes; synthetic keys and credentials",
"started_at": "2026-11-03T22:15:58+00:00",
"keycape_image": "forgejo.coulomb.social/coulomb/key-cape@sha256:9e082dca78f65a4af3e92d3de3705b99e49ca9b298d9562ad0d37511c4306f8d",
"issuer_contract_commit": "82b471fc368a952fa4bbe83e8c8cfcfe56c443b9",
"approval_engine_commit": "f41a9ca82ce0e491e7846c5e2ae33fcbd3bde667",
"net_kingdom_commit": "e1ef4bf6b19b211c62f4ff1ff5c430af78d64250",
"executor_source_sha256": {
"cli.py": "b6cb53a7c71a06c61daf02ff1740d065cd754b1b5d2640571cce2b3f44ad8ff2",
"clients.py": "f0ce580f1beba91b7154051a35c088205de01f26d63a119a23bd926fe1c772cb",
"executor.py": "3da69db9cdd0ba7241246204dd92a57a84779f078ea3cd6d2705f3a8cc11b544"
},
"case_sha256": "32ba48b4131e22c993bbf8e705bd4f371a0e11222b069279cd196f2b880003c5",
"keycape_client": {
"clientId": "nk-bind-executor-approval",
"audience": "approval-engine",
"allowedScopes": [
"approval:create",
"approval:read",
"approval:consume"
],
"grantTypes": [
"client_credentials"
],
"serviceSubject": "service:nk-bind-executor",
"tenant": "tenant:platform",
"tokenLifetime": "15m"
},
"limitations": [
"approval-engine runs from source in-process, not the deployed image",
"the founder's aal2 entry is seeded through approval-engine's store; no human PKCE login",
"the client secret is handed to the client directly; the OpenBao lane is covered by the T18 tests",
"no informed-decision presentation (T21 tests cover the memo against its real validator)",
"plain HTTP on loopback; production uses KeyCape's public HTTPS issuer"
],
"checks": {
"request_phase_creates_as_executor_via_verified_keycape_jwt": true,
"create_with_another_actor_refused": true,
"preflight_refuses_unapproved": true,
"service_approve_of_human_control_refused": true,
"human_aal2_entry_approves": true,
"executor_preflight_accepts_keycape_aal2_entry": true,
"executor_consumes_once_and_confirms_by_read_back": true,
"same_digest_retry_idempotent": true,
"second_run_on_consumed_approval_refused_before_consume": true,
"different_digest_refused": true,
"changed_admission_refused_by_binding_digest": true,
"revoke_before_consume_refused_by_executor_and_engine": true,
"aal1_human_entry_refused_by_executor": true,
"executor_client_refused_approve_scope_by_issuer": true,
"token_for_another_scope_refused_by_engine": true,
"wrong_client_secret_refused": true
},
"status": "passed",
"cleanup_complete": true,
"finished_at": "2026-11-03T22:16:00+00:00"
}

View file

@ -9,7 +9,7 @@ flavor: design
owner: claude
topic_slug: netkingdom
created: "2026-10-30"
updated: "2026-10-31"
updated: "2026-11-03"
related:
- NK-WP-0054
- NK-WP-0040
@ -805,7 +805,7 @@ also needs T20 and T21.
```task
id: NK-WP-0056-T19
status: todo
status: done
priority: medium
assignee: claude-code
depends_on: [NK-WP-0056-T18]
@ -818,6 +818,22 @@ approval-engine's real source and KeyCape's pinned image with synthetic keys (se
**Done when:** the exercise passes and its receipt is recorded.
**Done 2026-11-03** ([evidence](../docs/evidence/2026-11-03-nk56-t19-approval-exercise/README.md)). The script is
`bounded-executor/exercise_approval_identity.py`. It runs the real request phase, KeyCape client, preflight and consume
against KeyCape `9e082dca` and approval-engine `f41a9ca`. 16 checks pass. Covered: create, approve, consume,
idempotent retry, different digest, changed admission, revoke-before-consume, wrong scopes and wrong secret, a service
approving, and an aal1 entry.
**Finding:** approval-engine stores an entry's assurance as KeyCape's JSON object, not a bare `aal2`. The executor
would have refused every real founder approval. Fixed in `625ad65` (`entry_is_aal2`). The request-phase tests no
longer expire with the example window.
Follow-ups:
- rebuild the executor image from `625ad65` and re-pin it in the rapp (T18's `c67cb044` has the defect);
- founder decision: should the request phase also declare `required_assurance: aal2`, so approval-engine enforces
the floor?
## Executor audit sender
```task