--- id: FLEX-WP-0035 type: workplan title: "Per-identity application access readback and readable protected-system registry for user-engine" domain: infotech repo: access-engine status: active owner: claude topic_slug: netkingdom created: "2026-10-09" updated: "2026-10-09" related: [USER-WP-0026, USER-WP-0028] state_hub_workstream_id: "cb9755fa-6824-5288-81c4-3aa2fbff1a9f" --- user-engine (message `f1637c32`, 2026-10-09, routed by net-kingdom `19a0cbcd`) needs to show a signed-in person which applications they can use. Its `USER-WP-0026-T03` and `USER-WP-0028-T02` are `blocked_on` access-engine. The PEP stance is already fail-closed: no decision renders `unavailable`, and allow is never inferred from membership, platform role, registration, or a missing record. ## What exists today (2026-10-09 review) - `POST /v1/batch_check` takes several resources in one call and returns one decision per resource, each with a `lifetime` (`expires_at`) per the decision envelope. Shape-wise this is close to request (1). - Caller auth authorizes the caller token against **every** `resource.system` in the batch (`cmd/flex-auth/main.go`, `/v1/batch_check`). The `flex-auth-user-engine` caller is bound to its own protected system, so a readback across other systems is a 403 today. That is correct behaviour for enforcement, and the reason this needs design rather than a config change. - There is no generic "may use this application" action. Each package has its own action vocabulary, so "access to system X" has no defined meaning yet. - There is no registry read endpoint. The protected-system registry is loaded from files and never served. ## Define the readback contract ```task id: FLEX-WP-0035-T01 status: done priority: high state_hub_task_id: "0585a684-d462-5caa-937f-b1730efddbd8" ``` Decide the action that a readback evaluates per system: a declared `application.access` action each consumer package must opt into, versus a per-system mapping held in the registry. A system that declares nothing is returned as `not_applicable`, never allow. State per input class the revocation-visibility deadline the readback carries (§9.7). Decide whether the readback is a decision for audit purposes (decision record, emission) or an explicitly non-enforcing advisory read. A displayed allow must not be usable as a PEP decision by another system. Gate: written contract in `docs/` plus an ADR or decision record; reviewed with user-engine before code. 2026-10-09: user-engine accepted the contract (message `f54df082`) with three amendments, now folded in: the `/v1/check` subject shape with `tenant`, the rendering table, and a separate `USER_ENGINE_READBACK_TOKEN_FILE`. Also added: a realm-level-claims-only rule. `FLEX-DEC-2026-019` is accepted. 2026-10-09: activated by the founder. Draft contract `docs/application-access-readback.md` and `FLEX-DEC-2026-019` (proposed). Key finding: each pin loads one consumer's package, so no existing pin can answer across applications. The draft proposes a dedicated readback pin with multi-package mode, a `purpose: "readback"` binding that keeps decisions non-transferable, and a new optional `client_binding` manifest field. Sent to user-engine with four review questions. The gate closes on their reply. ## Caller authorization for cross-system readback ```task id: FLEX-WP-0035-T02 status: done priority: high depends_on: [FLEX-WP-0035-T01] state_hub_task_id: "eb0b6a91-7750-553b-b87f-c4262eb9df07" ``` Add a distinct caller capability (readback scope) instead of widening the `flex-auth-user-engine` binding to other systems' check rights. The readback caller must not be able to obtain a decision that another system's PEP would accept, and its decisions must say so in the binding. Gate: tests prove a readback-scoped token gets 403 on `/v1/check` for a foreign system and is admitted only on the readback route. 2026-10-09 done. `flex-auth serve-readback` is a separate subcommand that mounts no `/v1/check` or `/v1/batch_check` (404 there). `--readback-caller` is bound under the non-system key `application-readback`. `CheckRequest.purpose` is hashed into `request_digest`, `submitted_request_digest` and `approval_binding_digest`, but only when set, so enforcement digests are unchanged. `/v1/check` refuses a request that carries it. Tests: `cmd/flex-auth/readback_test.go` (`TestReadbackPrincipalCannotCheckOnEnforcementPin`, `TestReadbackHTTPCallerAndRequestShape`, `TestReadbackWithholdsUncommittedAllow`) and `pkg/api/purpose_test.go`. ## Readable protected-system registry ```task id: FLEX-WP-0035-T03 status: done priority: medium depends_on: [FLEX-WP-0035-T01] state_hub_task_id: "5dbb3280-8490-58a5-a0bb-f3883e568f58" ``` Serve a read-only projection: system id, display name, key-cape client binding. Include the registry-snapshot digest so the catalogue is tied to the same snapshot that decisions record (G1, `FLEX-WP-0019`). Grants and revocations stay inside access-engine; user-domain admins get no grant surface from this work. Gate: endpoint plus schema, admitted only for the readback caller, tests. 2026-10-09 done. `GET /v1/readback/systems`; optional `client_binding` on `protected_system_manifest`. Schemas `readback_request`, `readback_response` and `readback_systems` are checked against live service output (`TestReadbackShapesMatchSchemas`). schemaguard gained `maxItems` for this. ## informed-decision declares `application.access` ```task id: FLEX-WP-0035-T05 status: wait priority: high blocked_on: message-from:informed-decision blocking_reason: "Source side done; informed-decision must agree to lifecycle v2 (its own lifecycle pin will run the same digest at the next rollout)." state_hub_task_id: "701cbd99-1a84-5290-8f66-15fa7f407d3b" ``` informed-decision is the first participant. Its `informed-decision.lifecycle` package reads `authentication_audience`, which the readback must not use (contract §3). Add an `application.access` rule over realm-level claims only, as a new package version with fixtures. Coordinate the version bump with informed-decision, because its own pin must run the same digest. Add `client_binding` to its registry entry. Gate: package valid with allow/deny fixtures, informed-decision agrees, and the rule reads no audience, authorized-party or client-scoped claim. 2026-10-09 source done. `informed-decision.lifecycle` is now v2 (`sha256:da01e045…`, `pin.json` updated). It adds `application.access`, which requires `purpose == "readback"` and the existing `human` bar: net-kingdom-admins, KeyCape AAL2 `pwd`+`otp` within 12h, platform tenant. It has no workload path and reads no audience or client-scoped claim. v1's twelve operations are unchanged. Eight Go cases cover it, plus a fixture showing that it denies without the readback purpose. The catalogue `examples/application-readback/` binds informed-decision to `https://kc.coulomb.social` / `informed-decision-approver`. `TestReadbackInformedDecisionCatalogue` proves the readback over the shipped files. The live lifecycle pin still runs v1 (image `bdac62bc`) until the T04 rollout. Waiting on informed-decision's agreement. ## Deploy the readback pin ```task id: FLEX-WP-0035-T04 status: wait needs_human: true depends_on: [FLEX-WP-0035-T05] blocking_reason: "Live pin change on railiance01 needs founder go." priority: medium state_hub_task_id: "82a8a7e6-54e9-5262-a271-24ce2f161362" ``` Add `values/flex-auth-application-readback.yaml` (or a deploy manifest) for a dedicated `flex-auth-application-readback` pin, plus a test asserting that its package paths match each participant's own pin values (digest-equality, §4). Add the user-engine ingress rule, then get the founder's go for the rollout. Promised to user-engine (reply to `d7e02758`, 2026-10-10), so the values file must match: URL `http://flex-auth-application-readback.flex-auth.svc.cluster.local.:8080` (trailing dot, in-cluster only), caller token audience `flex-auth` (`serve-readback --caller-audience` default), readback caller `system:serviceaccount:user-engine:user-engine`. The pin ships with the image that contains `serve-readback`. Hand user-engine the evidence that unblocks `USER-WP-0026-T03`.