Admit list for the informed-decision overview as compact-sitting v3 (FLEX-DEC-2026-017).
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 2s
Build and Publish Container Image / build-and-push (push) Successful in 1m14s

list is a separate rule with exact-record scope over the union of the eight
sitting records and the three T03 records, each pinned by approval id, digest and
its own memo version, with a 12-hour MFA window. read and the five acts keep the
v2 rule unchanged — same eight records, same 900-second window — so a list allow
satisfies nothing else.

The operator chose exact-record scope over the consumer's preferred type-wide
scope: the PDP checks no recipient, so type-wide scope with a relaxed window would
have left the consumer's structural match — which the consumer itself says is
not an entitlement — as the only scope. The 12-hour bound replaces the requested
"no bound" so the PDP still states one. The KeyCape stale-timestamp defect is not
worked around; read stays strict.

417 evaluator checks: 168 v2 unchanged, 231 list, 18 proving no act widens to the
T03 records. Also fixes tools/exercise_t03_review_policy.py, which had been
failing since f85479c moved the T03 records to memo version 2 and it still sent
version 1. Test-only.

Not deployed: the pin serving the live review surface changes only with operator
confirmation.

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

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 28468@bnt-lap001
Assistant-Session: c76569b2-6056-4dad-aea4-49cd7a018f5d
This commit is contained in:
tegwick 2026-09-21 23:06:40 +02:00
parent 1a3f2f7d78
commit ad7b7f536a
7 changed files with 1928 additions and 8 deletions

View file

@ -1953,3 +1953,60 @@ denies (mismatch, absent) were added. 42 fixtures, 33 embedded tests, all pass.
of behaviour for any caller that is not tenant-engine; per of behaviour for any caller that is not tenant-engine; per
`FLEX-DEC-2026-008` it is a version change, not a silent edit. tenant-engine `FLEX-DEC-2026-008` it is a version change, not a silent edit. tenant-engine
asked for no notice period. asked for no notice period.
## FLEX-DEC-2026-017 — `informed-decision.compact-sitting` v3 admits `list`: exact-record scope, 12-hour MFA window, acts unchanged
**Status:** accepted — operator decision, 2026-09-21
**Requested by:** `informed-decision` (`INFD-IN-0008`, `INFD-WP-0004-T02`),
hub message `a7b9ef3b-f95c-4c50-bfc5-7628d16881f6`
**Workplan:** `FLEX-WP-0032`
**Package:** `informed-decision.compact-sitting` v3,
`sha256:dd40c80c5719851737178097cdee65afac108b54e19c07370e549b860f1a9655`
**Decision.** v3 adds one action, `list`, as a rule of its own. It is not a
seventh member of the act set, and it changes nothing that v2 decided.
1. **Acts unchanged.** `read`, `acknowledge`, `accept`, `return`, `discuss` and
`decline` keep the v2 rule unchanged: the same eight records, the same
900-second MFA window. A `list` allow satisfies no other action.
2. **Exact-record scope.** `list` ranges over the union of this package's eight
records and the three `SECRETS-WP-0010-T03-*` records from
`informed-decision.t03-review`, each pinned by approval id, binding digest
and its own memo version (T03 at 2, sitting at 1).
3. **A lighter bar, not an absent one.** `list` requires the same identity as
the acts with a **12-hour** MFA window instead of 900 seconds.
**Why exact-record over resource-type-wide.** The consumer preferred
resource-type-wide scope (option A). The v2 rule scopes a decision three ways —
exact-record pinning, group, fresh MFA — and checks no recipient. Option A,
together with a relaxed window, would have removed both PDP-side scopes for
`list` at once, leaving the consumer's structural recipient match as the only
thing between a group member and every memo's question and status. The
consumer's own request says that match "is not an entitlement." A PDP cannot
rest an allow on a check its consumer disclaims. Under exact-record scope a memo
becomes listable when someone admits it, and a package version per sitting is
the cost of that property.
**Why a bound at all.** The request asked for no freshness bound. Unbounded
would have let an MFA of any age satisfy `list` for as long as the consumer
presented a key-cape session, and the PDP would have stated nothing. Twelve
hours covers the observed production refusals (1744 s) with ample margin and
keeps the bound in the policy, where a reviewer can read it.
**What this does not do.** It does not work around the KeyCape stale-timestamp
defect (`INFD-IN-0005`), which made the overview fail in the first place. The
case for `list` stands on its own — looking back at your own outcomes is not
binding a new one — and `read` stays strict. Loosening a binding bar to absorb
an identity-layer fault would move key-cape's defect into flex-auth's policy.
**Evidence.** 417 evaluator checks
(`docs/evidence/2026-09-21-sitting-review-policy-v3.json`): the 168 v2 checks
unchanged; 231 `list` checks over the 11-record union, including the observed
1744 s case allowed, the 12-hour bound refused past it, and stale MFA still
refused for `read`; and 18 proving no act widens to the T03 records even with
fresh MFA. Rego tests assert the union is exact and that T03 records are outside
the act scope. T03's own 57 checks pass.
**Rollout.** Deploying v3 changes the pin serving a live human-review surface,
and is confirmed with the operator before it runs. informed-decision updates its
admission input to the version and digest above once the pin serves v3.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,20 @@
{
"memo:SECRETS-WP-0010-T03-apply": {
"approval_id": "9935335c-8e9a-566e-a48e-6a5b5f4882eb",
"binding_digest": "sha256:03cc5b37437f14e86b686ce4054f976556334eff3fa260b03e8014ed3d9217e5",
"memo_version": 2,
"label": "t03-apply"
},
"memo:SECRETS-WP-0010-T03-verify": {
"approval_id": "273d6882-6253-5dc9-ac54-544f92ef5e56",
"binding_digest": "sha256:72d9267d038c17107c880ffc9009793ce9c70defbddfe2219628854b811a04b2",
"memo_version": 2,
"label": "t03-verify"
},
"memo:SECRETS-WP-0010-T03-exec": {
"approval_id": "7ba0c13b-68cd-5b3e-9481-42ba9e385e68",
"binding_digest": "sha256:f2cf0fb53b740900756ccde5587c3578fcff44beef2f1edab17b9a198a4033d8",
"memo_version": 2,
"label": "t03-exec"
}
}

View file

@ -2,11 +2,11 @@
id: informed-decision.compact-sitting id: informed-decision.compact-sitting
name: Compact sitting exact-record human review name: Compact sitting exact-record human review
namespace: informed-decision:decision-memo namespace: informed-decision:decision-memo
version: v2 version: v3
status: ready status: ready
package: flexauth.informed_decision.compact_sitting package: flexauth.informed_decision.compact_sitting
allow_ttl: 30s allow_ttl: 30s
actions: [read, acknowledge, accept, return, discuss, decline] actions: [read, acknowledge, accept, return, discuss, decline, list]
owner: flex-auth owner: flex-auth
fixtures: [fixtures.json] fixtures: [fixtures.json]
caring: caring:
@ -26,6 +26,25 @@ and the identity bar are unchanged.
It is not an expansion of `examples/informed-decision-t03` / FLEX-WP-0027. It is not an expansion of `examples/informed-decision-t03` / FLEX-WP-0027.
`memo:infd-20260914-c01` is omitted (create-client still undecided). `memo:infd-20260914-c01` is omitted (create-client still undecided).
v3 (2026-09-21, `FLEX-WP-0032`, `FLEX-DEC-2026-017`) adds one action, `list`,
for informed-decision's decision overview (`INFD-IN-0008`). It is a separate
rule, not an extra member of the act set, and it changes nothing else:
- `read` and the five acts keep the v2 rule unchanged — the same eight records,
the same 900-second MFA window. A `list` allow satisfies no other action.
- `list` covers the **union** of this package's eight records and the three
`SECRETS-WP-0010-T03-*` records from `informed-decision.t03-review`, each
pinned by approval id, binding digest and its own memo version (T03 at 2).
Scope stays exact-record: a memo is listable only once someone admitted it.
The operator declined resource-type-wide scope because the PDP checks no
recipient, and a type-wide rule would have left consumer code — which the
consumer itself says is not an entitlement — as the only scope.
- `list` carries the same identity bar with a **12-hour** MFA window instead of
900 seconds. Looking back at your own outcomes is not binding a new one. The
bar is lighter, not absent: the PDP still states a bound.
- The KeyCape stale-timestamp defect (`INFD-IN-0005`) is **not** worked around
here. `read` stays strict; that defect is key-cape's to fix.
The identity bar matches T03: authenticated informed-decision caller, The identity bar matches T03: authenticated informed-decision caller,
`net-kingdom-admins`, fresh KeyCape AAL2 MFA. No permission follows from `net-kingdom-admins`, fresh KeyCape AAL2 MFA. No permission follows from
memo content or presentation state. TokenReview must admit memo content or presentation state. TokenReview must admit
@ -73,6 +92,31 @@ records := {
} }
} }
list_only_records := {
"memo:SECRETS-WP-0010-T03-apply": {
"approval_id": "9935335c-8e9a-566e-a48e-6a5b5f4882eb",
"binding_digest": "sha256:03cc5b37437f14e86b686ce4054f976556334eff3fa260b03e8014ed3d9217e5",
"memo_version": 2
},
"memo:SECRETS-WP-0010-T03-verify": {
"approval_id": "273d6882-6253-5dc9-ac54-544f92ef5e56",
"binding_digest": "sha256:72d9267d038c17107c880ffc9009793ce9c70defbddfe2219628854b811a04b2",
"memo_version": 2
},
"memo:SECRETS-WP-0010-T03-exec": {
"approval_id": "7ba0c13b-68cd-5b3e-9481-42ba9e385e68",
"binding_digest": "sha256:f2cf0fb53b740900756ccde5587c3578fcff44beef2f1edab17b9a198a4033d8",
"memo_version": 2
}
}
# listable is the union list may range over. Sitting records are memo version 1,
# as the act rule pins; T03 records carry their own version (2).
listable := object.union(
{id: object.union(r, {"memo_version": 1}) | some id, r in records},
list_only_records,
)
decision := {"effect": "allow", "reason": "operator_admitted_compact_sitting"} if { decision := {"effect": "allow", "reason": "operator_admitted_compact_sitting"} if {
input.tenant == "tenant:platform" input.tenant == "tenant:platform"
input.subject.tenant == "tenant:platform" input.subject.tenant == "tenant:platform"
@ -100,6 +144,33 @@ decision := {"effect": "allow", "reason": "operator_admitted_compact_sitting"} i
input.context.approval_id == record.approval_id input.context.approval_id == record.approval_id
input.context.approval_binding_digest == record.binding_digest input.context.approval_binding_digest == record.binding_digest
input.action in {"read", "acknowledge", "accept", "return", "discuss", "decline"} input.action in {"read", "acknowledge", "accept", "return", "discuss", "decline"}
} else := {"effect": "allow", "reason": "operator_admitted_overview_list"} if {
input.action == "list"
input.tenant == "tenant:platform"
input.subject.tenant == "tenant:platform"
input.subject.type == "human"
is_string(input.subject.id)
input.subject.id != ""
input.subject.attributes.principal_type_source == "authentication-derived"
input.subject.attributes.tenant_source in {"registration-supplied", "directory-asserted"}
"net-kingdom-admins" in input.subject.attributes.groups
assurance := input.subject.attributes.assurance
assurance.level == "aal2"
assurance.mfa == true
assurance.source == "key-cape"
assurance.methods == ["pwd", "otp"]
is_number(assurance.at)
assurance.at > 0
age := time.now_ns() / 1000000000 - assurance.at
age >= -30
age <= 43200
input.resource.tenant == "tenant:platform"
input.resource.system == "informed-decision"
input.resource.type == "decision-memo"
record := listable[input.resource.id]
input.context.memo_version == record.memo_version
input.context.approval_id == record.approval_id
input.context.approval_binding_digest == record.binding_digest
} else := {"effect": "deny", "reason": "compact_sitting_scope_or_identity_refused"} if { } else := {"effect": "deny", "reason": "compact_sitting_scope_or_identity_refused"} if {
true true
} }
@ -113,4 +184,14 @@ import data.flexauth.informed_decision.compact_sitting
test_unknown_request_denied if { test_unknown_request_denied if {
compact_sitting.decision.effect == "deny" with input as {} compact_sitting.decision.effect == "deny" with input as {}
} }
test_listable_is_exact_union if {
count(compact_sitting.listable) == count(compact_sitting.records) + count(compact_sitting.list_only_records)
}
test_t03_records_not_in_act_scope if {
every id, _ in compact_sitting.list_only_records {
not compact_sitting.records[id]
}
}
``` ```

View file

@ -87,6 +87,77 @@ with tempfile.TemporaryDirectory() as temp:
target[path[-1]] = value target[path[-1]] = value
check(label + ':' + name, candidate, 'deny') check(label + ':' + name, candidate, 'deny')
# --- v3 list (FLEX-WP-0032, FLEX-DEC-2026-017) ---------------------------
# list ranges over the union of the act records and the list-only records;
# every act stays scoped to the act records, with the 900 s window.
list_only = json.loads((r / 'list_only_records.json').read_text())
listable = {k: dict(v, memo_version=1) for k, v in records.items()} | list_only
now = int(time.time())
for memo, record in listable.items():
request = {
'id': 'local-regression',
'tenant': 'tenant:platform',
'subject': {
'id': 'synthetic-reviewer', 'type': 'human', 'tenant': 'tenant:platform',
'attributes': {
'groups': ['net-kingdom-admins'], 'roles': [],
'tenant_source': 'registration-supplied',
'principal_type_source': 'authentication-derived',
'assurance': {'level': 'aal2', 'mfa': True, 'methods': ['pwd', 'otp'],
'source': 'key-cape', 'at': now},
},
},
'resource': {'id': memo, 'type': 'decision-memo',
'system': 'informed-decision', 'tenant': 'tenant:platform'},
'action': 'list',
'context': {'memo_version': record['memo_version'],
'approval_id': record['approval_id'],
'approval_binding_digest': record['binding_digest']},
}
label = 'list:' + record['label']
def variant(path, value, req=request):
c = copy.deepcopy(req)
t = c
for key in path[:-1]:
t = t[key]
t[path[-1]] = value
return c
at = ['subject', 'attributes', 'assurance', 'at']
check(label + ':fresh', request, 'allow')
# The observed production refusal: 1744 s, allowed for list...
check(label + ':observed-1744s', variant(at, now - 1744), 'allow')
check(label + ':inside-12h', variant(at, now - 43200 + 60), 'allow')
# ...but the 12 h bound is real, and read does not follow list.
check(label + ':past-12h', variant(at, now - 43200 - 60), 'deny')
is_act_record = memo in records
read_stale = variant(at, now - 1744) | {'action': 'read'}
check(label + ':read-stale-refused', read_stale, 'deny')
if not is_act_record:
# T03 records are listable only; no act widens to them, even fresh.
for action in ['read', 'acknowledge', 'accept', 'return', 'discuss', 'decline']:
check(label + ':act-' + action + '-not-widened', request | {'action': action}, 'deny')
for name, path, value in [
('wrong-group', ['subject', 'attributes', 'groups'], ['net-kingdom-users']),
('no-group', ['subject', 'attributes', 'groups'], []),
('service', ['subject', 'type'], 'service'),
('agent', ['subject', 'type'], 'agent'),
('future-mfa', at, now + 300),
('no-mfa', ['subject', 'attributes', 'assurance', 'mfa'], False),
('aal1', ['subject', 'attributes', 'assurance', 'level'], 'aal1'),
('forged-human-route', ['subject', 'attributes', 'principal_type_source'], 'registration-supplied'),
('indeterminate-route', ['subject', 'attributes', 'principal_type_source'], 'indeterminate'),
('wrong-tenant', ['subject', 'tenant'], 'tenant:other'),
('other-memo', ['resource', 'id'], 'memo:other'),
('omitted-c01', ['resource', 'id'], 'memo:infd-20260914-c01'),
('other-system', ['resource', 'system'], 'secrets-engine'),
('changed-version', ['context', 'memo_version'], record['memo_version'] + 1),
('changed-approval', ['context', 'approval_id'], 'other'),
('changed-digest', ['context', 'approval_binding_digest'], 'sha256:' + '0' * 64),
]:
check(label + ':' + name, variant(path, value), 'deny')
args.receipt.write_text(json.dumps({ args.receipt.write_text(json.dumps({
'scope': 'local actual evaluator with synthetic identity; no live human approvals; T03 package untouched', 'scope': 'local actual evaluator with synthetic identity; no live human approvals; T03 package untouched',
'checks': results, 'checks': results,

View file

@ -15,10 +15,10 @@ with tempfile.TemporaryDirectory() as temp:
results.append({'check':name,'effect':d['effect']}) results.append({'check':name,'effect':d['effect']})
return d return d
for memo,record in records.items(): for memo,record in records.items():
request={'id':'local-regression','tenant':'tenant:platform','subject':{'id':'synthetic-reviewer','type':'human','tenant':'tenant:platform','attributes':{'groups':['net-kingdom-admins'],'roles':[],'tenant_source':'registration-supplied','principal_type_source':'authentication-derived','assurance':{'level':'aal2','mfa':True,'methods':['pwd','otp'],'source':'key-cape','at':int(time.time())}}},'resource':{'id':memo,'type':'decision-memo','system':'informed-decision','tenant':'tenant:platform'},'action':'accept','context':{'memo_version':1,'approval_id':record['approval_id'],'approval_binding_digest':record['binding_digest']},'policy_version':'v1'} request={'id':'local-regression','tenant':'tenant:platform','subject':{'id':'synthetic-reviewer','type':'human','tenant':'tenant:platform','attributes':{'groups':['net-kingdom-admins'],'roles':[],'tenant_source':'registration-supplied','principal_type_source':'authentication-derived','assurance':{'level':'aal2','mfa':True,'methods':['pwd','otp'],'source':'key-cape','at':int(time.time())}}},'resource':{'id':memo,'type':'decision-memo','system':'informed-decision','tenant':'tenant:platform'},'action':'accept','context':{'memo_version':record['memo_version'],'approval_id':record['approval_id'],'approval_binding_digest':record['binding_digest']},'policy_version':'v1'}
for action in ['read','acknowledge','accept','return','discuss','decline']: for action in ['read','acknowledge','accept','return','discuss','decline']:
d=check(record['action']+':'+action,request|{'action':action},'allow') d=check(record['action']+':'+action,request|{'action':action},'allow')
for name,path,value in [('wrong-group',['subject','attributes','groups'],['net-kingdom-users']),('no-group',['subject','attributes','groups'],[]),('service',['subject','type'],'service'),('stale-mfa',['subject','attributes','assurance','at'],int(time.time())-901),('future-mfa',['subject','attributes','assurance','at'],int(time.time())+300),('no-mfa',['subject','attributes','assurance','mfa'],False),('forged-human-route',['subject','attributes','principal_type_source'],'registration-supplied'),('wrong-tenant',['subject','tenant'],'tenant:other'),('other-memo',['resource','id'],'memo:other'),('changed-version',['context','memo_version'],2),('changed-approval',['context','approval_id'],'other'),('changed-digest',['context','approval_binding_digest'],'sha256:'+'0'*64),('consume',['action'],'consume')]: for name,path,value in [('wrong-group',['subject','attributes','groups'],['net-kingdom-users']),('no-group',['subject','attributes','groups'],[]),('service',['subject','type'],'service'),('stale-mfa',['subject','attributes','assurance','at'],int(time.time())-901),('future-mfa',['subject','attributes','assurance','at'],int(time.time())+300),('no-mfa',['subject','attributes','assurance','mfa'],False),('forged-human-route',['subject','attributes','principal_type_source'],'registration-supplied'),('wrong-tenant',['subject','tenant'],'tenant:other'),('other-memo',['resource','id'],'memo:other'),('changed-version',['context','memo_version'],record['memo_version']+1),('changed-approval',['context','approval_id'],'other'),('changed-digest',['context','approval_binding_digest'],'sha256:'+'0'*64),('consume',['action'],'consume')]:
candidate=copy.deepcopy(request); target=candidate candidate=copy.deepcopy(request); target=candidate
for key in path[:-1]:target=target[key] for key in path[:-1]:target=target[key]
target[path[-1]]=value target[path[-1]]=value

View file

@ -4,7 +4,7 @@ type: workplan
title: "Admit a list action for the informed-decision overview without handing the scope to the consumer" title: "Admit a list action for the informed-decision overview without handing the scope to the consumer"
domain: infotech domain: infotech
repo: flex-auth repo: flex-auth
status: proposed status: active
flavor: implementation flavor: implementation
owner: claude owner: claude
topic_slug: netkingdom topic_slug: netkingdom
@ -83,18 +83,21 @@ surface was admitted by the operator (`FLEX-WP-0027-T01`, `FLEX-WP-0028`).
```task ```task
id: FLEX-WP-0032-T01 id: FLEX-WP-0032-T01
status: wait status: done
priority: high priority: high
state_hub_task_id: "f741d47e-50b9-5eb1-81d9-ffec847be77a" state_hub_task_id: "f741d47e-50b9-5eb1-81d9-ffec847be77a"
``` ```
Owner: operator decides; `flex-auth` records as a `FLEX-DEC`. Owner: operator decides; `flex-auth` records as a `FLEX-DEC`.
2026-09-21: done. Operator chose **(B) exact-record** and a **12-hour** MFA
window for `list`. Recorded as `FLEX-DEC-2026-017`.
## 2. Compile compact-sitting v3 ## 2. Compile compact-sitting v3
```task ```task
id: FLEX-WP-0032-T02 id: FLEX-WP-0032-T02
status: todo status: done
priority: high priority: high
state_hub_task_id: "a24fc5cf-f663-5338-97da-169af7fb47fb" state_hub_task_id: "a24fc5cf-f663-5338-97da-169af7fb47fb"
``` ```
@ -110,11 +113,17 @@ Owner: `flex-auth`.
- A version bump, per `FLEX-DEC-2026-008`: a widened bar must be visible to the - A version bump, per `FLEX-DEC-2026-008`: a widened bar must be visible to the
consumer as a version change. consumer as a version change.
2026-09-21: done. v3, digest
`sha256:dd40c80c5719851737178097cdee65afac108b54e19c07370e549b860f1a9655`.
The v2 act rule is kept unchanged so its effect cannot drift; `list` is a
separate `else` branch. T03 pins in `examples/informed-decision-sitting/list_only_records.json`,
copied from the T03 package's records.
## 3. Fixtures and the exercise receipt ## 3. Fixtures and the exercise receipt
```task ```task
id: FLEX-WP-0032-T03 id: FLEX-WP-0032-T03
status: todo status: done
priority: high priority: high
state_hub_task_id: "984f11f4-6ec9-5f7d-840b-4dee2e121212" state_hub_task_id: "984f11f4-6ec9-5f7d-840b-4dee2e121212"
``` ```
@ -130,6 +139,15 @@ sitting checks and T03's 57.
Gate: `flex-auth validate`, `test-policy`, and the exercise scripts green; a Gate: `flex-auth validate`, `test-policy`, and the exercise scripts green; a
receipt in `docs/evidence/`. receipt in `docs/evidence/`.
2026-09-21: done. 417 checks (168 v2 unchanged, 231 `list`, 18 no-act-widening),
receipt `docs/evidence/2026-09-21-sitting-review-policy-v3.json`; validate and
three Rego tests green; T03's 57 pass.
Found on the way: `tools/exercise_t03_review_policy.py` had been failing since
`f85479c` moved the T03 records to memo version 2 — it still sent version 1, so
the reproduce command `FLEX-WP-0027-T01` cites was broken. Fixed to read each
record's own version. Test-only; no policy change.
## 4. Publish and hand back ## 4. Publish and hand back
```task ```task