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

@ -87,6 +87,77 @@ with tempfile.TemporaryDirectory() as temp:
target[path[-1]] = value
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({
'scope': 'local actual evaluator with synthetic identity; no live human approvals; T03 package untouched',
'checks': results,

View file

@ -15,10 +15,10 @@ with tempfile.TemporaryDirectory() as temp:
results.append({'check':name,'effect':d['effect']})
return d
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']:
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
for key in path[:-1]:target=target[key]
target[path[-1]]=value