FLEX-WP-0035: activate; draft application access readback contract and FLEX-DEC-2026-019
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 2s

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

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 609675@bnt-lap001
Assistant-Session: 4987a36e-eb38-4b10-803c-15a29cd78212
This commit is contained in:
tegwick 2026-10-09 19:11:55 +02:00
parent 229a98c527
commit 9d9ca69b4c
4 changed files with 228 additions and 2 deletions

View file

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

View file

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

View file

@ -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-<consumer>` 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.

View file

@ -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`,