From 9d9ca69b4c209bcd34cd619dd8158a6b68a477c9 Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 9 Oct 2026 19:11:55 +0200 Subject: [PATCH] FLEX-WP-0035: activate; draft application access readback contract and FLEX-DEC-2026-019 Co-Authored-By: Claude Opus 5.5 Assistant: claude-code Assistant-Model: opus Assistant-Process: 609675@bnt-lap001 Assistant-Session: 4987a36e-eb38-4b10-803c-15a29cd78212 --- WORK-RECORDS.md | 5 + decisions/decisions.md | 30 +++ docs/application-access-readback.md | 178 ++++++++++++++++++ ...-0035-per-identity-application-readback.md | 17 +- 4 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 docs/application-access-readback.md diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index cfcc89b..3c5f595 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -43,6 +43,7 @@ | workplan | FLEX-WP-0032 | finished | — | workplans/FLEX-WP-0032-informed-decision-list-action.md | | workplan | FLEX-WP-0033 | finished | — | workplans/FLEX-WP-0033-informed-decision-lifecycle.md | | workplan | FLEX-WP-0034 | finished | — | workplans/FLEX-WP-0034-informed-decision-review-discovery.md | +| workplan | FLEX-WP-0035 | proposed | — | workplans/FLEX-WP-0035-per-identity-application-readback.md | | task | FLEX-WP-ADHOC-2026-10-03-T01 | done | — | workplans/ADHOC-2026-10-03.md | | task | FLEX-WP-ADHOC-2026-10-03-T02 | wait | — | workplans/ADHOC-2026-10-03.md | | task | FLEX-WP-0001-T001 | done | — | workplans/FLEX-WP-0001-repo-intent-and-architecture-baseline.md | @@ -200,6 +201,10 @@ | task | FLEX-WP-0033-T04 | done | — | workplans/FLEX-WP-0033-informed-decision-lifecycle.md | | task | FLEX-WP-0034-T01 | done | — | workplans/FLEX-WP-0034-informed-decision-review-discovery.md | | task | FLEX-WP-0034-T02 | done | — | workplans/FLEX-WP-0034-informed-decision-review-discovery.md | +| task | FLEX-WP-0035-T01 | todo | — | workplans/FLEX-WP-0035-per-identity-application-readback.md | +| task | FLEX-WP-0035-T02 | todo | — | workplans/FLEX-WP-0035-per-identity-application-readback.md | +| task | FLEX-WP-0035-T03 | todo | — | workplans/FLEX-WP-0035-per-identity-application-readback.md | +| task | FLEX-WP-0035-T04 | wait | — | workplans/FLEX-WP-0035-per-identity-application-readback.md | | intake | FLEX-IN-0001 | closed | — | intakes/intakes.md | | intake | FLEX-IN-0002 | closed | — | intakes/intakes.md | | decision | FLEX-DEC-2026-001 | resolved | — | decisions/decisions.md | diff --git a/decisions/decisions.md b/decisions/decisions.md index 34ae42a..bea9514 100644 --- a/decisions/decisions.md +++ b/decisions/decisions.md @@ -2080,3 +2080,33 @@ endpoint. **Bound.** Unchanged from `cadence.yaml`: none of this detects a compromised flex-auth suppressing a record together with its own count. + +## FLEX-DEC-2026-019 — Application access readback is a per-package opt-in action, advisory by binding, served from a dedicated pin + +**Date:** 2026-10-09 +**Status:** proposed (pending user-engine review) +**Workplan:** `FLEX-WP-0035-T01` +**Contract:** `docs/application-access-readback.md` + +**Question.** How does access-engine tell user-engine which applications a +signed-in person may use, without widening any pin's caller binding, and +without letting registry content decide an allow? + +**Ruling (proposed).** + +1. **The action is `application.access`, declared per package.** A system + takes part by giving the action rules in its own versioned package. If it + does not, the answer is `not_applicable`. No registry-level mapping, because + registry content is outside decision provenance until G1. +2. **The answer is advisory by construction.** The binding carries + `purpose: "readback"` inside the hashed request, so its digests never match + an enforcement request. The route evaluates only `application.access`. +3. **It runs on a dedicated readback pin** that loads participating packages + at the digests their own pins run, enforced by a digest-equality test. Its + caller binding admits the readback route only. Fan-out to consumer pins is + rejected, because it widens every pin's binding and ingress. +4. **Commitments do not grow.** Each result restates the §9.7 per-input-class + deadlines from `docs/decision-input-freshness.md` and carries + `lifetime.expires_at`. Readback decisions are recorded as their own + non-load-bearing `readback` event class under `FLEX-DEC-2026-018`. +5. **Grants stay inside access-engine.** No user-domain grant surface. diff --git a/docs/application-access-readback.md b/docs/application-access-readback.md new file mode 100644 index 0000000..59c8638 --- /dev/null +++ b/docs/application-access-readback.md @@ -0,0 +1,178 @@ +# Application access readback + +Status: proposed, for review with user-engine +Date: 2026-10-09 +Workplan: `FLEX-WP-0035-T01` +Decision: `FLEX-DEC-2026-019` +Consumer: user-engine (`USER-WP-0026-T03`, `USER-WP-0028-T02`) + +user-engine wants to show a signed-in person which applications they can use. +This document defines what access-engine answers to that request, what the +answer means, and what it must never be used for. + +## 1. Constraints from the current runtime + +1. **One pin evaluates one consumer.** Each `flex-auth-` Deployment + loads one `--registry` snapshot and one `--policy` package. The + `flex-auth-user-engine` pin holds no rules for any other application. It + cannot answer for them, and widening it to do so would mix packages. +2. **Caller auth is per system.** `/v1/batch_check` authorizes the caller + token against every `resource.system` in the batch. A caller bound to + `user-engine` gets 403 for any other system. That is correct, and it stays. +3. **There is no common action.** Each package has its own action + vocabulary. "May use the application" has no meaning until a package says + what it means. +4. **Registry content is not in decision provenance** until G1 + (`FLEX-WP-0019`). Under the 2026-08-19 boundary, anything that decides an + allow belongs in a versioned policy package, not in the registry. +5. **No key-cape client binding exists** in `protected_system_manifest`. + +## 2. The readback action: `application.access`, opted into per package + +A system takes part in the readback by declaring the action +`application.access` in its own policy package and giving it rules there. + +- The rules are the system's own. They are reviewed, versioned and tested like + any other action, and the decision records `policy_package_digest`. +- A system whose package does not declare the action answers + `not_applicable`. user-engine renders that as `unavailable`. +- **No registry-level mapping.** Mapping "application access" onto an existing + action in the registry was considered and rejected. Under constraint 4 it + would let content outside provenance decide an allow. + +`application.access` means: this subject, holding these claims, may begin to +use the application. It grants nothing inside the application. Every action +the application performs is still checked by that application's PEP against +its own pin. + +## 3. The answer is advisory and cannot be transferred + +A readback decision is a real flex-auth decision. It is evaluated, recorded +and attributable, but it cannot be accepted as an enforcement decision by any +PEP: + +- The binding carries `purpose: "readback"`. The field is part of the hashed + request, so `request_digest` and `submitted_request_digest` can never equal + those of an enforcement request for the same subject and resource. Under + §6.4.2 a PEP's digest comparison therefore fails. +- The readback route evaluates only `application.access`. Any other action is + refused with 400 before evaluation. The readback surface cannot produce a + decision about a real operation. +- The decision is evaluated over the claims that user-engine presents for the + signed-in person, taken from its key-cape session. It is only as current as + those claims. The target application's PEP receives its own claims on its + own path. + +## 4. Where it runs: a dedicated readback pin + +The readback runs on a dedicated pin, `flex-auth-application-readback`. It +loads the participating systems' packages at **the same package digests** +their own pins run, plus a merged registry snapshot. + +- Drift control: each decision states the `policy_package_digest` of the + package it used. A test compares every package digest in the readback pin's + values against that consumer's own pin values, and fails on any mismatch. +- The only admitted caller is a readback binding (T02), for example + `application-readback=system:serviceaccount:user-engine:user-engine`. That + binding authorizes the readback route only. It never authorizes `/v1/check` + or `/v1/batch_check` on any pin. +- Rejected alternative: fan out from user-engine, or from a readback service, + to each consumer pin. That needs a caller binding and an ingress rule on + every consumer pin, which is exactly the widening constraint 2 forbids. It + also makes per-page latency the sum of N pins. + +Code needed: `serve` today takes one `--policy`. The readback pin needs a +multi-package mode keyed by `resource.system`, restricted to +`application.access`. + +## 5. Request and response + +```http +POST /v1/readback/application_access +``` + +```json +{ + "subject": {"issuer": "https://key-cape…/realms/…", "sub": "…", "type": "user"}, + "context": {"groups": ["…"], "acr": "…", "amr": ["pwd", "otp"], "auth_time": 0}, + "systems": ["informed-decision", "markitect"] +} +``` + +Response, one entry per requested system and in request order: + +```json +{ + "registry_snapshot_digest": "sha256:…", + "results": [ + { + "system": "informed-decision", + "effect": "allow", + "decision_id": "decision:…", + "lifetime": {"expires_at": "…"}, + "revocation_visibility": { + "claims": "immediate", + "registry": "reload_or_lifetime", + "policy_package": "reload_or_lifetime", + "directory": "next_check" + }, + "policy_package": "informed-decision.lifecycle", + "policy_package_digest": "sha256:…" + }, + {"system": "markitect", "effect": "not_applicable", "reason": "no application.access rule"} + ] +} +``` + +- `effect` is one of `allow`, `deny`, `not_applicable`. A system the pin does + not know answers `not_applicable`; it is never omitted. A whole-request + failure is a non-2xx response, and user-engine renders every row + `unavailable`. +- `revocation_visibility` restates per input class the deadlines published + in `docs/decision-input-freshness.md`. It contains nothing new. The + readback commits to no more than §9.7 already does. +- user-engine must not display an allow past `lifetime.expires_at`, and must + re-read rather than extend. + +## 6. Records + +Readback decisions are recorded like any decision. They form their own event +class, `readback`, declared in `cadence.yaml` as not load-bearing: a readback +`allow` authorizes nothing. Under `FLEX-DEC-2026-018`, a failed commit still +withholds an allow (503, rendered `unavailable`) and still releases +`deny`/`not_applicable`. + +## 7. Registry projection (T03) + +```http +GET /v1/readback/systems +``` + +The response lists every system in the merged snapshot: `id`, `name`, +`client_binding` (`{issuer, client_id}` or absent), `readback` (whether its +package declares `application.access`), and the `registry_snapshot_digest`. + +This needs one new optional field on `protected_system_manifest`: +`client_binding`. A system without one is still listed, and user-engine's rule +then excludes it from the workload catalogue. The projection authorizes +nothing. It describes what the readback pin knows. + +## 8. Grants + +Grants and revocations stay inside access-engine, in packages and registry +content under their normal review. This work gives user-domain admins no grant +or revoke surface. + +## 9. Open questions for user-engine review + +1. **Claim set.** Is forwarding the session's verified claims (groups, `acr`, + `amr`, `auth_time`) acceptable to you? Or do you want the readback to + resolve groups itself? Resolving them itself would add a directory + dependency. +2. **First participants.** The proposal is informed-decision first: a + human-facing web app with KeyCape AAL2 rules already in its packages. Which + applications do you need in your first release? +3. **Batch size.** The ceiling is 64 systems per request. Is that enough? +4. **Record volume.** Each page view produces N decision records. If that is + too much, the alternative is a shorter TTL plus client-side reuse up to + `expires_at`, not unrecorded decisions. diff --git a/workplans/FLEX-WP-0035-per-identity-application-readback.md b/workplans/FLEX-WP-0035-per-identity-application-readback.md index 07eee1d..d8f341b 100644 --- a/workplans/FLEX-WP-0035-per-identity-application-readback.md +++ b/workplans/FLEX-WP-0035-per-identity-application-readback.md @@ -4,12 +4,13 @@ type: workplan title: "Per-identity application access readback and readable protected-system registry for user-engine" domain: infotech repo: access-engine -status: proposed +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`) @@ -38,8 +39,9 @@ record. ```task id: FLEX-WP-0035-T01 -status: todo +status: progress priority: high +state_hub_task_id: "0585a684-d462-5caa-937f-b1730efddbd8" ``` Decide the action that a readback evaluates per system: a declared @@ -54,6 +56,14 @@ 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: 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 @@ -61,6 +71,7 @@ id: FLEX-WP-0035-T02 status: todo 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 @@ -78,6 +89,7 @@ id: FLEX-WP-0035-T03 status: todo 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. @@ -96,6 +108,7 @@ needs_human: true depends_on: [FLEX-WP-0035-T02, FLEX-WP-0035-T03] blocking_reason: "Live pin change on railiance01 needs founder go." priority: medium +state_hub_task_id: "82a8a7e6-54e9-5262-a271-24ce2f161362" ``` Roll the readback route and its caller binding onto `flex-auth-user-engine`,