Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion adapters/http/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@
)
from engine.runtime.actor import MembershipRejectionAuditReceipt
from engine.runtime.budget import PackageBudgetRequest
from engine.runtime.citation import (
PRIVATE_FILE_CITATION_OPEN_PROFILE,
CitationAuthorityUnavailable,
)
from engine.runtime.construction import required_kernel_dependencies
from engine.runtime.context_run import ContextRunPersistenceUnavailable
from engine.runtime.contracts import (
Expand Down Expand Up @@ -221,6 +225,7 @@ def create_app(
required_kernel_dependencies(),
clock=clock,
query_digest_keyring=query_digest_keyring,
citation_profile=PRIVATE_FILE_CITATION_OPEN_PROFILE,
)
if runtime is not None and query_digest_keyring is not None:
raise TypeError(
Expand Down Expand Up @@ -486,11 +491,15 @@ def resolve_context(
private_binding = authentication.private_delivery_binding
if delivery_evidence_ref is None:
if private_binding is not None:
if type(runtime_request) is OpenCitation:
return _citation_not_available_response(request_id)
raise TransportAuthenticationFailed
elif (
type(runtime_request) is not Acquire
type(runtime_request) not in {Acquire, OpenCitation}
or type(private_binding) is not VerifiedPrivateDeliveryBinding
):
if type(runtime_request) is OpenCitation:
return _citation_not_available_response(request_id)
raise TransportAuthenticationFailed
try:
organization_verification = selected_organization_authority.verify_existing(
Expand All @@ -509,6 +518,8 @@ def resolve_context(
checked_at=received_at,
)
except (OrganizationVerificationRejected, TypeError, ValueError):
if type(runtime_request) is OpenCitation:
return _citation_not_available_response(request_id)
raise TransportAuthenticationFailed from None
try:
with selected_membership_authority.current_user_actor(
Expand Down Expand Up @@ -651,6 +662,10 @@ def resolve_context(
redemption_request,
)
except DeliveryEvidenceNotAvailable:
if type(runtime_request) is OpenCitation:
return _citation_not_available_response(
invocation.request_id
)
raise TransportAuthenticationFailed from None
except DeliveryEvidenceAuthorityUnavailable:
raise TrustedAuthorityUnavailable from None
Expand Down Expand Up @@ -690,6 +705,8 @@ def resolve_context(
raise TrustedAuthorityUnavailable from None
if membership_rejection_observer is not None:
membership_rejection_observer(error.audit_receipt)
if type(runtime_request) is OpenCitation:
return _citation_not_available_response(request_id)
raise TransportAuthenticationFailed from None
except MembershipAuthorityUnavailable:
raise TrustedAuthorityUnavailable from None
Expand All @@ -699,6 +716,8 @@ def resolve_context(
raise TrustedAuthorityUnavailable from None
except EgressGrantIssuanceUnavailable:
raise TrustedAuthorityUnavailable from None
except CitationAuthorityUnavailable:
raise TrustedAuthorityUnavailable from None
except ScopeAuthorityUnavailable:
raise TrustedAuthorityUnavailable from None
except ActiveReleaseUnavailable:
Expand All @@ -709,6 +728,19 @@ def resolve_context(
return app


def _citation_not_available_response(request_id: str) -> JSONResponse:
"""Return the one non-enumerating result after transport authentication."""

return JSONResponse(
{"kind": "citation_not_available"},
status_code=200,
headers={
"Cache-Control": "no-store",
"X-Context-Request-Id": request_id,
},
)


def _package_budget_from_wire(
body: AcquireWire | ContinueWire,
) -> PackageBudgetRequest | None:
Expand Down
100 changes: 100 additions & 0 deletions docs/decisions/0051-reauthorize-opaque-citation-opens.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
---
name: adr-0051-reauthorize-opaque-citation-opens
version: "1.0.0"
description: >
Issue digest-only multi-use citation locators after authorized projection and
reauthorize every open through the sealed Runtime Kernel.
---

# 0051. Reauthorize every opaque citation open

- Status: accepted
- Date: 2026-07-24
- Refines: ADR-0012, ADR-0013, ADR-0023, ADR-0025, ADR-0028, ADR-0031, ADR-0045, ADR-0046, ADR-0048

## Context

A citation must let a later caller request the exact prior Evidence target, but
the reference cannot preserve the prior caller's authorization. Membership,
Resource access, Source lifecycle, field rights, Policy Epoch, delivery
audience, and egress policy may all differ at open time. Treating the reference
as a bearer capability would bypass those current facts; returning a source URL
would expose both location and authority-sensitive metadata.

The locator is also useful across retries. A denied open must therefore neither
consume it nor extend its lifetime, and denial must not reveal whether the
target once existed.

## Decision

An authorized File `Evidence` receives a server-issued `CitationOpenRef` only
after `CandidateRef -> AuthorizationKernel -> AuthorizedProjection` has
completed. The reference is opaque, type-separated from continuation and egress
capabilities, and included in Evidence integrity and the public Package digest.

PostgreSQL stores only the SHA-256 locator digest, digest/profile and retention
metadata, prior Package/Evidence refs, and exact Resource/Revision/Fragment
location lineage. It stores no source URL, prior principal, Membership,
audience, purpose, Policy Epoch, authorization decision, or bearer. A dedicated
NOLOGIN definer owns three function-only operations. The Runtime login may
issue and redeem through two of them but has no locator-table privilege. A
restricted security-operator login may invoke only exact-Organization cleanup,
which uses database time and deletes digest lineage only after the fixed
profile `retain_until`. FORCE RLS and exact same-Organization foreign keys
remain mandatory.

Redemption is multi-use and content-free. It returns at most one `CandidateRef`
plus prior Package/Evidence location lineage; it does not return content or an
authorization receipt. Database time decides issuance and expiry. Missing,
expired, forged, cross-kind, cross-Organization, disabled, tombstoned, or stale
location lineage maps to the same internal not-available condition without
mutation.

Every active `OpenCitation` obtains a new current `UserActor` transaction and a
new trusted direct or private delivery context. For private delivery, the HTTP
metadata carries a new request-bound `DeliveryEvidenceRef` whose purpose is
`citation.open`; trusted audience facts never enter the body. Runtime computes
the current full trusted scope, feeds the redeemed `CandidateRef` through the
same sealed Kernel locator, scope, field projection, budget, provenance, final
epoch, and audit gates, and never calls candidate discovery. A successful open
produces a replacement audience-bound `ContextPackage`, fresh citation locator,
matching `EgressGrant`, and authorized `ContextRun`. The retained query digest
uses the fixed semantic value `citation.open`, never the locator bearer.

If the locator or current authorization yields no Evidence, Runtime persists
only the existing generic delivered-empty ContextRun/DecisionAudit lineage and
returns `citation_not_available`. It issues no egress grant and exposes no
existence detail. A denied open does not consume, refresh, or otherwise mutate
the original locator, so a later authorized opener can succeed.

Issue #69 activates only private/direct File citation issuance and opening over
the public HTTP v0 contract and generated TypeScript SDK. Group/public
`AudienceSnapshot`, non-File provider citation semantics, and Continue remain
`NOT_ACTIVE`.

## Rationale

Location is sufficient to restart authorization; carrying any previous
decision would create a second authorization system. Multi-use locators make
retries deterministic while current-transaction reauthorization makes every
open independently revocable. Including the locator in Package integrity
prevents substitution without turning it into authority.

## Consequences

- Reader A may issue and reopen a locator; reader B receives only the generic
unavailable outcome; reader A may still reopen it afterward.
- Every content-bearing open crosses `CandidateRef`, `AuthorizationKernel`, and
`AuthorizedProjection` before a replacement Package or grant exists.
- Locator database outages are service unavailability, while validly decided
misses and denials are `citation_not_available`.
- Rollback refuses while locator lineage remains; the dedicated security
operator cleans it only after the versioned citation retention window.
- Ordinary traces and public responses contain neither the locator bearer nor
prior trusted authorization facts.

## Revisit trigger

Revisit before activating group/public citation delivery, a non-File provider's
locator semantics, a different retention profile, or a public citation contract
that cannot preserve the same generic denial and sealed Kernel path.
4 changes: 4 additions & 0 deletions docs/decisions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,10 @@ kernel, capability separation, and publication visibility model.
| File Source progress | [0043 — Separate acquisition and publication progress](0043-separate-file-acquisition-progress-from-publication-progress.md) | Append accepted changes separately from contiguous Runtime-visibility completion and expose them through an Organization/Source-scoped Control read | One ambiguous checkpoint, skipped publication gaps, Runtime authorization from watermarks, or false standard ProviderPort capability claims |
| File Source offboarding | [0044 — Disable before cleanup](0044-disable-file-sources-before-cleanup.md) | One trusted Control transaction terminally disables the Source, advances its Organization Policy Epoch, cancels outstanding work, and records immutable pending cleanup lineage | Cleanup-defined revocation, bulk Resource deletion, application-only lifecycle checks, post-disable leases/tickets, or treating progress as authority |
| Private delivery ingress | [0045 — Redeem private delivery evidence at ingress](0045-redeem-private-delivery-evidence-at-ingress.md) | One digest-only service/request/asker/audience/epoch-bound DeliveryEvidenceRef constructs private TrustedDeliveryContext inside the current UserActor transaction before content work | Raw trusted delivery facts on the wire, bearer persistence, application-role minting/table reads, alternate Runtime paths, or claiming later M2 carriers |
| Exact Package egress | [0046 — Bind egress to one exact Package hop](0046-bind-egress-to-one-exact-package-hop.md) | One digest-only grant binds one exact audience-bound Package to one model or channel preflight hop and redeems atomically | Treating Package construction as disclosure authority, arbitrary content at egress, cross-hop reuse, or bypassing final policy |
| Public OpenAPI v0 | [0047 — Freeze OpenAPI v0 through one Runtime path](0047-freeze-openapi-v0-through-one-runtime-path.md) | One public `/v0/resolve` schema and a hidden provisional v1 bridge share the same sealed Runtime; Package release lineage is read-only from the Learning-published active manifest | Two authorization compositions, caller-authored release facts, Runtime publication/fallback, or in-place mutation of historical snapshots |
| Generated TypeScript SDK | [0048 — Generate the TypeScript SDK behind a closed facade](0048-generate-typescript-sdk-behind-a-closed-facade.md) | OpenAPI v0 generates the semantic contract and internal fetch client; a package export map exposes only a metadata-safe facade and the contract checksum | Handwritten wire schemas, raw generated transport exports, arbitrary caller headers, floating generator/runtime versions, or claiming inactive capability redemption |
| Citation open | [0051 — Reauthorize opaque citation opens](0051-reauthorize-opaque-citation-opens.md) | Digest-only multi-use locators recover only content-free target lineage; every open uses a current UserActor and the sealed Kernel to produce a replacement Package | Bearer authority, prior-decision reuse, source URLs, locator consumption on denial, or content before exact reauthorization |

Each baseline ADR is `accepted` and contains Context, Decision, Rationale,
Consequences, and Revisit trigger sections. A revisit trigger permits review; it
Expand Down Expand Up @@ -127,6 +129,7 @@ touched:
- [0030 — Bound ticket audiences](0030-bound-ticket-audiences.md)
- [0049 — Exact private ActionPlane prepare](0049-prepare-one-exact-private-effect.md)
- [0050 — Exact private ActionPlane perform](0050-perform-one-exact-private-effect.md)
- [0051 — Reauthorize opaque citation opens](0051-reauthorize-opaque-citation-opens.md)
- [0031 — Authorized-only ContextRun lineage](0031-persist-authorized-context-run-lineage.md)
- [0032 — Membership-bound materialized fields](0032-bind-materialized-fields-to-membership-projection-rights.md)
- [0033 — Organization release promotion owner](0033-promote-organization-releases-through-one-learning-owner.md)
Expand All @@ -142,3 +145,4 @@ touched:
- [0043 — Separate File acquisition and publication progress](0043-separate-file-acquisition-progress-from-publication-progress.md)
- [0044 — Disable File sources before cleanup](0044-disable-file-sources-before-cleanup.md)
- [0045 — Redeem private delivery evidence at ingress](0045-redeem-private-delivery-evidence-at-ingress.md)
- [0046 — Bind egress to one exact Package hop](0046-bind-egress-to-one-exact-package-hop.md)
4 changes: 4 additions & 0 deletions engine/persistence/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
PostgreSQLAccessPolicyControl,
ResourceAccessRevocation,
)
from engine.persistence.citation import PostgreSQLCitationOpenRetentionPort
from engine.persistence.configuration import (
ACTION_EXECUTE_DEFINER_ROLE,
ACTION_PREPARE_DEFINER_ROLE,
ACTION_ROLE,
CITATION_DEFINER_ROLE,
DELIVERY_EVIDENCE_DEFINER_ROLE,
EGRESS_GRANT_DEFINER_ROLE,
EGRESS_ROLE,
Expand Down Expand Up @@ -94,6 +96,7 @@
"DatabaseConfiguration",
"ACTION_PREPARE_DEFINER_ROLE",
"ACTION_ROLE",
"CITATION_DEFINER_ROLE",
"AccessChangeRejected",
"AccessPolicyControlUnavailable",
"DatabaseConfigurationError",
Expand Down Expand Up @@ -121,6 +124,7 @@
"ContextRunView",
"OperatorAuthorizationProvenance",
"PostgreSQLContextRunReader",
"PostgreSQLCitationOpenRetentionPort",
"PostgreSQLControlStore",
"PostgreSQLDeliveryEvidenceIssuerPort",
"PostgreSQLDeliveryEvidenceRetentionPort",
Expand Down
36 changes: 36 additions & 0 deletions engine/persistence/citation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Restricted PostgreSQL cleanup for retained citation-locator lineage."""

from __future__ import annotations

from uuid import UUID

from sqlalchemy import Engine, text
from sqlalchemy.exc import SQLAlchemyError

from engine.persistence.role_guard import assert_security_operator_role
from engine.runtime.citation import CitationAuthorityUnavailable


class PostgreSQLCitationOpenRetentionPort:
"""Delete retained digests through the dedicated security-operator login."""

def __init__(self, engine: Engine) -> None:
self._engine = engine

def delete_expired_lineage(self, organization_id: UUID) -> int:
try:
with self._engine.begin() as connection:
assert_security_operator_role(connection)
deleted = connection.execute(
text(
"SELECT "
"context_security_delete_expired_citation_open_lineage("
":organization_id)"
),
{"organization_id": organization_id},
).scalar_one()
except (AssertionError, SQLAlchemyError):
raise CitationAuthorityUnavailable from None
if type(deleted) is not int or deleted < 0:
raise CitationAuthorityUnavailable
return deleted
1 change: 1 addition & 0 deletions engine/persistence/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
ACTION_EXECUTE_DEFINER_ROLE = "context_engine_action_execute_definer"
EGRESS_GRANT_DEFINER_ROLE = "context_engine_egress_grant_definer"
DELIVERY_EVIDENCE_DEFINER_ROLE = "context_engine_delivery_evidence_definer"
CITATION_DEFINER_ROLE = "context_engine_citation_definer"
ACCESS_POLICY_DEFINER_ROLE = "context_engine_access_policy_definer"
WORKER_LEASE_DEFINER_ROLE = "context_engine_worker_lease_definer"
CONTEXT_RUN_READER_DEFINER_ROLE = "context_engine_context_run_reader_definer"
Expand Down
Loading
Loading