From 4aa5f5e9fa045a0c07ce5cdbe073214697524a1a Mon Sep 17 00:00:00 2001 From: stone Date: Mon, 27 Jul 2026 22:24:01 +0800 Subject: [PATCH] control: add local operator authentication (#110) --- STATUS.md | 10 +- applications/control.py | 14 + applications/operator_authentication.py | 269 +++++++++++++++++ eval/catalogs/m0-security-evidence.yaml | 26 +- eval/catalogs/security-catalog.schema.json | 73 ++++- eval/catalogs/security-invariants.yaml | 57 +++- scripts/validate_security_catalog.py | 85 ++++++ .../test_validate_m0_security_evidence.py | 8 +- .../catalog/test_validate_security_catalog.py | 26 +- .../test_local_operator_authentication.py | 280 ++++++++++++++++++ 10 files changed, 825 insertions(+), 23 deletions(-) create mode 100644 applications/operator_authentication.py create mode 100644 tests/unit/test_local_operator_authentication.py diff --git a/STATUS.md b/STATUS.md index 397f6b2a..7147c24f 100644 --- a/STATUS.md +++ b/STATUS.md @@ -39,12 +39,18 @@ capability can never be reported as a passing one. The default application **rejects every credential and performs zero content I/O**. ADR-0068 separately activates one explicit loopback dogfood composition; -it does not widen the default. The following are known, designed, and -deliberately not active: +it does not widen the default. ADR-0069 also admits a separate, short-lived +local operator process only when complete Control, release, dogfood, and worker +credential separation is explicitly configured; it adds no HTTP route and +grants one Control operation per call. Production operator authentication, +multiple operators, durable role assignment, delegation, RBAC, and every +network-reachable operator surface remain `NOT_ACTIVE`. The following are +known, designed, and deliberately not active: | Capability | Note | |---|---| | Production authentication (OAuth / JWT) | Module-level default application is reject-all across all three production authorities (authentication, Organization, Membership) | +| Production operator authentication / admin API | The opt-in local operator composition is one fixed identity per plane, local-process-only, and never a production ancestor | | Durable general Principal / Agent grants | The default scope authority returns seven missing operands; dogfood separately carries the bounded current File operands and binds one configured Agent/purpose to the Release ceiling only | | General / multi-user Source and Resource ACLs | Dogfood uses current mirrored File access plus Membership field rights only; source-native and multi-user authorities remain absent | | General content retrieval | Only the loopback File pgvector dogfood `Acquire` carrier is active | diff --git a/applications/control.py b/applications/control.py index 3b45ef22..ad84211b 100644 --- a/applications/control.py +++ b/applications/control.py @@ -3,8 +3,13 @@ from __future__ import annotations import argparse +import os from collections.abc import Sequence +from applications.operator_authentication import ( + LocalOperatorAuthorities, + LocalOperatorConfiguration, +) from engine.persistence.migrations import migrate_to_head @@ -30,5 +35,14 @@ def main(argv: Sequence[str] | None = None) -> None: print(revision, flush=True) +def local_operator_authorities() -> LocalOperatorAuthorities | None: + """Construct local operator authority only after complete explicit opt-in.""" + + configuration = LocalOperatorConfiguration.load(os.environ) + if configuration is None: + return None + return configuration.authorities() + + if __name__ == "__main__": main() diff --git a/applications/operator_authentication.py b/applications/operator_authentication.py new file mode 100644 index 00000000..87dc1f48 --- /dev/null +++ b/applications/operator_authentication.py @@ -0,0 +1,269 @@ +"""Explicit local-only operator authentication composition.""" + +from __future__ import annotations + +import hmac +from collections.abc import Callable, Mapping +from dataclasses import dataclass, field +from datetime import UTC, datetime, timedelta +from uuid import UUID + +from engine.control import ( + ControlOperation, + ControlOperatorAuthenticationRejected, + ControlOperatorAuthority, + VerifiedControlOperatorIdentity, +) +from engine.learning import ( + ReleaseOperatorAuthenticationRejected, + ReleaseOperatorAuthority, + VerifiedReleaseOperatorIdentity, + release_authority_digest, +) + +CONTROL_OPERATOR_SECRET_ENV = "CONTEXT_ENGINE_CONTROL_OPERATOR_SECRET" +RELEASE_OPERATOR_SECRET_ENV = "CONTEXT_ENGINE_RELEASE_OPERATOR_SECRET" +OPERATOR_ORGANIZATION_ENV = "CONTEXT_ENGINE_OPERATOR_ORGANIZATION_ID" +CONTROL_OPERATOR_OPERATIONS_ENV = "CONTEXT_ENGINE_CONTROL_OPERATOR_OPERATIONS" +DOGFOOD_SECRET_ENV = "CONTEXT_ENGINE_DOGFOOD_SECRET" +WORKER_SECRET_ENV = "CONTEXT_ENGINE_WORKER_LEASE_SIGNING_KEY_HEX" +OPERATOR_ENVIRONMENT_VARIABLES = frozenset( + { + CONTROL_OPERATOR_SECRET_ENV, + RELEASE_OPERATOR_SECRET_ENV, + OPERATOR_ORGANIZATION_ENV, + CONTROL_OPERATOR_OPERATIONS_ENV, + DOGFOOD_SECRET_ENV, + WORKER_SECRET_ENV, + } +) +LOCAL_OPERATOR_TTL = timedelta(minutes=15) +LOCAL_CONTROL_OPERATOR_REF = "operator:local-control:v1" +LOCAL_CONTROL_BINDING_REF = "binding:local-control:v1" +LOCAL_CONTROL_AUTHORITY_REF = "authority:local-control:v1" +LOCAL_RELEASE_OPERATOR_REF = "operator:local-release:v1" +LOCAL_RELEASE_BINDING_REF = "binding:local-release:v1" +LOCAL_RELEASE_AUTHORITY_REF = "authority:local-release:v1" + + +class LocalOperatorConfigurationUnavailable(ValueError): + """The local operator composition is absent, partial, or unsafe.""" + + def __init__(self) -> None: + super().__init__("operator authentication rejected") + + +def _secret(value: object) -> bytes: + if ( + type(value) is not str + or len(value.encode("utf-8")) < 32 + or value != value.strip() + or any(character.isspace() for character in value) + ): + raise LocalOperatorConfigurationUnavailable + return value.encode("utf-8") + + +def _worker_secret(value: object) -> bytes: + if type(value) is not str or len(value) != 64: + raise LocalOperatorConfigurationUnavailable + try: + decoded = bytes.fromhex(value) + except ValueError: + raise LocalOperatorConfigurationUnavailable from None + if len(decoded) != 32: + raise LocalOperatorConfigurationUnavailable + return decoded + + +@dataclass(frozen=True, slots=True) +class LocalOperatorConfiguration: + """One fixed local Control identity and one separate release identity.""" + + organization_id: UUID + control_secret: bytes = field(repr=False) + release_secret: bytes = field(repr=False) + control_operations: frozenset[ControlOperation] = field(repr=False) + + def __post_init__(self) -> None: + if type(self.organization_id) is not UUID: + raise LocalOperatorConfigurationUnavailable + for value in (self.control_secret, self.release_secret): + if type(value) is not bytes or len(value) < 32: + raise LocalOperatorConfigurationUnavailable + if hmac.compare_digest(self.control_secret, self.release_secret): + raise LocalOperatorConfigurationUnavailable + if ( + type(self.control_operations) is not frozenset + or not self.control_operations + or any( + type(operation) is not ControlOperation + for operation in self.control_operations + ) + ): + raise LocalOperatorConfigurationUnavailable + + @classmethod + def load( + cls, + environment: Mapping[str, str], + ) -> LocalOperatorConfiguration | None: + configured = OPERATOR_ENVIRONMENT_VARIABLES.intersection(environment) + if not configured: + return None + if configured != OPERATOR_ENVIRONMENT_VARIABLES: + raise LocalOperatorConfigurationUnavailable + try: + raw_operations = environment[CONTROL_OPERATOR_OPERATIONS_ENV].split(",") + if any(not value or value != value.strip() for value in raw_operations): + raise ValueError + operations = frozenset(ControlOperation(value) for value in raw_operations) + if len(operations) != len(raw_operations): + raise ValueError + configuration = cls( + organization_id=UUID(environment[OPERATOR_ORGANIZATION_ENV]), + control_secret=_secret(environment[CONTROL_OPERATOR_SECRET_ENV]), + release_secret=_secret(environment[RELEASE_OPERATOR_SECRET_ENV]), + control_operations=operations, + ) + configured_secrets = ( + configuration.control_secret, + configuration.release_secret, + _secret(environment[DOGFOOD_SECRET_ENV]), + _worker_secret(environment[WORKER_SECRET_ENV]), + ) + for index, secret in enumerate(configured_secrets): + if any( + hmac.compare_digest(secret, other) + for other in configured_secrets[index + 1 :] + ): + raise LocalOperatorConfigurationUnavailable + return configuration + except (KeyError, TypeError, ValueError, UnicodeError): + raise LocalOperatorConfigurationUnavailable from None + + def authorities( + self, + *, + clock: Callable[[], datetime] | None = None, + ) -> LocalOperatorAuthorities: + active_clock = clock or (lambda: datetime.now(UTC)) + return LocalOperatorAuthorities( + control=ControlOperatorAuthority( + LocalControlOperatorAuthenticator(self, clock=active_clock), + call_ttl=LOCAL_OPERATOR_TTL, + clock=active_clock, + ), + release=ReleaseOperatorAuthority( + LocalReleaseOperatorAuthenticator(self, clock=active_clock), + call_ttl=LOCAL_OPERATOR_TTL, + clock=active_clock, + ), + ) + + def __repr__(self) -> str: + return "LocalOperatorConfiguration()" + + +@dataclass(frozen=True, slots=True) +class LocalOperatorAuthorities: + """Separately scoped authorities constructed only after explicit opt-in.""" + + control: ControlOperatorAuthority + release: ReleaseOperatorAuthority + + +class LocalControlOperatorAuthenticator: + """Constant-time verifier for one fixed local Control identity.""" + + __slots__ = ("_configuration", "_clock") + + def __init__( + self, + configuration: LocalOperatorConfiguration, + *, + clock: Callable[[], datetime], + ) -> None: + if type(configuration) is not LocalOperatorConfiguration: + raise TypeError("operator authentication rejected") + if not callable(clock): + raise TypeError("operator authentication rejected") + self._configuration = configuration + self._clock = clock + + def authenticate(self, opaque_credential: str) -> VerifiedControlOperatorIdentity: + if type(opaque_credential) is not str: + raise ControlOperatorAuthenticationRejected + try: + supplied = opaque_credential.encode("utf-8") + except UnicodeEncodeError: + raise ControlOperatorAuthenticationRejected from None + if not hmac.compare_digest( + supplied, + self._configuration.control_secret, + ): + raise ControlOperatorAuthenticationRejected + now = self._clock() + return VerifiedControlOperatorIdentity( + organization_id=self._configuration.organization_id, + operator_ref=LOCAL_CONTROL_OPERATOR_REF, + authentication_binding_ref=LOCAL_CONTROL_BINDING_REF, + authority_ref=LOCAL_CONTROL_AUTHORITY_REF, + allowed_operations=self._configuration.control_operations, + valid_from=now, + expires_at=now + LOCAL_OPERATOR_TTL, + ) + + def __repr__(self) -> str: + return "LocalControlOperatorAuthenticator()" + + +class LocalReleaseOperatorAuthenticator: + """Constant-time verifier for a separate fixed local release identity.""" + + __slots__ = ("_configuration", "_clock") + + def __init__( + self, + configuration: LocalOperatorConfiguration, + *, + clock: Callable[[], datetime], + ) -> None: + if type(configuration) is not LocalOperatorConfiguration: + raise TypeError("operator authentication rejected") + if not callable(clock): + raise TypeError("operator authentication rejected") + self._configuration = configuration + self._clock = clock + + def authenticate(self, opaque_credential: str) -> VerifiedReleaseOperatorIdentity: + if type(opaque_credential) is not str: + raise ReleaseOperatorAuthenticationRejected + try: + supplied = opaque_credential.encode("utf-8") + except UnicodeEncodeError: + raise ReleaseOperatorAuthenticationRejected from None + if not hmac.compare_digest( + supplied, + self._configuration.release_secret, + ): + raise ReleaseOperatorAuthenticationRejected + now = self._clock() + authority_digest = release_authority_digest( + organization_id=self._configuration.organization_id, + operator_ref=LOCAL_RELEASE_OPERATOR_REF, + authentication_binding_ref=LOCAL_RELEASE_BINDING_REF, + authority_ref=LOCAL_RELEASE_AUTHORITY_REF, + ) + return VerifiedReleaseOperatorIdentity( + organization_id=self._configuration.organization_id, + operator_ref=LOCAL_RELEASE_OPERATOR_REF, + authentication_binding_ref=LOCAL_RELEASE_BINDING_REF, + authority_ref=LOCAL_RELEASE_AUTHORITY_REF, + authority_digest=authority_digest, + valid_from=now, + expires_at=now + LOCAL_OPERATOR_TTL, + ) + + def __repr__(self) -> str: + return "LocalReleaseOperatorAuthenticator()" diff --git a/eval/catalogs/m0-security-evidence.yaml b/eval/catalogs/m0-security-evidence.yaml index 5317f650..43612145 100644 --- a/eval/catalogs/m0-security-evidence.yaml +++ b/eval/catalogs/m0-security-evidence.yaml @@ -574,6 +574,26 @@ "id": "RUNTIME-DOGFOOD-EPOCH-102", "layer": "runtime", "selector": "tests/integration/test_dogfood_runtime_activation.py::test_dogfood_mid_resolve_policy_epoch_change_vetoes_stale_evidence" + }, + { + "id": "RUNTIME-LOCAL-OPERATOR-ABSENT-110", + "layer": "runtime", + "selector": "tests/unit/test_local_operator_authentication.py::test_operator_configuration_is_absent_by_default_and_partial_values_fail_closed" + }, + { + "id": "RUNTIME-LOCAL-OPERATOR-SCOPE-110", + "layer": "runtime", + "selector": "tests/unit/test_local_operator_authentication.py::test_authority_grants_one_allowed_operation_per_context_lifetime" + }, + { + "id": "RUNTIME-LOCAL-OPERATOR-CROSS-PLANE-110", + "layer": "runtime", + "selector": "tests/unit/test_local_operator_authentication.py::test_control_and_release_credentials_are_rejected_across_planes" + }, + { + "id": "RUNTIME-LOCAL-OPERATOR-EXTERNAL-110", + "layer": "runtime", + "selector": "tests/unit/test_local_operator_authentication.py::test_dogfood_and_worker_credentials_are_rejected_by_both_planes" } ], "invariantMappings": [ @@ -862,7 +882,11 @@ "PG-RELEASE-OWNER-019" ], "runtime": [ - "RUNTIME-RELEASE-OWNER-019" + "RUNTIME-RELEASE-OWNER-019", + "RUNTIME-LOCAL-OPERATOR-ABSENT-110", + "RUNTIME-LOCAL-OPERATOR-SCOPE-110", + "RUNTIME-LOCAL-OPERATOR-CROSS-PLANE-110", + "RUNTIME-LOCAL-OPERATOR-EXTERNAL-110" ] } } diff --git a/eval/catalogs/security-catalog.schema.json b/eval/catalogs/security-catalog.schema.json index e8ed8d8d..4983d00f 100644 --- a/eval/catalogs/security-catalog.schema.json +++ b/eval/catalogs/security-catalog.schema.json @@ -95,8 +95,8 @@ }, "activations": { "type": "array", - "minItems": 24, - "maxItems": 24, + "minItems": 25, + "maxItems": 25, "uniqueItems": true, "prefixItems": [ { @@ -1154,6 +1154,57 @@ "non-File providers" ] } + }, + { + "const": { + "issueRef": "#110", + "invariantRef": "RELEASE-OWNER-019", + "carrier": "explicit local-only Control and release operator authentication", + "status": "active_fail_closed", + "policyEpochScope": "not-runtime-authority", + "controlBoundary": "complete local configuration -> separate constant-time credential verification -> fixed Control or release identity -> one operation-bound authority context", + "testEvidence": [ + { + "id": "RUNTIME-LOCAL-OPERATOR-ABSENT-110", + "surface": "tests/unit/test_local_operator_authentication.py::test_operator_configuration_is_absent_by_default_and_partial_values_fail_closed", + "oracle": "Absent configuration constructs no operator authority; every partial configuration produces the same generic refusal without Organization, operation, allowed-set, or credential disclosure." + }, + { + "id": "RUNTIME-LOCAL-OPERATOR-SCOPE-110", + "surface": "tests/unit/test_local_operator_authentication.py::test_authority_grants_one_allowed_operation_per_context_lifetime", + "oracle": "An enumerated Control operation obtains its own bounded TrustedControlCall; an operation outside the exact allowed set receives the same generic refusal, and the call has no authority after its context closes." + }, + { + "id": "RUNTIME-LOCAL-OPERATOR-CROSS-PLANE-110", + "surface": "tests/unit/test_local_operator_authentication.py::test_control_and_release_credentials_are_rejected_across_planes", + "oracle": "The Control and release credentials establish distinct fixed identities and each is rejected by the other plane with the same generic plane-local refusal." + }, + { + "id": "RUNTIME-LOCAL-OPERATOR-EXTERNAL-110", + "surface": "tests/unit/test_local_operator_authentication.py::test_dogfood_and_worker_credentials_are_rejected_by_both_planes", + "oracle": "The dogfood Runtime credential and WorkerLease signing credential are each rejected by both operator authenticators and retained by neither representation." + } + ], + "deferredEvidence": [ + "production operator identity-provider authentication and key lifecycle", + "durable multi-operator assignment, delegation, and revocation", + "network-reachable administrative authentication" + ], + "futureCarriers": [ + "production operator authentication composition", + "durable multi-operator authorization model", + "authenticated administrative API" + ], + "notActive": [ + "production operator authentication", + "a second operator identity", + "role assignment, delegation, or RBAC", + "network-reachable operator surface", + "admin API or admin UI", + "Control operation subcommands", + "Release promotion subcommand" + ] + } } ], "items": false @@ -1464,7 +1515,11 @@ "PROC-FILE-RECLAIM-093", "RUNTIME-DOGFOOD-AUTH-102", "RUNTIME-DOGFOOD-CARRIER-102", - "RUNTIME-DOGFOOD-EPOCH-102" + "RUNTIME-DOGFOOD-EPOCH-102", + "RUNTIME-LOCAL-OPERATOR-ABSENT-110", + "RUNTIME-LOCAL-OPERATOR-SCOPE-110", + "RUNTIME-LOCAL-OPERATOR-CROSS-PLANE-110", + "RUNTIME-LOCAL-OPERATOR-EXTERNAL-110" ] }, "surface": { @@ -1515,7 +1570,8 @@ "#89", "#91", "#93", - "#102" + "#102", + "#110" ] }, "invariantRef": { @@ -1528,7 +1584,8 @@ "SCOPE-INTERSECTION-004", "TRANSPORT-UNTRUSTED-008", "EGRESS-011", - "CITATION-AUTH-010" + "CITATION-AUTH-010", + "RELEASE-OWNER-019" ] }, "carrier": { @@ -1556,7 +1613,8 @@ "autonomous first-attempt dispatch of explicit scheduled File upserts", "bounded autonomous reclaim of expired scheduled File upserts", "loopback-only single-Membership dogfood HTTP authentication", - "loopback-only File pgvector dogfood Acquire delivery" + "loopback-only File pgvector dogfood Acquire delivery", + "explicit local-only Control and release operator authentication" ] }, "status": { @@ -1595,7 +1653,8 @@ "function-only scheduler login -> current page/acquisition/audience/receiver eligibility -> deterministic SKIP LOCKED selector -> database-timed generation-one lease -> existing WorkerLease and File worker", "function-only scheduler login -> expired lease plus database-owned exponential backoff -> current page/acquisition/audience/receiver revalidation -> deterministic recovery-first SKIP LOCKED selector -> exact next-generation WorkerLease -> existing durable-boundary File worker resume", "explicit local environment opt-in -> constant-time bearer verification -> one fixed configured identity -> current UserActor Membership transaction", - "current UserActor transaction -> database-derived exact EffectiveScope -> pre-LIMIT pgvector scope reduction -> CandidateRef -> sealed AuthorizationKernel -> AuthorizedProjection -> ContextPackage" + "current UserActor transaction -> database-derived exact EffectiveScope -> pre-LIMIT pgvector scope reduction -> CandidateRef -> sealed AuthorizationKernel -> AuthorizedProjection -> ContextPackage", + "complete local configuration -> separate constant-time credential verification -> fixed Control or release identity -> one operation-bound authority context" ] }, "testEvidence": { diff --git a/eval/catalogs/security-invariants.yaml b/eval/catalogs/security-invariants.yaml index ef464901..1adc2f20 100644 --- a/eval/catalogs/security-invariants.yaml +++ b/eval/catalogs/security-invariants.yaml @@ -26,7 +26,8 @@ "#89", "#91", "#93", - "#102" + "#102", + "#110" ], "documentRefs": [ "README.md", @@ -57,9 +58,10 @@ "docs/decisions/0058-schedule-only-upserts-from-mixed-file-pages.md", "docs/decisions/0059-dispatch-scheduled-file-imports-through-exact-leases.md", "docs/decisions/0060-reclaim-expired-file-imports-with-bounded-retries.md", - "docs/decisions/0068-activate-loopback-dogfood-runtime.md" + "docs/decisions/0068-activate-loopback-dogfood-runtime.md", + "docs/decisions/0069-admit-an-explicit-local-operator-composition.md" ], - "reconciliation": "Issue #2 fixes the product and testing decisions, issue #5 requires exactly fifteen release invariants and twelve canonical acceptance fixtures, and ADR-0019 resolves the later nineteen-label prose expansion without weakening any safeguard. Issue #15 activates only Organization-level next-request resolve(Acquire) revocation evidence under REVOCATION-006: at that activation, PG-REVOCATION-006, RUN-006, and CACHE-002 are active while BLOB-002 and Continue, citation, Policy-Epoch-bound WorkerLease, production ContextAccessTicket/ActionTicket, audit, outbox, cleanup, finer-epoch, UI, and external-admin carriers remain future or NOT_ACTIVE; later issue records are authoritative for subsequently activated carriers. Issue #16 activates only the M0 refusal gate for unavailable Continue, profile-disabled OpenCitation, and server-owned unavailable Acquire plans: at that activation its real continuation, profile-enabled citation, federated/source-native, and File carriers remain future, while its Runtime and HTTP refusal surfaces prove generic outcomes before content I/O; Issue #69 later activates the private/direct File profile-enabled citation carrier. Issue #17 activates only the signed one-shot persistent no-op durable-job WorkerLease subcarrier under WORKER-LEASE-007. It binds one exact worker audience but no end-user delivery audience or Policy Epoch, and proves only LEASE-SIGNING-017, PG-WORKER-LEASE-NOOP-017, and WORKER-LEASE-REPLAY-007; Source, Resource, Revision, Policy Epoch, end-user delivery audience, idempotency, generation, business mutation, outbox, File publication, and the full ACCEPT-008 matrix remain deferred or NOT_ACTIVE. Issue #18 activates only distinct signed synthetic ContextAccessTicket Provider-read and ActionTicket no-op channel-action subcarriers under ACTION-SEPARATION-014, with current Organization-v0 Policy Epoch validation. TICKET-AUDIENCE-018 and PG-TICKET-EPOCH-018 do not activate production ContextProvider integration, ContextRuntime ticket integration, BotDelivery, full M2 ActionPlane.prepare/perform, a real Sender or external effect, payload/destination/approval/idempotency binding, durable one-shot/replay/reconciliation, or full ACCEPT-012 PASS; those remain future or NOT_ACTIVE. Issue #19 activates only the current Acquire authorized-only ContextRun and restricted delivered-empty DecisionAudit subcarrier under TRACE-REDACTION-012. DIGEST-019, RUN-LINEAGE-019, AUTHORIZED-RUN-019, and PG-TRACE-REDACTION-012 prove deterministic Package and Organization-bound query digests, retained-UserActor-transaction persistence, decisionRef resolution, redaction, and short-lived exact-Organization operator ticket reads with no application-role table access; the supported reader commits deletion before returning, while a direct caller rollback is not claimed as durable exactly-once redemption. Raw query retention, full ContextPackage body retention, unauthenticated transport failures as ContextRuns, cross-Organization analytics, and general observability redaction remain NOT_ACTIVE. Issue #48 activates only the current ACCEPT-002 authenticated HTTP Acquire Membership field-projection carrier under SCOPE-INTERSECTION-004, INDEX-NOT-AUTHORITY-005, and TRACE-REDACTION-012. PROP-FIELD-PROJECTION-048, PG-FIELD-PROJECTION-048, and HTTP-ACCEPT-002-048 bind one current Membership/version field ceiling to same-transaction FORCE-RLS reduction, the sealed AuthorizationKernel, AuthorizedProjection and Evidence integrity, and authorized-only ContextRun/audit persistence. General permission DSLs, caller-authored projection lists, CandidateRef or index field authority, production Provider/source-native ACL negotiation, Supply publication, File/Base field ACL, typed fields, Continue, and Issue #20 runner substitution remain future or NOT_ACTIVE; Issue #69 later activates private/direct File OpenCitation through the same field-projection gates. Issue #63 activates only the digest-only private authenticated HTTP Acquire DeliveryEvidenceRef carrier under TRANSPORT-UNTRUSTED-008. PROP-DELIVERY-EVIDENCE-063, PG-DELIVERY-EVIDENCE-063, HTTP-DELIVERY-EVIDENCE-063, and FILE-DELIVERY-EVIDENCE-063 prove exact service/request/Organization/asker/Membership-version/destination/consumer/purpose/audience/epoch/lifetime binding, stable identical retry identity, role isolation, expiry cleanup, pre-content generic rejection, and one File-backed sealed Runtime delivery. Group AudienceSnapshot, group/public DeliveryEvidenceRef, production ModelGateway, ActionPlane, and the BotDelivery application remain future or NOT_ACTIVE; Issues #64, #66, #69, and #70 later activate the frozen OpenAPI, generated TypeScript SDK, private/direct OpenCitation, and deterministic private model-egress carriers. Issue #65 activates only one opaque digest-only model or channel EgressGrant after final Package policy, exact atomic PostgreSQL redemption and restricted audit, nominal BotDelivery inputs, and deterministic network-free ModelGateway or Sender-preflight spies under EGRESS-011. PROP-EGRESS-011, PG-EGRESS-011, and RUNTIME-EGRESS-011 prove exact Package/Organization/purpose/audience/epoch/hop/profile/lifetime binding and zero additional bytes on replay. Real model/provider calls, a real Sender or channel write, ActionTicket effects, group AudienceSnapshot revalidation, and the BotDelivery application process remain future or NOT_ACTIVE; Issue #64 later activates the generated SDK consumer and Issue #70 later activates the deterministic private TypeScript ModelGateway. Issue #66 activates the frozen public POST /v0/resolve OpenAPI carrier under TRANSPORT-UNTRUSTED-008. OPENAPI-CONTRACT-066, OPENAPI-BREAKING-066, HTTP-V0-066, and PG-RUNTIME-RELEASE-066 prove one public closed operation, deterministic immutable snapshot and breaking-change refusal, a hidden v1 bridge through the same handler and sealed Runtime path, and exact read-only observation of the active Learning-promoted release with fail-closed missing-release behavior before content work. A production BotDelivery caller, Continue redemption, MCP, group AudienceSnapshot, and external effects remain future or NOT_ACTIVE; Issues #64 and #69 later activate the generated TypeScript SDK and private/direct OpenCitation redemption through this frozen operation. Issue #64 activates only the packaged generated TypeScript POST /v0/resolve client under TRANSPORT-UNTRUSTED-008. SDK-CONTRACT-064 and SDK-LIVE-FILE-064 prove deterministic pinned generation, strict closed types, a narrow export map and metadata-only facade, installable tarball consumption, and one real PostgreSQL/File-backed Acquire through CandidateRef, AuthorizationKernel, AuthorizedProjection, ContextPackage, and opaque model egress grant. Issue #69 later extends SDK-LIVE-FILE-064 with a successful private/direct File OpenCitation through a second request-bound DeliveryEvidenceRef; Issue #70 extends the installed SDK fixture into deterministic Package-bound model generation; generated Continue remains generic unavailable. External package publication, production provider access, MCP, group AudienceSnapshot, real Continue redemption, and external effects remain future or NOT_ACTIVE. Issue #67 activates only private ActionPlane.prepare for create-placeholder, finalize-reply, and private-follow-up operation-specific tickets under ACTION-SEPARATION-014. PG-ACTION-PREPARE-067 proves exact current delivery, Organization, destination, audience, source, payload, approval, epoch, lifetime, and idempotency binding under a dedicated non-owner PostgreSQL role with digest-only FORCE-RLS persistence and zero effects. Issue #68 activates private ActionPlane.perform only through a deterministic Sender twin. PG-ACTION-PERFORM-068 proves one pre-Sender current-authority validation, one provider-attempt identity, immutable receipt replay, zero-effect ticket/payload mutation and stale-audience refusal, same-label cross-Organization isolation, and monotonic applied/rejected reconciliation including crash interleavings. Real provider or channel network effects, group AudienceSnapshot, compensation/delete, production BotDelivery orchestration, and the full ACCEPT-012 pass remain future or NOT_ACTIVE. The canonical set is IDs 001 through 012, 014, 015, and 019: CACHE-SCOPE-013 remains a preregistered conditional extension; AUDIENCE-016 is absorbed by SCOPE-INTERSECTION-004 and EGRESS-011; ACL-PROOF-017 is absorbed by INDEX-NOT-AUTHORITY-005 and REVOCATION-006; DELIVERY-EVIDENCE-018 is absorbed by TRANSPORT-UNTRUSTED-008. ACCEPT-001 through ACCEPT-012 follow ADR-0019's category order. Protected-asset references A-01 through A-08 refer, in order, to the eight bullets in the threat model's Protected assets section. Every expectedEvidence value below is a stable planned case identifier, not a claim that the case ran or passed; only an exact activation record upgrades named evidence, while fixture carrier status and the explicit M0 oracle preserve every other accepted-versus-active distinction. Issue #69 activates private/direct File CitationOpenRef issuance and OpenCitation under CITATION-AUTH-010: digest-only multi-use locators reveal only prior Package/Evidence and Fragment location lineage, every open obtains a current UserActor and trusted delivery context then traverses CandidateRef, AuthorizationKernel, AuthorizedProjection, a replacement ContextPackage, EgressGrant, ContextRun, and restricted DecisionAudit. PG-CITATION-AUTH-010, RUNTIME-CITATION-AUTH-010, and SDK-LIVE-FILE-064 prove A/B/A reauthorization, non-consumption on denial, database-clock expiry, cross-kind and cross-Organization opacity, and the generated SDK carrier. Group/public AudienceSnapshot, non-File providers, raw source URL locators, and Continue remain future or NOT_ACTIVE. Issue #70 activates only the private deterministic TypeScript ModelGateway under EGRESS-011. TS-MODEL-EGRESS-070, SDK-MODEL-EGRESS-070, and PG-MODEL-EGRESS-070 prove one current Package, exact grant redemption, closed provider input, bounded Package-subset citations, replay zero bytes, and digest-only retained audit. Real providers, streaming, group AudienceSnapshot, model-authored ActionPlane authority, and external effects remain future or NOT_ACTIVE. Issue #71 activates the complete private File-backed deterministic-twin BotDelivery carrier under TRANSPORT-UNTRUSTED-008, CITATION-AUTH-010, EGRESS-011, and ACTION-SEPARATION-014. TS-PRIVATE-BOT-FLOW-071, SDK-PRIVATE-BOT-FLOW-071, and PG-PRIVATE-BOT-FLOW-071 prove the independent TypeScript process and import boundary, exact verified private event binding, opaque DeliveryEvidenceRef transport, installed generated-SDK HTTP resolve, sealed File Package path, controlled model generation, distinct placeholder/final ActionPlane effects, digest-only DeliveryReceipt audit, citation reopening, and the composed wrong-binding oracles. The historical FIXTURE-ACCEPT-012 continues to prove the M0 unavailable cross-capability baseline separately. Live Feishu/model/Sender network carriers, group/public delivery, compensation/delete, Continue, and MCP remain NOT_ACTIVE. Issue #81 activates deterministic shallow File readChanges and whole-page acknowledgement under WORKER-LEASE-007. PG-FILE-CHANGE-ACTIVATE-081, PG-FILE-CHANGE-PAGE-081, and PG-FILE-CHANGE-DENY-081 prove immutable v3 activation, provider-authenticated content-free paging, exact replay, predecessor ordering, post-commit cursor issuance, FORCE-RLS tenant isolation, source/version invalidation, and zero implicit job or publication effect. Automatic scheduling, deletion execution, recursive discovery, full resync, and Runtime authority from cursor or checkpoint metadata remain NOT_ACTIVE. Issue #83 activates only explicit accepted File page scheduling through the existing file.import acquisition, WorkerLease, and publication path under WORKER-LEASE-007. PG-FILE-CHANGE-SCHEDULE-083, PG-FILE-CHANGE-SCHEDULE-DENY-083, and PG-FILE-CHANGE-SUPERSESSION-083 prove whole-page atomicity, exact job replay, explicit current FileImportAudience and receiver validation, immutable raw-byte observation lineage, pre-compiler drift refusal, and scan-epoch fences before content read and visible publication with zero stale publication effect. Autonomous polling, implicit audience inheritance, deletion execution, automatic retry/reclaim, dead-letter handling, full resync, and Runtime authority from provider checkpoints remain NOT_ACTIVE. Issue #85 activates bounded File delete observations only under WORKER-LEASE-007. PG-FILE-DELETE-DETECT-085, PG-FILE-DELETE-PAGE-085, and PG-FILE-DELETE-NO-EFFECT-085 prove exact latest-complete same-SourceVersion baseline binding, stable shallow diff, immutable canonical persistence/replay, forged/incomplete/stale/cross-Organization refusal, mixed-page scheduling refusal, and zero tombstone, Policy Epoch, cleanup, watermark, or generated-SDK Runtime visibility effect. At that activation, deletion execution remains NOT_ACTIVE; autonomous polling, retry/reclaim/dead-letter, full resync, recursive scan, and Runtime authority from baseline or delete metadata remain NOT_ACTIVE. Issue #87 later activates only exact trusted current File delete execution through the existing #28 tombstone authority under REVOCATION-006. PG-FILE-DELETE-EXECUTE-087, PG-FILE-DELETE-REPLAY-087, and HTTP-FILE-DELETE-INVISIBLE-087 prove current complete-scan revalidation, server-derived effect identity, atomic tombstone/epoch/cleanup/binding, exact replay, mismatch rollback, and immediate sealed-Runtime invisibility. Issue #89 later activates only the exact upsert projection of one current mixed v4 page under WORKER-LEASE-007. PG-FILE-MIXED-UPSERT-SCHEDULE-089, PG-FILE-MIXED-UPSERT-REPLAY-089, and HTTP-FILE-MIXED-UPSERT-NO-DELETE-089 prove complete-page validation, gapped original-ordinal job binding, exact replay, partial-lineage refusal, and zero delete/tombstone authority through the generated SDK. Autonomous scheduling, automatic upsert/delete ordering, batch deletion execution, Provider deletion authority, physical cleanup, restore/recreate, retry/reclaim/dead-letter, full resync, and Runtime authority from observation, page, checkpoint, or execution metadata remain NOT_ACTIVE. Issue #91 activates only scheduler-owned first-attempt File dispatch through the existing exact WorkerLease. Issue #93 extends that carrier with database-timed exponential backoff and at most three automatic higher-generation reclaims through the existing durable-boundary recovery path. Exhausted attempts remain untouched; dead-letter handling, operator remediation, provider polling, and delete ordering remain NOT_ACTIVE. Issue #102 activates only an explicit loopback single-Membership authentication composition and File pgvector Acquire carrier through the same current UserActor transaction and sealed Runtime. RUNTIME-DOGFOOD-AUTH-102, RUNTIME-DOGFOOD-CARRIER-102, and RUNTIME-DOGFOOD-EPOCH-102 prove generic secret or Membership refusal, exact EffectiveScope reduction before ANN LIMIT, deterministic network-free embedding with Release-bound model/input identity, and final Policy Epoch veto. Production authentication, a second human, network exposure beyond the maintainer machine, group/public audience, dogfood OpenCitation, Continue, hybrid retrieval, external query embedding, and non-File providers remain NOT_ACTIVE." + "reconciliation": "Issue #2 fixes the product and testing decisions, issue #5 requires exactly fifteen release invariants and twelve canonical acceptance fixtures, and ADR-0019 resolves the later nineteen-label prose expansion without weakening any safeguard. Issue #15 activates only Organization-level next-request resolve(Acquire) revocation evidence under REVOCATION-006: at that activation, PG-REVOCATION-006, RUN-006, and CACHE-002 are active while BLOB-002 and Continue, citation, Policy-Epoch-bound WorkerLease, production ContextAccessTicket/ActionTicket, audit, outbox, cleanup, finer-epoch, UI, and external-admin carriers remain future or NOT_ACTIVE; later issue records are authoritative for subsequently activated carriers. Issue #16 activates only the M0 refusal gate for unavailable Continue, profile-disabled OpenCitation, and server-owned unavailable Acquire plans: at that activation its real continuation, profile-enabled citation, federated/source-native, and File carriers remain future, while its Runtime and HTTP refusal surfaces prove generic outcomes before content I/O; Issue #69 later activates the private/direct File profile-enabled citation carrier. Issue #17 activates only the signed one-shot persistent no-op durable-job WorkerLease subcarrier under WORKER-LEASE-007. It binds one exact worker audience but no end-user delivery audience or Policy Epoch, and proves only LEASE-SIGNING-017, PG-WORKER-LEASE-NOOP-017, and WORKER-LEASE-REPLAY-007; Source, Resource, Revision, Policy Epoch, end-user delivery audience, idempotency, generation, business mutation, outbox, File publication, and the full ACCEPT-008 matrix remain deferred or NOT_ACTIVE. Issue #18 activates only distinct signed synthetic ContextAccessTicket Provider-read and ActionTicket no-op channel-action subcarriers under ACTION-SEPARATION-014, with current Organization-v0 Policy Epoch validation. TICKET-AUDIENCE-018 and PG-TICKET-EPOCH-018 do not activate production ContextProvider integration, ContextRuntime ticket integration, BotDelivery, full M2 ActionPlane.prepare/perform, a real Sender or external effect, payload/destination/approval/idempotency binding, durable one-shot/replay/reconciliation, or full ACCEPT-012 PASS; those remain future or NOT_ACTIVE. Issue #19 activates only the current Acquire authorized-only ContextRun and restricted delivered-empty DecisionAudit subcarrier under TRACE-REDACTION-012. DIGEST-019, RUN-LINEAGE-019, AUTHORIZED-RUN-019, and PG-TRACE-REDACTION-012 prove deterministic Package and Organization-bound query digests, retained-UserActor-transaction persistence, decisionRef resolution, redaction, and short-lived exact-Organization operator ticket reads with no application-role table access; the supported reader commits deletion before returning, while a direct caller rollback is not claimed as durable exactly-once redemption. Raw query retention, full ContextPackage body retention, unauthenticated transport failures as ContextRuns, cross-Organization analytics, and general observability redaction remain NOT_ACTIVE. Issue #48 activates only the current ACCEPT-002 authenticated HTTP Acquire Membership field-projection carrier under SCOPE-INTERSECTION-004, INDEX-NOT-AUTHORITY-005, and TRACE-REDACTION-012. PROP-FIELD-PROJECTION-048, PG-FIELD-PROJECTION-048, and HTTP-ACCEPT-002-048 bind one current Membership/version field ceiling to same-transaction FORCE-RLS reduction, the sealed AuthorizationKernel, AuthorizedProjection and Evidence integrity, and authorized-only ContextRun/audit persistence. General permission DSLs, caller-authored projection lists, CandidateRef or index field authority, production Provider/source-native ACL negotiation, Supply publication, File/Base field ACL, typed fields, Continue, and Issue #20 runner substitution remain future or NOT_ACTIVE; Issue #69 later activates private/direct File OpenCitation through the same field-projection gates. Issue #63 activates only the digest-only private authenticated HTTP Acquire DeliveryEvidenceRef carrier under TRANSPORT-UNTRUSTED-008. PROP-DELIVERY-EVIDENCE-063, PG-DELIVERY-EVIDENCE-063, HTTP-DELIVERY-EVIDENCE-063, and FILE-DELIVERY-EVIDENCE-063 prove exact service/request/Organization/asker/Membership-version/destination/consumer/purpose/audience/epoch/lifetime binding, stable identical retry identity, role isolation, expiry cleanup, pre-content generic rejection, and one File-backed sealed Runtime delivery. Group AudienceSnapshot, group/public DeliveryEvidenceRef, production ModelGateway, ActionPlane, and the BotDelivery application remain future or NOT_ACTIVE; Issues #64, #66, #69, and #70 later activate the frozen OpenAPI, generated TypeScript SDK, private/direct OpenCitation, and deterministic private model-egress carriers. Issue #65 activates only one opaque digest-only model or channel EgressGrant after final Package policy, exact atomic PostgreSQL redemption and restricted audit, nominal BotDelivery inputs, and deterministic network-free ModelGateway or Sender-preflight spies under EGRESS-011. PROP-EGRESS-011, PG-EGRESS-011, and RUNTIME-EGRESS-011 prove exact Package/Organization/purpose/audience/epoch/hop/profile/lifetime binding and zero additional bytes on replay. Real model/provider calls, a real Sender or channel write, ActionTicket effects, group AudienceSnapshot revalidation, and the BotDelivery application process remain future or NOT_ACTIVE; Issue #64 later activates the generated SDK consumer and Issue #70 later activates the deterministic private TypeScript ModelGateway. Issue #66 activates the frozen public POST /v0/resolve OpenAPI carrier under TRANSPORT-UNTRUSTED-008. OPENAPI-CONTRACT-066, OPENAPI-BREAKING-066, HTTP-V0-066, and PG-RUNTIME-RELEASE-066 prove one public closed operation, deterministic immutable snapshot and breaking-change refusal, a hidden v1 bridge through the same handler and sealed Runtime path, and exact read-only observation of the active Learning-promoted release with fail-closed missing-release behavior before content work. A production BotDelivery caller, Continue redemption, MCP, group AudienceSnapshot, and external effects remain future or NOT_ACTIVE; Issues #64 and #69 later activate the generated TypeScript SDK and private/direct OpenCitation redemption through this frozen operation. Issue #64 activates only the packaged generated TypeScript POST /v0/resolve client under TRANSPORT-UNTRUSTED-008. SDK-CONTRACT-064 and SDK-LIVE-FILE-064 prove deterministic pinned generation, strict closed types, a narrow export map and metadata-only facade, installable tarball consumption, and one real PostgreSQL/File-backed Acquire through CandidateRef, AuthorizationKernel, AuthorizedProjection, ContextPackage, and opaque model egress grant. Issue #69 later extends SDK-LIVE-FILE-064 with a successful private/direct File OpenCitation through a second request-bound DeliveryEvidenceRef; Issue #70 extends the installed SDK fixture into deterministic Package-bound model generation; generated Continue remains generic unavailable. External package publication, production provider access, MCP, group AudienceSnapshot, real Continue redemption, and external effects remain future or NOT_ACTIVE. Issue #67 activates only private ActionPlane.prepare for create-placeholder, finalize-reply, and private-follow-up operation-specific tickets under ACTION-SEPARATION-014. PG-ACTION-PREPARE-067 proves exact current delivery, Organization, destination, audience, source, payload, approval, epoch, lifetime, and idempotency binding under a dedicated non-owner PostgreSQL role with digest-only FORCE-RLS persistence and zero effects. Issue #68 activates private ActionPlane.perform only through a deterministic Sender twin. PG-ACTION-PERFORM-068 proves one pre-Sender current-authority validation, one provider-attempt identity, immutable receipt replay, zero-effect ticket/payload mutation and stale-audience refusal, same-label cross-Organization isolation, and monotonic applied/rejected reconciliation including crash interleavings. Real provider or channel network effects, group AudienceSnapshot, compensation/delete, production BotDelivery orchestration, and the full ACCEPT-012 pass remain future or NOT_ACTIVE. The canonical set is IDs 001 through 012, 014, 015, and 019: CACHE-SCOPE-013 remains a preregistered conditional extension; AUDIENCE-016 is absorbed by SCOPE-INTERSECTION-004 and EGRESS-011; ACL-PROOF-017 is absorbed by INDEX-NOT-AUTHORITY-005 and REVOCATION-006; DELIVERY-EVIDENCE-018 is absorbed by TRANSPORT-UNTRUSTED-008. ACCEPT-001 through ACCEPT-012 follow ADR-0019's category order. Protected-asset references A-01 through A-08 refer, in order, to the eight bullets in the threat model's Protected assets section. Every expectedEvidence value below is a stable planned case identifier, not a claim that the case ran or passed; only an exact activation record upgrades named evidence, while fixture carrier status and the explicit M0 oracle preserve every other accepted-versus-active distinction. Issue #69 activates private/direct File CitationOpenRef issuance and OpenCitation under CITATION-AUTH-010: digest-only multi-use locators reveal only prior Package/Evidence and Fragment location lineage, every open obtains a current UserActor and trusted delivery context then traverses CandidateRef, AuthorizationKernel, AuthorizedProjection, a replacement ContextPackage, EgressGrant, ContextRun, and restricted DecisionAudit. PG-CITATION-AUTH-010, RUNTIME-CITATION-AUTH-010, and SDK-LIVE-FILE-064 prove A/B/A reauthorization, non-consumption on denial, database-clock expiry, cross-kind and cross-Organization opacity, and the generated SDK carrier. Group/public AudienceSnapshot, non-File providers, raw source URL locators, and Continue remain future or NOT_ACTIVE. Issue #70 activates only the private deterministic TypeScript ModelGateway under EGRESS-011. TS-MODEL-EGRESS-070, SDK-MODEL-EGRESS-070, and PG-MODEL-EGRESS-070 prove one current Package, exact grant redemption, closed provider input, bounded Package-subset citations, replay zero bytes, and digest-only retained audit. Real providers, streaming, group AudienceSnapshot, model-authored ActionPlane authority, and external effects remain future or NOT_ACTIVE. Issue #71 activates the complete private File-backed deterministic-twin BotDelivery carrier under TRANSPORT-UNTRUSTED-008, CITATION-AUTH-010, EGRESS-011, and ACTION-SEPARATION-014. TS-PRIVATE-BOT-FLOW-071, SDK-PRIVATE-BOT-FLOW-071, and PG-PRIVATE-BOT-FLOW-071 prove the independent TypeScript process and import boundary, exact verified private event binding, opaque DeliveryEvidenceRef transport, installed generated-SDK HTTP resolve, sealed File Package path, controlled model generation, distinct placeholder/final ActionPlane effects, digest-only DeliveryReceipt audit, citation reopening, and the composed wrong-binding oracles. The historical FIXTURE-ACCEPT-012 continues to prove the M0 unavailable cross-capability baseline separately. Live Feishu/model/Sender network carriers, group/public delivery, compensation/delete, Continue, and MCP remain NOT_ACTIVE. Issue #81 activates deterministic shallow File readChanges and whole-page acknowledgement under WORKER-LEASE-007. PG-FILE-CHANGE-ACTIVATE-081, PG-FILE-CHANGE-PAGE-081, and PG-FILE-CHANGE-DENY-081 prove immutable v3 activation, provider-authenticated content-free paging, exact replay, predecessor ordering, post-commit cursor issuance, FORCE-RLS tenant isolation, source/version invalidation, and zero implicit job or publication effect. Automatic scheduling, deletion execution, recursive discovery, full resync, and Runtime authority from cursor or checkpoint metadata remain NOT_ACTIVE. Issue #83 activates only explicit accepted File page scheduling through the existing file.import acquisition, WorkerLease, and publication path under WORKER-LEASE-007. PG-FILE-CHANGE-SCHEDULE-083, PG-FILE-CHANGE-SCHEDULE-DENY-083, and PG-FILE-CHANGE-SUPERSESSION-083 prove whole-page atomicity, exact job replay, explicit current FileImportAudience and receiver validation, immutable raw-byte observation lineage, pre-compiler drift refusal, and scan-epoch fences before content read and visible publication with zero stale publication effect. Autonomous polling, implicit audience inheritance, deletion execution, automatic retry/reclaim, dead-letter handling, full resync, and Runtime authority from provider checkpoints remain NOT_ACTIVE. Issue #85 activates bounded File delete observations only under WORKER-LEASE-007. PG-FILE-DELETE-DETECT-085, PG-FILE-DELETE-PAGE-085, and PG-FILE-DELETE-NO-EFFECT-085 prove exact latest-complete same-SourceVersion baseline binding, stable shallow diff, immutable canonical persistence/replay, forged/incomplete/stale/cross-Organization refusal, mixed-page scheduling refusal, and zero tombstone, Policy Epoch, cleanup, watermark, or generated-SDK Runtime visibility effect. At that activation, deletion execution remains NOT_ACTIVE; autonomous polling, retry/reclaim/dead-letter, full resync, recursive scan, and Runtime authority from baseline or delete metadata remain NOT_ACTIVE. Issue #87 later activates only exact trusted current File delete execution through the existing #28 tombstone authority under REVOCATION-006. PG-FILE-DELETE-EXECUTE-087, PG-FILE-DELETE-REPLAY-087, and HTTP-FILE-DELETE-INVISIBLE-087 prove current complete-scan revalidation, server-derived effect identity, atomic tombstone/epoch/cleanup/binding, exact replay, mismatch rollback, and immediate sealed-Runtime invisibility. Issue #89 later activates only the exact upsert projection of one current mixed v4 page under WORKER-LEASE-007. PG-FILE-MIXED-UPSERT-SCHEDULE-089, PG-FILE-MIXED-UPSERT-REPLAY-089, and HTTP-FILE-MIXED-UPSERT-NO-DELETE-089 prove complete-page validation, gapped original-ordinal job binding, exact replay, partial-lineage refusal, and zero delete/tombstone authority through the generated SDK. Autonomous scheduling, automatic upsert/delete ordering, batch deletion execution, Provider deletion authority, physical cleanup, restore/recreate, retry/reclaim/dead-letter, full resync, and Runtime authority from observation, page, checkpoint, or execution metadata remain NOT_ACTIVE. Issue #91 activates only scheduler-owned first-attempt File dispatch through the existing exact WorkerLease. Issue #93 extends that carrier with database-timed exponential backoff and at most three automatic higher-generation reclaims through the existing durable-boundary recovery path. Exhausted attempts remain untouched; dead-letter handling, operator remediation, provider polling, and delete ordering remain NOT_ACTIVE. Issue #102 activates only an explicit loopback single-Membership authentication composition and File pgvector Acquire carrier through the same current UserActor transaction and sealed Runtime. RUNTIME-DOGFOOD-AUTH-102, RUNTIME-DOGFOOD-CARRIER-102, and RUNTIME-DOGFOOD-EPOCH-102 prove generic secret or Membership refusal, exact EffectiveScope reduction before ANN LIMIT, deterministic network-free embedding with Release-bound model/input identity, and final Policy Epoch veto. Production authentication, a second human, network exposure beyond the maintainer machine, group/public audience, dogfood OpenCitation, Continue, hybrid retrieval, external query embedding, and non-File providers remain NOT_ACTIVE. Issue #110 activates only the explicit local-process Control and release identity-verification composition under RELEASE-OWNER-019. Its four refusal classes prove absent or partial configuration, exact Control operation scope, cross-plane credential rejection, and mandatory separation from the configured dogfood and worker credentials; every authorized Control operation still receives one lifetime-bound TrustedControlCall. Production authentication, additional operators, durable assignment, delegation, RBAC, network surfaces, operator operation subcommands, and release promotion remain NOT_ACTIVE." }, "hardOracles": [ { @@ -1086,6 +1088,55 @@ "external query embedding", "non-File providers" ] + }, + { + "issueRef": "#110", + "invariantRef": "RELEASE-OWNER-019", + "carrier": "explicit local-only Control and release operator authentication", + "status": "active_fail_closed", + "policyEpochScope": "not-runtime-authority", + "controlBoundary": "complete local configuration -> separate constant-time credential verification -> fixed Control or release identity -> one operation-bound authority context", + "testEvidence": [ + { + "id": "RUNTIME-LOCAL-OPERATOR-ABSENT-110", + "surface": "tests/unit/test_local_operator_authentication.py::test_operator_configuration_is_absent_by_default_and_partial_values_fail_closed", + "oracle": "Absent configuration constructs no operator authority; every partial configuration produces the same generic refusal without Organization, operation, allowed-set, or credential disclosure." + }, + { + "id": "RUNTIME-LOCAL-OPERATOR-SCOPE-110", + "surface": "tests/unit/test_local_operator_authentication.py::test_authority_grants_one_allowed_operation_per_context_lifetime", + "oracle": "An enumerated Control operation obtains its own bounded TrustedControlCall; an operation outside the exact allowed set receives the same generic refusal, and the call has no authority after its context closes." + }, + { + "id": "RUNTIME-LOCAL-OPERATOR-CROSS-PLANE-110", + "surface": "tests/unit/test_local_operator_authentication.py::test_control_and_release_credentials_are_rejected_across_planes", + "oracle": "The Control and release credentials establish distinct fixed identities and each is rejected by the other plane with the same generic plane-local refusal." + }, + { + "id": "RUNTIME-LOCAL-OPERATOR-EXTERNAL-110", + "surface": "tests/unit/test_local_operator_authentication.py::test_dogfood_and_worker_credentials_are_rejected_by_both_planes", + "oracle": "The dogfood Runtime credential and WorkerLease signing credential are each rejected by both operator authenticators and retained by neither representation." + } + ], + "deferredEvidence": [ + "production operator identity-provider authentication and key lifecycle", + "durable multi-operator assignment, delegation, and revocation", + "network-reachable administrative authentication" + ], + "futureCarriers": [ + "production operator authentication composition", + "durable multi-operator authorization model", + "authenticated administrative API" + ], + "notActive": [ + "production operator authentication", + "a second operator identity", + "role assignment, delegation, or RBAC", + "network-reachable operator surface", + "admin API or admin UI", + "Control operation subcommands", + "Release promotion subcommand" + ] } ], "invariants": [ diff --git a/scripts/validate_security_catalog.py b/scripts/validate_security_catalog.py index 54a10043..fdf64d47 100644 --- a/scripts/validate_security_catalog.py +++ b/scripts/validate_security_catalog.py @@ -2269,6 +2269,90 @@ ], } +CANONICAL_LOCAL_OPERATOR_AUTHENTICATION_ACTIVATION: dict[str, object] = { + "issueRef": "#110", + "invariantRef": "RELEASE-OWNER-019", + "carrier": "explicit local-only Control and release operator authentication", + "status": "active_fail_closed", + "policyEpochScope": "not-runtime-authority", + "controlBoundary": ( + "complete local configuration -> separate constant-time credential " + "verification -> fixed Control or release identity -> one " + "operation-bound authority context" + ), + "testEvidence": [ + { + "id": "RUNTIME-LOCAL-OPERATOR-ABSENT-110", + "surface": ( + "tests/unit/test_local_operator_authentication.py::" + "test_operator_configuration_is_absent_by_default_and_partial_" + "values_fail_closed" + ), + "oracle": ( + "Absent configuration constructs no operator authority; every " + "partial configuration produces the same generic refusal without " + "Organization, operation, allowed-set, or credential disclosure." + ), + }, + { + "id": "RUNTIME-LOCAL-OPERATOR-SCOPE-110", + "surface": ( + "tests/unit/test_local_operator_authentication.py::" + "test_authority_grants_one_allowed_operation_per_context_lifetime" + ), + "oracle": ( + "An enumerated Control operation obtains its own bounded " + "TrustedControlCall; an operation outside the exact allowed set " + "receives the same generic refusal, and the call has no authority " + "after its context closes." + ), + }, + { + "id": "RUNTIME-LOCAL-OPERATOR-CROSS-PLANE-110", + "surface": ( + "tests/unit/test_local_operator_authentication.py::" + "test_control_and_release_credentials_are_rejected_across_planes" + ), + "oracle": ( + "The Control and release credentials establish distinct fixed " + "identities and each is rejected by the other plane with the same " + "generic plane-local refusal." + ), + }, + { + "id": "RUNTIME-LOCAL-OPERATOR-EXTERNAL-110", + "surface": ( + "tests/unit/test_local_operator_authentication.py::" + "test_dogfood_and_worker_credentials_are_rejected_by_both_planes" + ), + "oracle": ( + "The dogfood Runtime credential and WorkerLease signing credential " + "are each rejected by both operator authenticators and retained by " + "neither representation." + ), + }, + ], + "deferredEvidence": [ + "production operator identity-provider authentication and key lifecycle", + "durable multi-operator assignment, delegation, and revocation", + "network-reachable administrative authentication", + ], + "futureCarriers": [ + "production operator authentication composition", + "durable multi-operator authorization model", + "authenticated administrative API", + ], + "notActive": [ + "production operator authentication", + "a second operator identity", + "role assignment, delegation, or RBAC", + "network-reachable operator surface", + "admin API or admin UI", + "Control operation subcommands", + "Release promotion subcommand", + ], +} + CANONICAL_ACTIVATIONS: list[dict[str, object]] = [ CANONICAL_REVOCATION_ACTIVATION, CANONICAL_UNAVAILABLE_CAPABILITY_ACTIVATION, @@ -2294,6 +2378,7 @@ CANONICAL_FILE_RECLAIM_ACTIVATION, CANONICAL_DOGFOOD_AUTHENTICATION_ACTIVATION, CANONICAL_DOGFOOD_RUNTIME_ACTIVATION, + CANONICAL_LOCAL_OPERATOR_AUTHENTICATION_ACTIVATION, ] CANONICAL_ACTIVATION_ISSUE_LIST = ", ".join( f"Issue {activation['issueRef']}" for activation in CANONICAL_ACTIVATIONS diff --git a/tests/catalog/test_validate_m0_security_evidence.py b/tests/catalog/test_validate_m0_security_evidence.py index a99e0ac0..7b483c55 100644 --- a/tests/catalog/test_validate_m0_security_evidence.py +++ b/tests/catalog/test_validate_m0_security_evidence.py @@ -118,7 +118,13 @@ def test_planned_catalog_evidence_is_separate_from_executable_refs() -> None: assert mapping["evidenceRefs"] == { "property": ["PROP-RELEASE-OWNER-019"], "postgres": ["PG-RELEASE-OWNER-019"], - "runtime": ["RUNTIME-RELEASE-OWNER-019"], + "runtime": [ + "RUNTIME-RELEASE-OWNER-019", + "RUNTIME-LOCAL-OPERATOR-ABSENT-110", + "RUNTIME-LOCAL-OPERATOR-SCOPE-110", + "RUNTIME-LOCAL-OPERATOR-CROSS-PLANE-110", + "RUNTIME-LOCAL-OPERATOR-EXTERNAL-110", + ], } diff --git a/tests/catalog/test_validate_security_catalog.py b/tests/catalog/test_validate_security_catalog.py index 23c6e826..0a1df4d9 100644 --- a/tests/catalog/test_validate_security_catalog.py +++ b/tests/catalog/test_validate_security_catalog.py @@ -40,6 +40,7 @@ CANONICAL_FILE_MIXED_UPSERT_SCHEDULING_ACTIVATION, CANONICAL_FILE_RECLAIM_ACTIVATION, CANONICAL_INVARIANT_IDS, + CANONICAL_LOCAL_OPERATOR_AUTHENTICATION_ACTIVATION, CANONICAL_MODEL_EGRESS_ACTIVATION, CANONICAL_OPENAPI_V0_ACTIVATION, CANONICAL_PRIVATE_BOT_DELIVERY_ACTIVATION, @@ -595,6 +596,7 @@ def make_catalog() -> dict[str, object]: copy.deepcopy(CANONICAL_FILE_RECLAIM_ACTIVATION), copy.deepcopy(CANONICAL_DOGFOOD_AUTHENTICATION_ACTIVATION), copy.deepcopy(CANONICAL_DOGFOOD_RUNTIME_ACTIVATION), + copy.deepcopy(CANONICAL_LOCAL_OPERATOR_AUTHENTICATION_ACTIVATION), ], "invariants": invariants, "fixtures": fixtures, @@ -721,6 +723,11 @@ def make_schema() -> dict[str, object]: ) }, {"const": copy.deepcopy(CANONICAL_DOGFOOD_RUNTIME_ACTIVATION)}, + { + "const": copy.deepcopy( + CANONICAL_LOCAL_OPERATOR_AUTHENTICATION_ACTIVATION + ) + }, ], "items": False, }, @@ -1122,7 +1129,7 @@ def test_issue_71_activates_private_accept_012_without_rewriting_m0_history( assert isinstance(upgrade_trigger, str) self.assertIn("Issue #71 activates", upgrade_trigger) self.assertEqual( - object_list_at(catalog, "activations")[-10], + object_list_at(catalog, "activations")[-11], CANONICAL_PRIVATE_BOT_DELIVERY_ACTIVATION, ) @@ -1380,7 +1387,7 @@ def test_issue_93_file_reclaim_activation_is_independently_frozen(self) -> None: prefix_items = schema_activations["prefixItems"] assert isinstance(prefix_items, list) schema_activation = object_at( - cast(dict[str, object], prefix_items[-3]), "const" + cast(dict[str, object], prefix_items[-4]), "const" ) expected_boundary = ( @@ -1523,7 +1530,7 @@ def test_issue_70_model_egress_activation_stops_before_real_provider(self) -> No def test_issue_71_private_bot_activation_stops_before_live_providers(self) -> None: catalog = make_catalog() - activation = object_list_at(catalog, "activations")[-10] + activation = object_list_at(catalog, "activations")[-11] self.assertEqual(activation, CANONICAL_PRIVATE_BOT_DELIVERY_ACTIVATION) self.assertEqual(activation["invariantRef"], "ACTION-SEPARATION-014") @@ -1547,7 +1554,7 @@ def test_issue_71_private_bot_activation_stops_before_live_providers(self) -> No def test_issue_81_file_change_activation_stops_before_scheduling(self) -> None: catalog = make_catalog() - activation = object_list_at(catalog, "activations")[-9] + activation = object_list_at(catalog, "activations")[-10] self.assertEqual(activation, CANONICAL_FILE_CHANGE_FEED_ACTIVATION) self.assertEqual(activation["invariantRef"], "WORKER-LEASE-007") @@ -1568,7 +1575,7 @@ def test_issue_81_file_change_activation_stops_before_scheduling(self) -> None: def test_issue_83_file_change_scheduling_stays_explicit(self) -> None: catalog = make_catalog() - activation = object_list_at(catalog, "activations")[-8] + activation = object_list_at(catalog, "activations")[-9] self.assertEqual( activation, @@ -1590,7 +1597,7 @@ def test_issue_83_file_change_scheduling_stays_explicit(self) -> None: def test_issue_85_file_delete_observation_has_no_execution_authority(self) -> None: catalog = make_catalog() - activation = object_list_at(catalog, "activations")[-7] + activation = object_list_at(catalog, "activations")[-8] self.assertEqual( activation, @@ -1614,7 +1621,7 @@ def test_issue_85_file_delete_observation_has_no_execution_authority(self) -> No def test_issue_87_executes_only_current_exact_file_deletes(self) -> None: catalog = make_catalog() - activation = object_list_at(catalog, "activations")[-6] + activation = object_list_at(catalog, "activations")[-7] self.assertEqual( activation, @@ -1635,7 +1642,7 @@ def test_issue_87_executes_only_current_exact_file_deletes(self) -> None: def test_issue_89_schedules_only_the_mixed_page_upsert_projection(self) -> None: catalog = make_catalog() - activation = object_list_at(catalog, "activations")[-5] + activation = object_list_at(catalog, "activations")[-6] self.assertEqual( activation, @@ -2076,7 +2083,7 @@ def test_tracked_catalog_freezes_issue_19_authority_and_bounded_scope( self.assertEqual(catalog["catalogVersion"], "1.3.0") self.assertEqual( - issue_refs[-23:], + issue_refs[-24:], [ "#15", "#16", @@ -2101,6 +2108,7 @@ def test_tracked_catalog_freezes_issue_19_authority_and_bounded_scope( "#91", "#93", "#102", + "#110", ], ) self.assertIn( diff --git a/tests/unit/test_local_operator_authentication.py b/tests/unit/test_local_operator_authentication.py new file mode 100644 index 00000000..5a3119b4 --- /dev/null +++ b/tests/unit/test_local_operator_authentication.py @@ -0,0 +1,280 @@ +from __future__ import annotations + +import ast +from datetime import UTC, datetime, timedelta +from pathlib import Path +from uuid import UUID + +import pytest + +from applications.control import local_operator_authorities +from applications.operator_authentication import ( + CONTROL_OPERATOR_OPERATIONS_ENV, + CONTROL_OPERATOR_SECRET_ENV, + DOGFOOD_SECRET_ENV, + OPERATOR_ORGANIZATION_ENV, + RELEASE_OPERATOR_SECRET_ENV, + WORKER_SECRET_ENV, + LocalControlOperatorAuthenticator, + LocalOperatorAuthorities, + LocalOperatorConfiguration, + LocalOperatorConfigurationUnavailable, + LocalReleaseOperatorAuthenticator, +) +from engine.control import ( + ControlOperation, + ControlOperatorAuthenticationRejected, + VerifiedControlOperatorIdentity, +) +from engine.control.authority import _validate_and_consume_control_call +from engine.learning import ( + ReleaseOperatorAuthenticationRejected, + VerifiedReleaseOperatorIdentity, +) + +ROOT = Path(__file__).parents[2] +NOW = datetime(2026, 7, 27, 13, 0, tzinfo=UTC) +ORGANIZATION_ID = UUID("81e18bca-86a1-478a-937d-7675c6fe69b0") +CONTROL_SECRET = "control-operator-secret-at-least-thirty-two-bytes" +RELEASE_SECRET = "release-operator-secret-at-least-thirty-two-bytes" +DOGFOOD_SECRET = "dogfood-secret-with-at-least-thirty-two-bytes" +WORKER_SECRET = "7" * 64 +WORKER_KEY_MATERIAL = "w" * 32 + + +def environment() -> dict[str, str]: + return { + OPERATOR_ORGANIZATION_ENV: str(ORGANIZATION_ID), + CONTROL_OPERATOR_SECRET_ENV: CONTROL_SECRET, + RELEASE_OPERATOR_SECRET_ENV: RELEASE_SECRET, + CONTROL_OPERATOR_OPERATIONS_ENV: ( + "register_source,activate_file_change_feed,read_source_progress" + ), + DOGFOOD_SECRET_ENV: DOGFOOD_SECRET, + WORKER_SECRET_ENV: WORKER_SECRET, + } + + +def _configuration() -> LocalOperatorConfiguration: + configuration = LocalOperatorConfiguration.load(environment()) + assert configuration is not None + return configuration + + +@pytest.mark.security_evidence( + id="RUNTIME-LOCAL-OPERATOR-ABSENT-110", + layer="runtime", +) +def test_operator_configuration_is_absent_by_default_and_partial_values_fail_closed( + monkeypatch: pytest.MonkeyPatch, +) -> None: + for name in environment(): + monkeypatch.delenv(name, raising=False) + assert LocalOperatorConfiguration.load({}) is None + assert local_operator_authorities() is None + + for missing_name in environment(): + partial = environment() + del partial[missing_name] + with pytest.raises( + LocalOperatorConfigurationUnavailable, + match="operator authentication rejected", + ) as failure: + LocalOperatorConfiguration.load(partial) + rendered = str(failure.value) + assert str(ORGANIZATION_ID) not in rendered + assert "register_source" not in rendered + assert "allowed" not in rendered + assert CONTROL_SECRET not in rendered + assert RELEASE_SECRET not in rendered + + assert CONTROL_SECRET not in repr(_configuration()) + assert RELEASE_SECRET not in repr(_configuration()) + + +def test_control_operations_are_an_exact_enumerated_set() -> None: + configuration = _configuration() + assert configuration.control_operations == frozenset( + { + ControlOperation.REGISTER_SOURCE, + ControlOperation.ACTIVATE_FILE_CHANGE_FEED, + ControlOperation.READ_SOURCE_PROGRESS, + } + ) + + for invalid in ( + "register_source,register_source", + "register_source,", + "register_source, read_source", + "register_source,not_an_operation", + ): + source = environment() + source[CONTROL_OPERATOR_OPERATIONS_ENV] = invalid + with pytest.raises(LocalOperatorConfigurationUnavailable): + LocalOperatorConfiguration.load(source) + + collision_cases = ( + {DOGFOOD_SECRET_ENV: CONTROL_SECRET}, + {RELEASE_OPERATOR_SECRET_ENV: WORKER_KEY_MATERIAL}, + ) + for collision in collision_cases: + source = environment() | collision + with pytest.raises(LocalOperatorConfigurationUnavailable): + LocalOperatorConfiguration.load(source) + + +@pytest.mark.security_evidence( + id="RUNTIME-LOCAL-OPERATOR-CROSS-PLANE-110", + layer="runtime", +) +def test_control_and_release_credentials_are_rejected_across_planes() -> None: + configuration = _configuration() + control = LocalControlOperatorAuthenticator(configuration, clock=lambda: NOW) + release = LocalReleaseOperatorAuthenticator(configuration, clock=lambda: NOW) + + control_identity = control.authenticate(CONTROL_SECRET) + release_identity = release.authenticate(RELEASE_SECRET) + assert type(control_identity) is VerifiedControlOperatorIdentity + assert type(release_identity) is VerifiedReleaseOperatorIdentity + assert control_identity.operator_ref != release_identity.operator_ref + + with pytest.raises( + ControlOperatorAuthenticationRejected, + match="control operator authentication rejected", + ): + control.authenticate(RELEASE_SECRET) + with pytest.raises( + ReleaseOperatorAuthenticationRejected, + match="release operator authentication rejected", + ): + release.authenticate(CONTROL_SECRET) + + rendered = repr(control) + repr(release) + for secret in (CONTROL_SECRET, RELEASE_SECRET): + assert secret not in rendered + + +@pytest.mark.security_evidence( + id="RUNTIME-LOCAL-OPERATOR-EXTERNAL-110", + layer="runtime", +) +def test_dogfood_and_worker_credentials_are_rejected_by_both_planes() -> None: + configuration = _configuration() + control = LocalControlOperatorAuthenticator(configuration, clock=lambda: NOW) + release = LocalReleaseOperatorAuthenticator(configuration, clock=lambda: NOW) + + for credential in (DOGFOOD_SECRET, WORKER_SECRET): + with pytest.raises(ControlOperatorAuthenticationRejected): + control.authenticate(credential) + with pytest.raises(ReleaseOperatorAuthenticationRejected): + release.authenticate(credential) + + rendered = repr(control) + repr(release) + assert DOGFOOD_SECRET not in rendered + assert WORKER_SECRET not in rendered + + for external_secret in (DOGFOOD_SECRET, WORKER_KEY_MATERIAL): + for operator_name in ( + CONTROL_OPERATOR_SECRET_ENV, + RELEASE_OPERATOR_SECRET_ENV, + ): + source = environment() + source[operator_name] = external_secret + with pytest.raises( + LocalOperatorConfigurationUnavailable, + match="operator authentication rejected", + ): + LocalOperatorConfiguration.load(source) + + +@pytest.mark.security_evidence( + id="RUNTIME-LOCAL-OPERATOR-SCOPE-110", + layer="runtime", +) +def test_authority_grants_one_allowed_operation_per_context_lifetime() -> None: + authorities = _configuration().authorities(clock=lambda: NOW) + assert type(authorities) is LocalOperatorAuthorities + + with authorities.control.authorize( + opaque_credential=CONTROL_SECRET, + operation=ControlOperation.REGISTER_SOURCE, + request_id="register-local-source", + ) as first: + assert first.operation is ControlOperation.REGISTER_SOURCE + assert first.expires_at == NOW + timedelta(minutes=15) + _validate_and_consume_control_call( + first, + authority=authorities.control, + expected_operation=ControlOperation.REGISTER_SOURCE, + checked_at=NOW, + ) + with authorities.control.authorize( + opaque_credential=CONTROL_SECRET, + operation=ControlOperation.READ_SOURCE_PROGRESS, + request_id="read-local-source-progress", + ) as second: + assert second.operation is ControlOperation.READ_SOURCE_PROGRESS + assert second is not first + + disallowed = authorities.control.authorize( + opaque_credential=CONTROL_SECRET, + operation=ControlOperation.OFFBOARD_FILE_SOURCE, + request_id="not-allowed", + ) + with pytest.raises( + ControlOperatorAuthenticationRejected, + match="control operator authentication rejected", + ), disallowed: + raise AssertionError("disallowed operation entered authority context") + + with authorities.control.authorize( + opaque_credential=CONTROL_SECRET, + operation=ControlOperation.REGISTER_SOURCE, + request_id="closed-context", + ) as closed_call: + pass + with pytest.raises(ControlOperatorAuthenticationRejected): + _validate_and_consume_control_call( + closed_call, + authority=authorities.control, + expected_operation=ControlOperation.REGISTER_SOURCE, + checked_at=NOW, + ) + + +def test_http_composition_cannot_reach_local_operator_authentication() -> None: + prohibited_module_names = { + "applications.control", + "applications.operator_authentication", + } + pending = [ + ROOT / "applications" / "api.py", + *sorted((ROOT / "adapters" / "http").rglob("*.py")), + ] + visited: set[Path] = set() + while pending: + path = pending.pop() + if path in visited: + continue + visited.add(path) + tree = ast.parse(path.read_text(encoding="utf-8"), filename=path) + imported: set[str] = set() + for node in ast.walk(tree): + if isinstance(node, ast.Import): + imported.update(alias.name for alias in node.names) + elif isinstance(node, ast.ImportFrom) and node.module is not None: + imported.add(node.module) + imported.update( + f"{node.module}.{alias.name}" + for alias in node.names + if alias.name != "*" + ) + assert prohibited_module_names.isdisjoint(imported), path + for module_name in imported: + module_path = ROOT.joinpath(*module_name.split(".")) + for candidate in ( + module_path.with_suffix(".py"), + module_path / "__init__.py", + ): + if candidate.is_file() and candidate not in visited: + pending.append(candidate)