Add recipient-scoped review discovery and settle initial lifecycle contracts

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a10780-6d41-7952-9718-dfbc1f41236f
This commit is contained in:
tegwick 2026-10-05 01:03:05 +02:00
parent dfcef879d0
commit da2c5a720f
9 changed files with 224 additions and 10 deletions

View file

@ -163,8 +163,9 @@ document supersedes their exclusion of agent collaboration and human authoring
for the new increment, while preserving human-only dispositions and the evidence
and engine boundaries. L0 and L2 compatibility remain required.
Open implementation contracts: workload identity beyond cluster service accounts;
publication audience policy and any later anonymous publication; source connector
set and retention; non-approval L2 binding integration. T02 must make these
explicit before implementation depends on them. None authorizes a production
rollout or changes external owners' contracts by declaration.
Initial implementation choices are explicit in `LifecycleAdmissionBoundary.md`:
only the admitted cluster workload, authenticated publication, caller-supplied
research provenance, append-only retention, organizational human approval, and
no optional Hub outcome write-back. External identities, anonymous publication,
source connectors/retention controls and non-approval binding are expansion work
under INFD-WP-0008, not hidden prerequisites or enabled production capabilities.

View file

@ -0,0 +1,33 @@
# Lifecycle admission and evidence boundary
2026-10-05; INFD-WP-0007-T02. This closes the implementation choices for the
initial production audience. Expansion is tracked separately; these limits are
observable behavior, not an implied grant to extend another owner's interface.
| Concern | Implemented contract |
| --- | --- |
| Human identity | KeyCape-authenticated net-kingdom-admins, tenant:platform; twelve-hour MFA authoring/list window, fifteen-minute review/read/act window |
| Agent identity | Kubernetes TokenReview for the registered State Hub service account with a separate lifecycle audience; unknown and external bearer identities refused |
| Publication | Explicit subject/group audience on an exact revision, authorized by Access Engine; not anonymous or indexed publicly; successor revisions require republication |
| Research | Caller retrieves evidence externally and submits reference, retrieval time, digest and summary; the service does not fetch URLs or attest their truth |
| Evaluation | Attributed assessment, uncertainty and source-event references constrained to the same record/revision/tenant |
| Retention | Append-only revisions, sources, comments, evaluations and binding evidence in the durable store; no automatic purge, mutable source replacement or source connector credentials |
| Documents | Digest-verified bytes supplied through the bounded API; the browser adds short text documents with optional required acknowledgments as new revisions |
| Review | Organizational English approval profile using the existing shared schema and native Approval Engine; recipient from the configured review group, initially platform-root |
| Discovery | Lifecycle workspace uses per-record lifecycle policy. Review overview uses its own list decision; attribute discovery requires the immutable named recipient plus verified native requester, human-control flag and digest |
| Human acceptance | Existing protected review surface, fresh policy/read/act decisions, exact revision/presentation and required acknowledgments; no agent binding route |
| Non-approval profiles | Discussion drafts may be authored and refined, but cannot create an approval review. L0/L2 core schema compatibility does not enable new production disposition profiles |
| Hub write-back | Notification/dispatcher delivery is admitted; optional automatic resolution write-back remains disabled and cannot imply permission to execute |
Identity verification stays in KeyCape/TokenReview; authorization stays in Access
Engine. The headless application service is shared by browser and private API.
Client-supplied research descriptions and evaluation conclusions remain claims
with attribution, not machine-verified facts. Packet bytes are retained so an
external URL changing cannot alter a past presentation.
No finite retention duration, deletion mechanism, new connector, external agent
identity or non-approval signing semantics is invented here. Those product
extensions require their own owner contracts and delivery evidence under
INFD-WP-0008. The existing source/evaluation and organizational approval workflows
provide all nine initial lifecycle operations without per-decision infrastructure
changes.

View file

@ -2,7 +2,8 @@
2026-10-04; `INFD-WP-0007-T02/T03`. This is the internal persistence slice of
`DecisionLifecycleService.md`. The protected application interface built on this
slice is documented in `LifecycleServiceContract.md`; HTTP exposure is pending.
slice is documented in `LifecycleServiceContract.md`; the deployed private HTTP
adapter is specified in `LifecycleHTTPContract.md`.
## Implemented boundary
@ -16,8 +17,8 @@ Each call carries `Attribution(tenant, Actor(subject, kind))` from its trusted
caller. This is audit attribution, not an authentication assertion. Like `Store`,
the repository is an internal persistence interface: the application/API
layer must authenticate callers and enforce current access-engine authorization
before invoking it. Supplying a tenant string is not an access grant. No route
or runtime configuration exposes these operations in this increment.
before invoking it. Supplying a tenant string is not an access grant. The deployed application adapters authenticate and authorize these operations;
this internal persistence interface is never exposed directly.
## Commands and reads

View file

@ -78,3 +78,12 @@ The lifecycle workspace discovers authorized records and exposes their outcomes.
A later change to legacy overview discovery needs an Access Engine contract,
not a consumer-side policy bypass. Broader binding profiles, external identities,
source connectors and optional Hub write-back remain T02/T05 work.
## Named-recipient review discovery — source continuation
The adapter now carries retained intake facts and the immutable memo recipient
for list decisions. It verifies native approval requester, human control and
binding digest before the PDP call. Access Engine compact-sitting v13 checks the
recipient and existing class/submitter/group admission with the twelve-hour list
window; read and act remain separate fifteen-minute decisions. Missing evidence
hides that row. This continuation requires the matching owner pin at release.

View file

@ -63,6 +63,8 @@ def build_request(session: HumanSession, memo, action, policy_version, *, intake
extra = {} if intake_context is None else dict(intake_context)
if extra and set(extra) != {"review_group", "memo_class", "intake_submitter", "approval_requester", "approval_human_control"}:
raise ValueError("exact intake policy context required")
if extra and action == "list":
extra["review_recipient"] = memo.binding.principal.id
return {"id": str(uuid.uuid4()), "tenant": "tenant:platform",
"subject": {"id": session.subject, "type": "human", "tenant": session.tenant.value,
"attributes": {"tenant_source": session.tenant.route.value,

View file

@ -81,7 +81,7 @@ class ReviewController:
def _authorize(self, session, memo, action):
self._memo(session, memo)
context = self.store.intake_context(memo) if self.intake_attributes and action != 'list' else None
context = self.store.intake_context(memo) if self.intake_attributes else None
if context is not None:
current = self._approval(session, memo)
if (current['binding'].get('actor') != context['approval_requester']
@ -167,6 +167,8 @@ class ReviewController:
def _overview_row(self, session, memo):
try:
_, observation = self._authorize(session, memo, "list")
except (ApprovalEngineError, ValueError, Conflict):
return OverviewRow(memo.id, "unavailable", reason="review_evidence_unavailable")
except ReviewError as exc:
if exc.code == "session_expired":
raise

View file

@ -8,12 +8,14 @@ import pytest
from informed_decision.disposition import Actor, ActorKind, Verb, DispositionRefused
from informed_decision.lifecycle import Attribution, LifecycleRepository
from informed_decision.lifecycle_service import Caller, LifecycleService, LifecycleError
from informed_decision.policy import build_request
from informed_decision.provenance import Claim, Route
from informed_decision.records import memo_from
from informed_decision.store import Conflict, Store
from test_intake import envelope
from test_intake_service import setup, request
from test_policy_client import policy
from test_browser_auth import signing_key
@pytest.fixture
@ -376,3 +378,79 @@ def test_retained_handoff_api_is_revision_scoped_and_reauthorizes(handoff):
assert request(intake, receipt['envelope'])[0] == 409
app.policy.transport.change = lambda data: data.update(effect='deny')
assert call(api, path, query='version=1')[:2] == (403, {'error': 'policy_denied'})
def test_overview_uses_retained_intake_context_and_fails_closed_per_row(handoff):
from informed_decision.review import ReviewController
from informed_decision.approval_client import ApprovalEngineError
from test_policy_client import human
app, actor, memo, options, intake, calls = handoff
receipt = prepare(handoff)
assert request(intake, receipt['envelope'])[0] == 201
store = app.repository.store
stored = store.memo(receipt['review_memo_id'])
session = replace(human(), subject=stored.binding.principal.id)
class Engine:
failure = False
def get_approval(self, approval_id):
if self.failure:
raise ApprovalEngineError(503, 'unavailable')
return {'status': 'requested', 'binding': {'digest': stored.approval_binding_digest,
'actor': 'informed-decision', 'human_control': True}}
engine = Engine()
controller = ReviewController(store, policy(), lambda session: engine, intake_attributes=True)
[row] = controller.overview(session)
assert row.question == memo.question
sent = controller.policy.transport.calls[-1][3]
assert sent['action'] == 'list'
assert sent['context']['review_recipient'] == session.subject
assert sent['context']['intake_submitter'] == 'state-hub'
assert 'review_recipient' not in build_request(
session, stored, 'read', 'v1', intake_context=store.intake_context(stored))['context']
assert store.evidence() == []
engine.failure = True
[row] = controller.overview(session)
assert row.group == 'unavailable' and row.question is None
assert len(controller.policy.transport.calls) == 1
def test_real_discovery_policy_lists_only_recipient_and_keeps_fresh_read(handoff, tmp_path, signing_key):
import os
from pathlib import Path
from informed_decision.policy import build_request
from informed_decision.review import ReviewController, ReviewError
from review_components import flex_component
from test_policy_client import human
binary, source = os.environ.get('INFD_FLEX_AUTH_BINARY'), os.environ.get('INFD_FLEX_AUTH_SOURCE')
if not binary or not source:
pytest.skip('actual owner discovery policy required')
fixture = Path(source) / 'examples/informed-decision-lifecycle-review'
if json.loads((fixture / 'pin.json').read_text())['version'] != 'v13':
pytest.skip('owner discovery v13 required')
app, actor, memo, options, intake, calls = handoff
options['memo_class'] = 'decision-record'
receipt = prepare(handoff)
# Admit this class in the synthetic intake registry used by the fixture.
registry = json.loads(intake.submitters_path.read_text())
for value in registry.values():
value['memo_classes'].append('decision-record')
intake.submitters_path.write_text(json.dumps(registry))
assert request(intake, receipt['envelope'])[0] == 201
store = app.repository.store
stored = store.memo(receipt['review_memo_id'])
session = replace(human(), subject=stored.binding.principal.id,
assurance={**human().assurance, 'at': int(time.time()) - 3600})
class Engine:
def get_approval(self, approval_id):
return {'status': 'requested', 'binding': {'digest': stored.approval_binding_digest,
'actor': 'informed-decision', 'human_control': True}}
with flex_component(binary, tmp_path, signing_key, fixture=fixture) as (pdp, calls):
controller = ReviewController(store, pdp, lambda session: Engine(), intake_attributes=True)
[row] = controller.overview(session)
assert row.question == memo.question and row.group == 'open'
with pytest.raises(ReviewError, match='policy_denied'):
controller.open(session, stored.id)
req = build_request(session, stored, 'list', pdp.version, intake_context=store.intake_context(stored))
req['context']['review_recipient'] = 'another-human'
assert pdp.check(req).outcome == 'policy_denied'
assert store.evidence() == []

View file

@ -42,7 +42,7 @@ boundary, revision/evidence semantics and acceptance without per-decision deploy
```task
id: INFD-WP-0007-T02
status: progress
status: done
priority: high
state_hub_task_id: "cde0c932-afce-5bec-8cee-a2f377955b9f"
```
@ -389,3 +389,21 @@ synthetic identity, including mobile and desktop inspection. Evidence is retaine
in docs/evidence/2026-10-05-lifecycle-authoring/. This source increment is not yet
released; production remains on the accepted initial cutover image. T02/T05 stay
in progress for broader contracts and attribute-based legacy review discovery.
## Authoring deployed; discovery continuation — 2026-10-05
Founder explicitly requested deployment and the remaining implementation.
ADMINISTER @ realm:kubernetes/railiance01, activation=APPROVED. Authoring image
79aabf37 is live through Argo source dfcef87 and Platform pin 0189965. Both health
endpoints and Argo Synced/Healthy pass. Fresh backup/isolated restore preserved
25 confirmed submissions, 174 delivered outbox rows, two records/nine events.
Native selected-workload GET retrieves the exact retained handoff and confirms
the prior human disposition. No new human acceptance is fabricated.
T02 now records explicit initial contracts in LifecycleAdmissionBoundary.md.
Expansion residuals have live records INFD-WP-0008-T01 through T04: external
identity/publication, connectors/retention controls, non-approval binding, optional
Hub write-back. T05 implements named-recipient review discovery in the consumer
and Access Engine FLEX-WP-0034 v13, with fresh read/act checks retained. Validation
671 passed, one skipped; 32 owner embedded policy checks plus owner regressions.
Discovery release and final native checks remain in progress.

View file

@ -0,0 +1,70 @@
---
id: INFD-WP-0008
type: workplan
title: "Lifecycle expansion beyond the initial production audience"
domain: infotech
repo: informed-decision
status: backlog
owner: codex
topic_slug: netkingdom
created: "2026-10-05"
updated: "2026-10-05"
origin: residual
origin_ref: INFD-WP-0007
---
Initial production choices are recorded in docs/specs/LifecycleAdmissionBoundary.md.
This workplan preserves expansion work without claiming it was delivered by the
initial organizational approval service. These tasks do not authorize live changes.
## External workload identity and expanded publication
```task
id: INFD-WP-0008-T01
status: todo
priority: medium
```
Contract with KeyCape and Access Engine for agents outside cluster TokenReview,
additional tenants/audiences and any anonymous publication. Retain per-record
policy and authentication-derived attribution; do not treat arbitrary JWT claims
as admission. Initial authenticated publication remains the supported path.
## Source connector and retention controls
```task
id: INFD-WP-0008-T02
status: todo
priority: medium
```
Select actual source connectors and define their retrieval/credential boundaries,
network restrictions, content limits and provenance. Define retention/export/
erasure requirements and owner review before introducing deletion. Existing
caller-supplied evidence and append-only custody remain the default.
## Non-approval binding profiles
```task
id: INFD-WP-0008-T03
status: todo
priority: medium
```
Define and implement non-approval L0/L2 presentation/disposition workflows against
the shared schema with the identity, authorization and audit owners. Do not create
an Approval Engine object solely to simulate an unrelated binding act. Preserve
schema compatibility, human-only binding and revision-specific evidence.
## Optional State Hub outcome write-back
```task
id: INFD-WP-0008-T04
status: todo
priority: low
```
Independently admit and verify optional resolution write-back with State Hub.
Keep explicit correlation, replay, and uncertain-outcome recovery; a human answer
must never become an execution authorization. Current notification delivery and
agent outcome retrieval remain available without automatic write-back.