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:
parent
dfcef879d0
commit
da2c5a720f
9 changed files with 224 additions and 10 deletions
|
|
@ -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
|
for the new increment, while preserving human-only dispositions and the evidence
|
||||||
and engine boundaries. L0 and L2 compatibility remain required.
|
and engine boundaries. L0 and L2 compatibility remain required.
|
||||||
|
|
||||||
Open implementation contracts: workload identity beyond cluster service accounts;
|
Initial implementation choices are explicit in `LifecycleAdmissionBoundary.md`:
|
||||||
publication audience policy and any later anonymous publication; source connector
|
only the admitted cluster workload, authenticated publication, caller-supplied
|
||||||
set and retention; non-approval L2 binding integration. T02 must make these
|
research provenance, append-only retention, organizational human approval, and
|
||||||
explicit before implementation depends on them. None authorizes a production
|
no optional Hub outcome write-back. External identities, anonymous publication,
|
||||||
rollout or changes external owners' contracts by declaration.
|
source connectors/retention controls and non-approval binding are expansion work
|
||||||
|
under INFD-WP-0008, not hidden prerequisites or enabled production capabilities.
|
||||||
|
|
|
||||||
33
docs/specs/LifecycleAdmissionBoundary.md
Normal file
33
docs/specs/LifecycleAdmissionBoundary.md
Normal 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.
|
||||||
|
|
@ -2,7 +2,8 @@
|
||||||
|
|
||||||
2026-10-04; `INFD-WP-0007-T02/T03`. This is the internal persistence slice of
|
2026-10-04; `INFD-WP-0007-T02/T03`. This is the internal persistence slice of
|
||||||
`DecisionLifecycleService.md`. The protected application interface built on this
|
`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
|
## 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`,
|
caller. This is audit attribution, not an authentication assertion. Like `Store`,
|
||||||
the repository is an internal persistence interface: the application/API
|
the repository is an internal persistence interface: the application/API
|
||||||
layer must authenticate callers and enforce current access-engine authorization
|
layer must authenticate callers and enforce current access-engine authorization
|
||||||
before invoking it. Supplying a tenant string is not an access grant. No route
|
before invoking it. Supplying a tenant string is not an access grant. The deployed application adapters authenticate and authorize these operations;
|
||||||
or runtime configuration exposes these operations in this increment.
|
this internal persistence interface is never exposed directly.
|
||||||
|
|
||||||
## Commands and reads
|
## Commands and reads
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
A later change to legacy overview discovery needs an Access Engine contract,
|
||||||
not a consumer-side policy bypass. Broader binding profiles, external identities,
|
not a consumer-side policy bypass. Broader binding profiles, external identities,
|
||||||
source connectors and optional Hub write-back remain T02/T05 work.
|
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.
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,8 @@ def build_request(session: HumanSession, memo, action, policy_version, *, intake
|
||||||
extra = {} if intake_context is None else dict(intake_context)
|
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"}:
|
if extra and set(extra) != {"review_group", "memo_class", "intake_submitter", "approval_requester", "approval_human_control"}:
|
||||||
raise ValueError("exact intake policy context required")
|
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",
|
return {"id": str(uuid.uuid4()), "tenant": "tenant:platform",
|
||||||
"subject": {"id": session.subject, "type": "human", "tenant": session.tenant.value,
|
"subject": {"id": session.subject, "type": "human", "tenant": session.tenant.value,
|
||||||
"attributes": {"tenant_source": session.tenant.route.value,
|
"attributes": {"tenant_source": session.tenant.route.value,
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ class ReviewController:
|
||||||
|
|
||||||
def _authorize(self, session, memo, action):
|
def _authorize(self, session, memo, action):
|
||||||
self._memo(session, memo)
|
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:
|
if context is not None:
|
||||||
current = self._approval(session, memo)
|
current = self._approval(session, memo)
|
||||||
if (current['binding'].get('actor') != context['approval_requester']
|
if (current['binding'].get('actor') != context['approval_requester']
|
||||||
|
|
@ -167,6 +167,8 @@ class ReviewController:
|
||||||
def _overview_row(self, session, memo):
|
def _overview_row(self, session, memo):
|
||||||
try:
|
try:
|
||||||
_, observation = self._authorize(session, memo, "list")
|
_, observation = self._authorize(session, memo, "list")
|
||||||
|
except (ApprovalEngineError, ValueError, Conflict):
|
||||||
|
return OverviewRow(memo.id, "unavailable", reason="review_evidence_unavailable")
|
||||||
except ReviewError as exc:
|
except ReviewError as exc:
|
||||||
if exc.code == "session_expired":
|
if exc.code == "session_expired":
|
||||||
raise
|
raise
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,14 @@ import pytest
|
||||||
from informed_decision.disposition import Actor, ActorKind, Verb, DispositionRefused
|
from informed_decision.disposition import Actor, ActorKind, Verb, DispositionRefused
|
||||||
from informed_decision.lifecycle import Attribution, LifecycleRepository
|
from informed_decision.lifecycle import Attribution, LifecycleRepository
|
||||||
from informed_decision.lifecycle_service import Caller, LifecycleService, LifecycleError
|
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.provenance import Claim, Route
|
||||||
from informed_decision.records import memo_from
|
from informed_decision.records import memo_from
|
||||||
from informed_decision.store import Conflict, Store
|
from informed_decision.store import Conflict, Store
|
||||||
from test_intake import envelope
|
from test_intake import envelope
|
||||||
from test_intake_service import setup, request
|
from test_intake_service import setup, request
|
||||||
from test_policy_client import policy
|
from test_policy_client import policy
|
||||||
|
from test_browser_auth import signing_key
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
@ -376,3 +378,79 @@ def test_retained_handoff_api_is_revision_scoped_and_reauthorizes(handoff):
|
||||||
assert request(intake, receipt['envelope'])[0] == 409
|
assert request(intake, receipt['envelope'])[0] == 409
|
||||||
app.policy.transport.change = lambda data: data.update(effect='deny')
|
app.policy.transport.change = lambda data: data.update(effect='deny')
|
||||||
assert call(api, path, query='version=1')[:2] == (403, {'error': 'policy_denied'})
|
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() == []
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ boundary, revision/evidence semantics and acceptance without per-decision deploy
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: INFD-WP-0007-T02
|
id: INFD-WP-0007-T02
|
||||||
status: progress
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "cde0c932-afce-5bec-8cee-a2f377955b9f"
|
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
|
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
|
released; production remains on the accepted initial cutover image. T02/T05 stay
|
||||||
in progress for broader contracts and attribute-based legacy review discovery.
|
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.
|
||||||
|
|
|
||||||
70
workplans/INFD-WP-0008-lifecycle-expansion-contracts.md
Normal file
70
workplans/INFD-WP-0008-lifecycle-expansion-contracts.md
Normal 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.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue