From e0bba57551f9518c292cbc1ceb78d2c2ba32c75d Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Sat, 19 Sep 2026 04:24:28 +0200 Subject: [PATCH 1/3] fix(runtime): align the served control-plane profile --- ...-served-control-plane-profile-preflight.md | 239 ++++++++++++ docs/requirements/API-404/requirement.md | 1 + .../bundles/retest-v32.json | 2 +- .../execution-snapshot-v32.json | 4 +- .../specification-coverage/analysis-v32.json | 2 +- ...specification-coverage-issue-1015-v32.json | 4 +- .../execution-snapshot-v32.json | 2 +- .../control_plane_api/_operation_routes.py | 78 ++-- .../control_plane_api/_participant_routes.py | 45 +-- .../control_plane_api/_responses.py | 35 +- .../control_plane_api/_workflow_routes.py | 18 +- .../raes_runtime/control_plane_api_guards.py | 6 +- ...control_plane_api_participant_retrieval.py | 33 +- .../tests/test_runtime_control_plane_api.py | 342 +++++++++++++++++- 14 files changed, 670 insertions(+), 141 deletions(-) create mode 100644 docs/decisions/issue-1188-served-control-plane-profile-preflight.md diff --git a/docs/decisions/issue-1188-served-control-plane-profile-preflight.md b/docs/decisions/issue-1188-served-control-plane-profile-preflight.md new file mode 100644 index 000000000..212c5d9d9 --- /dev/null +++ b/docs/decisions/issue-1188-served-control-plane-profile-preflight.md @@ -0,0 +1,239 @@ +# Issue 1188 Served Control-Plane Profile Alignment Preflight + +Date: 2026-09-19 + +Issue: #1188. Requirement: API-404. Decision: ADR-104. Work package: CP-8. + +## Decision + +Profile P2 is the existing reference HTTP adapter in front of the same +`RuntimeControlPlane` and one P1 owner. It is not a second operation service, +identity model, authorization namespace, audit writer, persistence workflow, or +mutation scheduler. P2 keeps one immutable target/run store scope, one +process-lifetime owner lease, one core mutation authority, and one application +worker. The HTTP layer authenticates, validates transport shape, performs +bounded admission/offload, and maps core outcomes; the core remains the +authority for referenced-subject authorization, operation context, claims, +terminal state, and audit. + +`ControlPlaneIdentity` is the authenticated transport principal and +`OperationAdmissionContext` is the immutable core/persisted authority context. +`operation_actor_scope()` and `operation_admission_context()` are the only +conversion boundary between them. Do not introduce an actor DTO, proxy-user +schema, route-local scope tuple, or caller-supplied actor field. Every HTTP +mutation passes the authenticated `ControlPlaneIdentity` to the core. The core +must reject a missing or untyped identity on a P2 call before a claim or backend +invocation; the existing `identity=None` trusted-embedder behavior remains only +for explicit P0/P1 in-process calls. + +Authorization is evaluated against the authoritative referenced object before +disclosure or claim: + +- target-wide plan operations, snapshots, and operational summaries require + the existing exact target binding and applicable role; +- participant-addressed mutations and governed projections additionally use + the existing `ParticipantControlSubjectBinding` or + `ParticipantAudienceSubjectBinding` policy, after resolving the referenced + participant or execution scope through incumbent compiled/runtime state; +- operation status and idempotent replay require the original actor and full + immutable authorization scope, plus the admitted target/run and operation + kind; absent and unauthorized operation ids have the same response; +- startup reconciliation has no new caller: it preserves the parent record's + immutable actor/scope and produces its terminal audit from that context; +- indeterminate resolution retains CP-3 semantics: a newly authenticated + target-bound operator may be a different actor, but must be reauthorized + against the parent's target/run and participant scopes. The linked child gets + the resolver's context and the parent actor is never rewritten. + +Role checks in `_ControlPlaneApiAuth` are transport admission, not sufficient +participant or operation authorization. Family-specific core checks in +participant control, crossing, retrieval, recovery, and operation reads are the +incumbents to share or converge. Do not copy their policy into route handlers or +infer authority from an operation id, idempotency key, receipt, request path, +snapshot revision, or auditor role. + +## Proxy and credential boundary + +`ControlPlaneSecurityConfig.strict_defaults()` remains fail closed. Bearer +tokens retain precedence and constant-time comparison; an invalid presented +bearer token never falls through to proxy headers. Proxy identity is accepted +only when `trust_proxy_identity_headers=True` is supplied by trusted application +composition. That flag is the deployer's assertion that the adapter is not +directly reachable and that the authenticating proxy strips both configured +identity headers from every external request before setting its own values. +Client-provided `x-raes-client-verified`, a configurable equivalent, source IP, +or TLS presence is not proof of that topology. + +Strengthen the existing config admission rather than adding an environment +loader: configured header names must be valid, distinct, and must not alias the +Authorization header; bearer keys and principal names must be non-empty; roles, +target names, actor ids, and subject bindings must fit the existing closed +context bounds; and duplicate/ambiguous subject bindings fail composition. +`trusted_identities` and `bearer_tokens` remain immutable copies. P2 adds no +token environment variable, secret file, command-line token, dynamic principal +registration endpoint, or request-body identity override. + +## Mutation, audit, and failure boundary + +All accepted mutation families continue through `RuntimeMutationAuthority`, +the atomic store claim, and `RuntimeDurabilityMixin`. A terminal operation uses +`commit_terminal_operation()` (or the incumbent participant transition +equivalent) to publish the snapshot/revision outcome, terminal record, and +`terminal_operation_audit()` together. Routes never append a success/failure +terminal audit. Remove the post-hoc receipt-audit calls and their no-op seam +rather than leaving an apparent second audit workflow. Authentication, +authorization/admission denial, request rejection, and read-access audits remain +separate bounded operational evidence and never masquerade as terminal audit. + +HTTP failure mapping is one shared adapter concern. Expected authorization, +not-found, claim/revision conflict, validation, and overload outcomes receive +stable coarse envelopes. Unexpected backend, store, policy-provider, or audit +provider failures receive the existing redacted 500 envelope. Route code must +not return `str(exc)` or provider-selected messages; the backend failure +diagnostic must not vary with exception text or class name. Tokens, raw headers, +request bodies, concrete request/host/database paths, SQL/WAL details, +credentials, tracebacks, exception chains, and provider-native values are +absent from responses, diagnostics, audit fields, and logs. + +A failed secondary audit must not replace an already selected 4xx/5xx response. +The request-size middleware and global exception handlers therefore need the +same best-effort audit rule. `_LOGGER.exception()` is not a safe provider-error +fallback because it emits a traceback and exception chain; log only a stable, +bounded event label. Do not create a public provider exception hierarchy. + +## Read and concurrency boundary + +`_ControlPlaneCallExecutor` remains the bounded ASGI offload owner: +`mutate()` reserves bounded mutation admission and delegates serialization to +the core; `run()` keeps non-mutating reads/audits off the event loop. The +process-bound store lease, `require_single_worker_configuration()` checks for +`WEB_CONCURRENCY`/`UVICORN_WORKERS`, and post-fork lease check remain cumulative +guards. A database uniqueness index, asyncio lock, or application-local queue +is not multi-worker coordination. The repository still has no serve command; +TLS termination, proxy configuration, supervisor flags, and service-account +privileges remain deployment responsibilities and no P3 claim follows. + +Side-effect-free retrievals use `run()` and `_project_snapshot_read()` so they +can proceed while backend work is in flight. Each snapshot-derived response +must be projected from one authoritative `SnapshotState` cut and carry that +cut's logical revision in `X-RAES-Snapshot-Revision`; provider transaction ids +and response-time rereads are not revisions. `get_operation()` reads the store +and authorizes the immutable record context before returning it. + +RUN-319 governed participant egress is deliberately different: when a retrieval +must append crossing history before disclosure, it is a read-shaped mutation +and must stay on the one mutation authority with revision/history-head checks. +Only the legacy/pure projection path may bypass mutation admission. Do not make +governed egress appear noncontending by returning before its evidence commit, +moving crossing history to the audit log, or introducing a second evidence +queue/writer. If nonblocking latency is later required for governed egress too, +that requires a separate decision reconciling ADR-104's single mutation +authority with RUN-319's commit-before-serialization rule; CP-8 must not weaken +either implicitly. + +## Canonical incumbents + +- Authentication/configuration: `ControlPlaneSecurityConfig`, + `ControlPlaneIdentity`, `ControlPlaneRole`, participant subject bindings, + `_ControlPlaneApiAuth`, and `ControlPlaneSecurityConfig.strict_defaults()`. +- Transport shape/admission: closed FastAPI/Pydantic request models, + `RequestSizeLimitMiddleware`, `RejectionAuditExecutor`, and + `_ControlPlaneCallExecutor`. +- Authority context and idempotency: `OperationAdmissionContext`, + `operation_actor_scope()`, `operation_admission_context()`, + `require_idempotency_key()`, `_require_same_idempotency_replay()`, and the + atomic provider claim. Caller `request_fingerprint` remains non-authoritative. +- Mutation and lifecycle: `RuntimeMutationAuthority`, `mutation_entry()`, + `RuntimeDurabilityMixin`, `TerminalCommitMode`, the closed operation + transition/diagnostic helpers, and CP-3 resolution/reconciliation. +- Persistence/coherence: `ControlPlaneStore`, `ControlPlaneStoreCommitAdapter`, + `InMemoryControlPlaneStore`, `LocalControlPlaneStore`, `SnapshotState`, + revision CAS, strict operation/audit codecs, immutable store scope, and + `RuntimeOwnerLease`. +- DTOs and schemas: the existing `OperationReceiptModel`, + `OperationStatusModel`, `RuntimeSnapshotEnvelopeModel`, participant view + models, generated control-plane schemas, and `portable_diagnostic_payload()`. + No HTTP-only copies or provider-specific public carriers are needed. +- Semantic validation: `control_plane_plan_diagnostics()`, planner-produced + plan authorization, backend input/result/snapshot-transition validation, + participant control/crossing policy gates, and participant retrieval + projection validators. Transport validation does not duplicate them. +- Secret handling: `value_free_account_placement_payload()`, canonical + value-free request commitments, ephemeral exact retry proof, and backend + account-credential sanitizers. Digests do not make a secret safe to persist. +- Observability: `AuditEvent`, `terminal_operation_audit()`, stable `Diagnostic` + codes, ADR-066, and module loggers with value-free fields. Audit, logs, + participant crossing history, captured evidence, and archival provenance are + distinct concepts. + +## Cross-cutting gates + +| Layer | Required passage | +| --- | --- | +| HTTP pre-parser | Request body crosses `RequestSizeLimitMiddleware`; oversized or malformed length fails before parsing or dispatch and uses bounded best-effort rejection audit. | +| Authentication | Bearer or explicitly enabled verified-proxy resolution produces one configured `ControlPlaneIdentity`; bearer failure cannot fall through and proxy headers are never accepted under strict defaults. | +| Transport shape | Existing closed request models reject unknown members and validate field bounds before domain reconstruction. Header idempotency uses the single core key validator. | +| Authorization | Route role/target admission is followed by core participant/operation/reference authorization using authoritative state and the typed identity, before claim, backend work, or disclosure. | +| Immutable context | `operation_admission_context()` validates and freezes actor, complete scope, target/run, kind, parent, and value-free commitment in the existing carrier. | +| Semantic/backend gates | Existing plan, realization, participant, information-flow, credential, backend-return, and snapshot-transition validators run once at their current authority boundaries. | +| Persistence | Atomic claim, expected revision/history heads, strict record codec, terminal transaction/audit binding, immutable store scope, lease admission, and readback/poison behavior remain mandatory. | +| Secret/OS exposure | Security config is explicit in-memory composition; no secret/path enters env or argv. Local storage retains private modes, anti-link checks, identity-pinned opens, admitted WAL/full-sync behavior, and integrity checks. | +| Error envelope/logging | Shared coarse 401/403/404/409/413/422/500/503 responses and safe diagnostics/log labels contain no exception/provider/request/credential/path material. | +| Response coherence | Snapshot-derived output and revision come from the same pinned cut; operation status comes from the authoritative record after context authorization. | + +## Extension seam + +The seam for the next authentication mechanism is a resolver that produces the +existing `ControlPlaneIdentity`; the seam for the next operation family is one +core authorization decision parameterized by existing `OperationKind`, target, +and resolved participant/operation subjects, followed by the existing context, +claim, mutation, and terminal-commit path. Do not build a generic policy DSL or +route middleware registry. A future P3 provider may change ownership and +coordination beneath these contracts only after its own decision; it does not +change actor or claim identity. + +## Verification guardrails + +Extend the existing HTTP auth, target-binding, request-size, bounded-admission, +offload, overload, and redacted-error suites rather than creating a parallel +P2 harness. Cover bearer and trusted-proxy paths, default proxy spoofing, +invalid-bearer precedence, both worker environment variables plus process/lease +contention, and identity/config shape failures. + +Cross-principal tests must exercise equal idempotency keys, guessed operation +ids, changed scopes, participant bindings, target/run mismatch, retry, status, +and linked resolution without becoming existence or receipt oracles. Atomicity +tests must inject commit rollback/unknown outcomes and prove one terminal record +and one matching core audit, with no route audit. Error tests inject secrets, +paths, SQL, bodies, exception strings, and traceback-bearing provider failures +and scan response, diagnostics, audit, and captured logs. Read tests hold a +backend mutation open and prove pure snapshot/status/summary/legacy participant +reads complete with their observed revision; governed RUN-319 egress remains a +separately asserted mutation contract. + +## Non-goals and anti-patterns + +- No P3, multiple application workers, multi-host ownership, leader election, + fencing, durable broker, distributed queue, shared-cache coherence, tenant + multiplexing, cross-target/run store, or exactly-once backend-effect claim. +- No new identity, actor, receipt, status, revision, audit, provider-error, + participant-subject, or idempotency schema; no second validator, serializer, + exception hierarchy, audit sink, or mutation lock. +- No identity-less HTTP core mutation, route-created actor string, trusted + client identity header by default, bearer-to-proxy fallback, proxy trust + inferred from a header, IP, or TLS alone, or mutable principal map. +- No route-only subject authorization, authorization by possession, broad + auditor override of actor-bound status, operation-id enumeration, or 403/404 + distinction that reveals a protected object. +- No post-hoc terminal audit, duplicate retry audit, terminal `save_record()`, + cache-authoritative receipt/status, lookup-then-claim, backend replay, or + store transaction across external work. +- No `detail=str(exc)`, exception class/message in a provider diagnostic, + traceback logging, raw request path/body/header/token, SQL, database path, or + credential in an observable envelope. +- No claim that a mutating RUN-319 projection is a noncontending read; no + response before required crossing evidence commits and no audit substitution + for participant history. +- No new serve CLI, TLS/proxy implementation, environment-based principal or + secret loader, service unit, health/runbook surface, profile discovery, SDL + semantics, backend effect semantics, or `RuntimeManager` integration. diff --git a/docs/requirements/API-404/requirement.md b/docs/requirements/API-404/requirement.md index 4dc6647ca..65c69e460 100644 --- a/docs/requirements/API-404/requirement.md +++ b/docs/requirements/API-404/requirement.md @@ -129,6 +129,7 @@ Requirement inventory phase. Status audit deferred until the full canonical grap - TESTS → TEST `implementations/python/tests/test_issue_1181_unified_control_plane_mutations.py` (CP-2 authority, claim ordering, validation gate, atomicity, audit provenance, capability, recovery, and facade-boundary acceptance tests) - TESTS → TEST `implementations/python/tests/test_issue_1183_store_ownership_leases.py` (CP-5 admission, scope binding, process ownership, shutdown ordering, and worker-posture acceptance tests) - DOCUMENTS → DOCUMENTATION `docs/decisions/issue-1184-atomic-idempotency-claims-preflight.md` (CP-7 atomic claim, replay authorization, migration, and cache-authority boundaries) +- DOCUMENTS → DOCUMENTATION `docs/decisions/issue-1188-served-control-plane-profile-preflight.md` (CP-8 P2 identity, authorization, audit, read, error-redaction, and single-owner boundaries) - TESTS → TEST `implementations/python/tests/test_issue_1184_atomic_idempotency_claims.py` (CP-7 atomic scoped claims, replay conflicts, migration, authorization, and authoritative-read regressions) - TESTS → TEST `implementations/python/tests/test_dsl_437_snapshot_durability_conformance.py` (Snapshot durability conformance under explicit local-store admission) - TESTS → TEST `implementations/python/tests/test_realization_envelope_contract.py` (Realization envelope persistence under explicit local-store admission) diff --git a/docs/research/formal-semantic-validation/bundles/retest-v32.json b/docs/research/formal-semantic-validation/bundles/retest-v32.json index 5a05e0c8d..fd909d362 100644 --- a/docs/research/formal-semantic-validation/bundles/retest-v32.json +++ b/docs/research/formal-semantic-validation/bundles/retest-v32.json @@ -118,5 +118,5 @@ "protocol_sha256": "abf94093e344bf495dfb04e8b0c5985c0beaab8ebb17a75e15c8674fa81b1a7c", "revision": "33.0.0", "snapshot_path": "docs/research/formal-semantic-validation/execution-snapshot-v32.json", - "snapshot_sha256": "8f234beda5b36f75a08cc72dbb990bc818f878b2e01b1839c158e55c039ff8bd" + "snapshot_sha256": "9c9ecae2fa8341cb65a5dc2b6760b49d2ef65ecd5777a5466c55bcca8c0bbd53" } diff --git a/docs/research/formal-semantic-validation/execution-snapshot-v32.json b/docs/research/formal-semantic-validation/execution-snapshot-v32.json index c97627dcc..3759e1c81 100644 --- a/docs/research/formal-semantic-validation/execution-snapshot-v32.json +++ b/docs/research/formal-semantic-validation/execution-snapshot-v32.json @@ -3,7 +3,7 @@ "execution_id": "issue-1183-execution-v31", "release_path": "docs/research/formal-semantic-validation/bundles/retest-v31.json", "release_revision": "32.0.0", - "release_sha256": "f0be18219af353ff931280ca7a5fec87fcf11c099d7995b4ccac0143ca7bd8fe" + "release_sha256": "c21fb3d252b94384ff5a085c11f3bb0cf05410ba4faa73f4f268d883235e27db" }, "captured_at": "2026-09-18T07:14:22+00:00", "commands": [ @@ -645,7 +645,7 @@ "source_state": { "base_revision": "9f004b08ffc0c2c3a2f30a1c0b72924ac233a7ce", "checkout_state": "modified", - "implementation_digest": "b7ec3ba3b8c71fb844f16d716a18770e6f76ac8261d925f4aec7c9a4a9027529", + "implementation_digest": "a096375193ad36c8ca5be5828d7a2ffaed4cedcceea72c5bc841d47e0e54b607", "profile": "python-reference-source/v2" }, "versions": { diff --git a/docs/research/specification-coverage/analysis-v32.json b/docs/research/specification-coverage/analysis-v32.json index 79e1d1e73..4dd11b6f2 100644 --- a/docs/research/specification-coverage/analysis-v32.json +++ b/docs/research/specification-coverage/analysis-v32.json @@ -90,5 +90,5 @@ } ], "snapshot_id": "raes-standardized-specification-coverage-issue-1015-v32", - "snapshot_sha256": "c4ab0008aa416102355511473365bc2aa30b54e092103d4151efb20d724830da" + "snapshot_sha256": "c448f7d6f02235c3c0c420c3f652b36f73e85790dc2b146b4a0885b40952d67a" } diff --git a/docs/research/specification-coverage/bundles/raes-standardized-specification-coverage-issue-1015-v32.json b/docs/research/specification-coverage/bundles/raes-standardized-specification-coverage-issue-1015-v32.json index 1b7f40592..a4300609f 100644 --- a/docs/research/specification-coverage/bundles/raes-standardized-specification-coverage-issue-1015-v32.json +++ b/docs/research/specification-coverage/bundles/raes-standardized-specification-coverage-issue-1015-v32.json @@ -1,10 +1,10 @@ { "analysis_path": "docs/research/specification-coverage/analysis-v32.json", - "analysis_sha256": "202c720c9cb9ca5c634172632f4d78fc6649917a10fa09517206c4329a72d284", + "analysis_sha256": "76bbf22ac511e8acae14a1259f98a420efbb6030abe6203e155754313f41cd1d", "bundle_id": "raes-standardized-specification-coverage", "protocol_path": "docs/research/specification-coverage/protocol-v1.json", "protocol_sha256": "e97a19e643e94c9e589dca823a63c6ce49d3329fe2a3cb888ab630838ed93125", "revision": "32.0.0", "snapshot_path": "docs/research/specification-coverage/execution-snapshot-v32.json", - "snapshot_sha256": "23b7fd79db04f08de5a5bf82ad393f52f26aca36f41a7e1273a3392223b4d308" + "snapshot_sha256": "7eb1ecd8c2d29155d8cce76e77b9d44ce9ad73becb44c0676ae43cdb536fa157" } diff --git a/docs/research/specification-coverage/execution-snapshot-v32.json b/docs/research/specification-coverage/execution-snapshot-v32.json index cb1464a48..d685556f4 100644 --- a/docs/research/specification-coverage/execution-snapshot-v32.json +++ b/docs/research/specification-coverage/execution-snapshot-v32.json @@ -688,7 +688,7 @@ "source_state": { "base_revision": "9f004b08ffc0c2c3a2f30a1c0b72924ac233a7ce", "checkout_state": "modified", - "implementation_digest": "b7ec3ba3b8c71fb844f16d716a18770e6f76ac8261d925f4aec7c9a4a9027529", + "implementation_digest": "a096375193ad36c8ca5be5828d7a2ffaed4cedcceea72c5bc841d47e0e54b607", "profile": "python-reference-source/v2" } } diff --git a/implementations/python/packages/raes_runtime/control_plane_api/_operation_routes.py b/implementations/python/packages/raes_runtime/control_plane_api/_operation_routes.py index db219abc0..189794dd6 100644 --- a/implementations/python/packages/raes_runtime/control_plane_api/_operation_routes.py +++ b/implementations/python/packages/raes_runtime/control_plane_api/_operation_routes.py @@ -2,6 +2,8 @@ from __future__ import annotations +import logging + from fastapi import FastAPI, HTTPException, Request, Response from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse @@ -33,10 +35,11 @@ _NOT_FOUND_RESPONSES, _conflict_detail, _receipt_response, - _record_operation_receipt_audit, _set_snapshot_revision_header, ) +_LOGGER = logging.getLogger(__name__) + class _IndeterminateResolutionRequest(BaseModel): model_config = ConfigDict(extra="forbid") @@ -44,6 +47,36 @@ class _IndeterminateResolutionRequest(BaseModel): disposition: IndeterminateResolutionDisposition +async def _record_admission_denial_best_effort( + request: Request, + control_plane: RuntimeControlPlane, + *, + action: str, + reason: str, +) -> None: + """Record a redacted admission-denial audit without letting its failure replace the response. + + A failed secondary audit must never escape and replace the already-selected + stable 4xx/5xx envelope (ADR-104 §7; issue-1188 preflight: the request-size + middleware and global exception handlers need the same best-effort audit + rule). Any audit or offload failure is swallowed after a stable log label, + and ``reason`` is always a stable code — never exception text or a provider + class name. + """ + + try: + await _control_plane_calls(request).run( + control_plane.record_audit, + action=action, + identity="anonymous", + allowed=False, + target=str(request.url.path), + reason=reason, + ) + except Exception: + _LOGGER.error("control-plane redacted-error audit persistence failed") + + def _install_request_guards( app: FastAPI, control_plane: RuntimeControlPlane, @@ -58,26 +91,17 @@ def _install_request_guards( @app.exception_handler(Exception) async def _redacted_errors(request: Request, exc: Exception) -> JSONResponse: - await _control_plane_calls(request).run( - control_plane.record_audit, - action=request.method, - identity="anonymous", - allowed=False, - target=str(request.url.path), - reason=f"internal-error:{type(exc).__name__}", + del exc + await _record_admission_denial_best_effort( + request, control_plane, action=request.method, reason="internal-error" ) return JSONResponse(status_code=500, content={"detail": "internal server error"}) @app.exception_handler(RequestValidationError) async def _redacted_request_validation_errors(request: Request, exc: RequestValidationError) -> JSONResponse: del exc - await _control_plane_calls(request).run( - control_plane.record_audit, - action=request.method, - identity="anonymous", - allowed=False, - target=str(request.url.path), - reason="request-validation-failed", + await _record_admission_denial_best_effort( + request, control_plane, action=request.method, reason="request-validation-failed" ) return JSONResponse(status_code=422, content={"detail": "request validation failed"}) @@ -163,14 +187,6 @@ async def submit_provisioning( ) except ValueError as exc: raise HTTPException(status_code=409, detail=_conflict_detail(exc)) from exc - _record_operation_receipt_audit( - calls, - control_plane, - action="submit_provisioning", - identity=identity.identity, - target=str(request.url.path), - receipt=receipt, - ) return _receipt_response(receipt) @@ -208,14 +224,6 @@ async def submit_orchestration( ) except ValueError as exc: raise HTTPException(status_code=409, detail=_conflict_detail(exc)) from exc - _record_operation_receipt_audit( - calls, - control_plane, - action="submit_orchestration", - identity=identity.identity, - target=str(request.url.path), - receipt=receipt, - ) return _receipt_response(receipt) @@ -253,14 +261,6 @@ async def submit_evaluation( ) except ValueError as exc: raise HTTPException(status_code=409, detail=_conflict_detail(exc)) from exc - _record_operation_receipt_audit( - calls, - control_plane, - action="submit_evaluation", - identity=identity.identity, - target=str(request.url.path), - receipt=receipt, - ) return _receipt_response(receipt) diff --git a/implementations/python/packages/raes_runtime/control_plane_api/_participant_routes.py b/implementations/python/packages/raes_runtime/control_plane_api/_participant_routes.py index e644361fd..fe61c1ab3 100644 --- a/implementations/python/packages/raes_runtime/control_plane_api/_participant_routes.py +++ b/implementations/python/packages/raes_runtime/control_plane_api/_participant_routes.py @@ -27,7 +27,6 @@ _NOT_FOUND_RESPONSES, _conflict_detail, _receipt_response, - _record_operation_receipt_audit, _set_snapshot_revision_header, ) @@ -62,14 +61,6 @@ async def control_participant_execution( ) except ValueError as exc: raise HTTPException(status_code=409, detail=_conflict_detail(exc)) from exc - _record_operation_receipt_audit( - calls, - control_plane, - action=f"participant_execution_{body.action}", - identity=identity.identity, - target=str(request.url.path), - receipt=receipt, - ) return _receipt_response(receipt) @app.get( @@ -89,7 +80,7 @@ async def get_participant_execution_state( lambda: control_plane.participant_execution_state(execution_scope_ref), ) except ValueError as exc: - raise HTTPException(status_code=404, detail=str(exc)) from exc + raise HTTPException(status_code=404, detail="participant execution not found") from exc await calls.run( control_plane.record_audit, action="get_participant_execution_state", @@ -176,14 +167,6 @@ async def initialize_participant_episode( ) except ValueError as exc: raise HTTPException(status_code=409, detail=_conflict_detail(exc)) from exc - _record_operation_receipt_audit( - calls, - control_plane, - action="initialize_participant_episode", - identity=identity.identity, - target=str(request.url.path), - receipt=receipt, - ) return _receipt_response(receipt) @app.post( @@ -209,14 +192,6 @@ async def reset_participant_episode( ) except ValueError as exc: raise HTTPException(status_code=409, detail=_conflict_detail(exc)) from exc - _record_operation_receipt_audit( - calls, - control_plane, - action="reset_participant_episode", - identity=identity.identity, - target=str(request.url.path), - receipt=receipt, - ) return _receipt_response(receipt) @@ -247,14 +222,6 @@ async def restart_participant_episode( ) except ValueError as exc: raise HTTPException(status_code=409, detail=_conflict_detail(exc)) from exc - _record_operation_receipt_audit( - calls, - control_plane, - action="restart_participant_episode", - identity=identity.identity, - target=str(request.url.path), - receipt=receipt, - ) return _receipt_response(receipt) @app.post( @@ -272,7 +239,7 @@ async def terminate_participant_episode( try: terminal_reason = ParticipantEpisodeTerminalReason(payload.terminal_reason) except ValueError as exc: - raise HTTPException(status_code=400, detail=f"invalid terminal_reason: {exc}") from exc + raise HTTPException(status_code=400, detail="invalid terminal_reason") from exc try: receipt = await calls.mutate( control_plane.terminate_participant_episode, @@ -284,12 +251,4 @@ async def terminate_participant_episode( ) except ValueError as exc: raise HTTPException(status_code=409, detail=_conflict_detail(exc)) from exc - _record_operation_receipt_audit( - calls, - control_plane, - action="terminate_participant_episode", - identity=identity.identity, - target=str(request.url.path), - receipt=receipt, - ) return _receipt_response(receipt) diff --git a/implementations/python/packages/raes_runtime/control_plane_api/_responses.py b/implementations/python/packages/raes_runtime/control_plane_api/_responses.py index 869b09012..3b715c0c5 100644 --- a/implementations/python/packages/raes_runtime/control_plane_api/_responses.py +++ b/implementations/python/packages/raes_runtime/control_plane_api/_responses.py @@ -2,21 +2,16 @@ from __future__ import annotations -from typing import TYPE_CHECKING - from fastapi import Response from raes_contracts.contracts import OperationReceiptModel from raes_contracts.diagnostics import portable_diagnostic_payload from raes_contracts.runtime_state import OperationReceipt -from ..control_plane_store import IDEMPOTENCY_CLAIM_CONFLICT - -if TYPE_CHECKING: - from ..control_plane import RuntimeControlPlane - from ._offload import _ControlPlaneCallExecutor +from ..control_plane_store import SnapshotRevisionConflict _CONFLICT_RESPONSES = {409: {"description": "Conflict"}} _CONFLICT_DETAIL = "operation conflict" +_SNAPSHOT_REVISION_CONFLICT_DETAIL = "snapshot revision conflict" _NOT_FOUND_RESPONSES = {404: {"description": "Not found"}} _BAD_REQUEST_CONFLICT_RESPONSES = { 400: {"description": "Bad request"}, @@ -30,9 +25,17 @@ def _set_snapshot_revision_header(response: Response, revision: int) -> None: def _conflict_detail(error: ValueError) -> str: - """Redact the authoritative claim conflict while retaining bounded validation errors.""" + """Map a core conflict to a stable, redacted detail without echoing exception text. + + P2 provider, store, and validation failures must never surface a raw + exception string (ADR-104 §7; FM3 invariant 10). ``SnapshotRevisionConflict`` + keeps its stable public label so a stale-write conflict stays diagnosable; + every other conflict collapses to the coarse ``operation conflict`` envelope. + """ - return _CONFLICT_DETAIL if str(error) == IDEMPOTENCY_CLAIM_CONFLICT else str(error) + if isinstance(error, SnapshotRevisionConflict): + return _SNAPSHOT_REVISION_CONFLICT_DETAIL + return _CONFLICT_DETAIL def _receipt_response(receipt: OperationReceipt) -> OperationReceiptModel: @@ -47,17 +50,3 @@ def _receipt_response(receipt: OperationReceipt) -> OperationReceiptModel: "diagnostics": [portable_diagnostic_payload(diag) for diag in receipt.diagnostics], } ) - - -def _record_operation_receipt_audit( - calls: _ControlPlaneCallExecutor, - control_plane: RuntimeControlPlane, - *, - action: str, - identity: str, - target: str, - receipt: OperationReceipt, -) -> None: - """Retain the route seam; core persistence owns operation audit.""" - - del calls, control_plane, action, identity, target, receipt diff --git a/implementations/python/packages/raes_runtime/control_plane_api/_workflow_routes.py b/implementations/python/packages/raes_runtime/control_plane_api/_workflow_routes.py index d1e5d35af..55bc4e831 100644 --- a/implementations/python/packages/raes_runtime/control_plane_api/_workflow_routes.py +++ b/implementations/python/packages/raes_runtime/control_plane_api/_workflow_routes.py @@ -8,7 +8,7 @@ from ..control_plane import RuntimeControlPlane from ._auth import _MutatingIdentity from ._offload import _control_plane_calls -from ._responses import _CONFLICT_RESPONSES, _conflict_detail, _receipt_response, _record_operation_receipt_audit +from ._responses import _CONFLICT_RESPONSES, _conflict_detail, _receipt_response def _register_workflow_routes( @@ -35,14 +35,6 @@ async def cancel_workflow( ) except ValueError as exc: raise HTTPException(status_code=409, detail=_conflict_detail(exc)) from exc - _record_operation_receipt_audit( - calls, - control_plane, - action="cancel_workflow", - identity=identity.identity, - target=str(request.url.path), - receipt=receipt, - ) return _receipt_response(receipt) @app.post("/workflows/reconcile-timeouts", responses=_CONFLICT_RESPONSES) @@ -59,12 +51,4 @@ async def reconcile_timeouts( ) except ValueError as exc: raise HTTPException(status_code=409, detail=_conflict_detail(exc)) from exc - _record_operation_receipt_audit( - calls, - control_plane, - action="reconcile_workflow_timeouts", - identity=identity.identity, - target=str(request.url.path), - receipt=receipt, - ) return _receipt_response(receipt) diff --git a/implementations/python/packages/raes_runtime/control_plane_api_guards.py b/implementations/python/packages/raes_runtime/control_plane_api_guards.py index c1a14022b..4ff0b3c6a 100644 --- a/implementations/python/packages/raes_runtime/control_plane_api_guards.py +++ b/implementations/python/packages/raes_runtime/control_plane_api_guards.py @@ -158,8 +158,10 @@ async def _reject( ) except Exception: # Admission already failed closed. An unavailable audit store must - # neither dispatch the body nor replace the stable rejection. - _LOGGER.exception("control-plane rejection audit persistence failed") + # neither dispatch the body nor replace the stable rejection. Log only a + # stable, bounded label: a traceback or exception chain here could carry + # provider/store internals (ADR-104 §7; issue-1188 preflight). + _LOGGER.error("control-plane rejection audit persistence failed") response = JSONResponse(status_code=status_code, content={"detail": detail}) await response(scope, receive, send) diff --git a/implementations/python/packages/raes_runtime/control_plane_api_participant_retrieval.py b/implementations/python/packages/raes_runtime/control_plane_api_participant_retrieval.py index aa9600225..871b75c30 100644 --- a/implementations/python/packages/raes_runtime/control_plane_api_participant_retrieval.py +++ b/implementations/python/packages/raes_runtime/control_plane_api_participant_retrieval.py @@ -74,14 +74,31 @@ async def _resolved_governed_view( audience_binding = _require_governed_audience_candidate(control_plane, identity, participant_address) calls = _control_plane_calls(request) - projection = await calls.mutate( - _governed_view, - lambda: control_plane._project_snapshot_read( - lambda: resolution.resolve(audience_binding, request.headers.get("idempotency-key", "")), - mutation_kind=OperationKind.PARTICIPANT_CROSSING, - ), - ) - view, revision = projection + idempotency_key = request.headers.get("idempotency-key", "") + if audience_binding is None: + # No crossing-policy resolver governs this participant: the projection + # records no crossing evidence, so it is a side-effect-free read. Serve it + # on the non-contending run() path from one authoritative state cut so it + # never waits on backend mutation latency (ADR-104 §2 P2; issue-1188 + # preflight "only the legacy/pure projection path may bypass mutation + # admission"). + view, revision = await calls.run( + _governed_view, + lambda: control_plane._project_snapshot_read( + lambda: resolution.resolve(audience_binding, idempotency_key), + ), + ) + else: + # Governed egress must commit its RUN-319 crossing occurrence before the + # view is disclosed, so it stays a read-shaped mutation on the one + # mutation authority with revision/history-head checks. + view, revision = await calls.mutate( + _governed_view, + lambda: control_plane._project_snapshot_read( + lambda: resolution.resolve(audience_binding, idempotency_key), + mutation_kind=OperationKind.PARTICIPANT_CROSSING, + ), + ) if view is None: raise HTTPException(status_code=404, detail=resolution.not_found_detail) await calls.run( diff --git a/implementations/python/tests/test_runtime_control_plane_api.py b/implementations/python/tests/test_runtime_control_plane_api.py index f08544b08..1bf364f29 100644 --- a/implementations/python/tests/test_runtime_control_plane_api.py +++ b/implementations/python/tests/test_runtime_control_plane_api.py @@ -569,7 +569,127 @@ def test_control_plane_api_redacts_unexpected_route_errors(monkeypatch: pytest.M assert response.status_code == 500 assert response.json() == {"detail": "internal server error"} assert "SECRET-BACKEND-DETAIL" not in response.text - assert audit_reason == "internal-error:RuntimeError" + # Stable audit reason: no exception class name (issue-1188 preflight). + assert audit_reason == "internal-error" + assert "RuntimeError" not in audit_reason + + +def test_control_plane_api_internal_error_survives_audit_failure(monkeypatch: pytest.MonkeyPatch) -> None: + """A failed secondary audit must not replace the stable 500 envelope (core-F1).""" + + target = create_stub_target() + control_plane = RuntimeControlPlane(target) + app = create_control_plane_app(control_plane, security=_test_security(target.name)) + + def failing_audit(*_args: object, **_kwargs: object) -> None: + raise RuntimeError("audit store unavailable /var/secret") + + monkeypatch.setattr(control_plane, "get_snapshot", lambda: (_ for _ in ()).throw(RuntimeError("boom"))) + monkeypatch.setattr(control_plane, "record_audit", failing_audit) + + with TestClient(app, raise_server_exceptions=False) as client: + response = client.get("/snapshot", headers={"authorization": "Bearer test-auditor-token"}) + + assert response.status_code == 500 + assert response.json() == {"detail": "internal server error"} + assert "audit store unavailable" not in response.text + assert "/var/secret" not in response.text + + +def test_control_plane_api_request_validation_error_survives_audit_failure( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A failed secondary audit must not replace the stable 422 envelope (core-F1).""" + + target = create_stub_target() + control_plane = RuntimeControlPlane(target) + app = create_control_plane_app(control_plane, security=_test_security(target.name)) + + def failing_audit(*_args: object, **_kwargs: object) -> None: + raise RuntimeError("audit store unavailable /var/secret") + + monkeypatch.setattr(control_plane, "record_audit", failing_audit) + headers = { + "x-raes-client-verified": "true", + "x-raes-client-identity": "backend-service", + } + + with TestClient(app, raise_server_exceptions=False) as client: + response = client.post("/operations/provisioning", json={"operations": "not-a-list"}, headers=headers) + + assert response.status_code == 422 + assert response.json() == {"detail": "request validation failed"} + assert "audit store unavailable" not in response.text + assert "/var/secret" not in response.text + + +@pytest.mark.parametrize( + ("method_name", "path", "payload"), + ( + ( + "submit_provisioning", + "/operations/provisioning", + {"operations": [], "diagnostics": [], "realization_authority": []}, + ), + ( + "submit_orchestration", + "/operations/orchestration", + {"operations": [], "startup_order": [], "diagnostics": []}, + ), + ("submit_evaluation", "/operations/evaluation", {"operations": [], "diagnostics": []}), + ), +) +def test_control_plane_api_redacts_core_conflict_exception_text( + monkeypatch: pytest.MonkeyPatch, + method_name: str, + path: str, + payload: dict[str, object], +) -> None: + target = create_stub_target() + control_plane = RuntimeControlPlane(target) + app = create_control_plane_app(control_plane, security=_test_security(target.name)) + secret = "token=abcd /var/secrets/store.db SELECT * FROM operations WHERE actor='alice'" + + def leaking(*_args: object, **_kwargs: object) -> None: + raise ValueError(secret) + + monkeypatch.setattr(control_plane, method_name, leaking) + headers = { + "x-raes-client-verified": "true", + "x-raes-client-identity": "backend-service", + } + + with TestClient(app) as client: + response = client.post(path, json=payload, headers=headers) + + assert response.status_code == 409 + assert response.json() == {"detail": "operation conflict"} + assert "token=abcd" not in response.text + assert "SELECT" not in response.text + assert "/var/secrets" not in response.text + + +def test_control_plane_api_redacts_participant_execution_state_error_text( + monkeypatch: pytest.MonkeyPatch, +) -> None: + target = create_stub_target() + control_plane = RuntimeControlPlane(target) + app = create_control_plane_app(control_plane, security=_test_security(target.name)) + + def leaking(*_args: object, **_kwargs: object) -> None: + raise ValueError("/secret/exec/path backend disclosure detail") + + monkeypatch.setattr(control_plane, "participant_execution_state", leaking) + + with TestClient(app) as client: + response = client.get( + "/participant-executions/exec-scope-1", + headers={"authorization": "Bearer test-operator-token"}, + ) + + assert response.status_code == 404 + assert "/secret/exec/path" not in response.text + assert "backend disclosure detail" not in response.text def test_control_plane_api_accepts_orchestration_plan_and_exposes_snapshot(): @@ -877,6 +997,88 @@ def test_control_plane_api_supports_idempotent_retries(): assert len(operation_audits) == 1 +def test_control_plane_api_scopes_idempotency_claims_per_principal() -> None: + """A shared idempotency key never returns another principal's receipt. + + Claims are scoped by (store, actor, kind, key) (ADR-104 §7; FM3 invariant 8), + so two principals presenting the same key mint distinct operations and neither + can read the other's (guessing an operation id grants no authority). + """ + + target = create_stub_target() + control_plane = RuntimeControlPlane(target) + app = create_control_plane_app(control_plane, security=_test_security(target.name)) + payload = {"operations": [], "diagnostics": [], "realization_authority": []} + backend_headers = { + "x-raes-client-verified": "true", + "x-raes-client-identity": "backend-service", + "idempotency-key": "shared-key", + } + operator_headers = { + "authorization": "Bearer test-operator-token", + "idempotency-key": "shared-key", + } + + with TestClient(app) as client: + backend = client.post("/operations/provisioning", json=payload, headers=backend_headers) + operator = client.post("/operations/provisioning", json=payload, headers=operator_headers) + cross_read = client.get( + f"/operations/{backend.json()['operation_id']}", + headers=operator_headers, + ) + + assert backend.status_code == 200 + assert operator.status_code == 200 + assert backend.json()["operation_id"] != operator.json()["operation_id"] + assert backend.json()["context"]["actor_id"] == "backend-service" + assert operator.json()["context"]["actor_id"] == "operator" + assert cross_read.status_code == 404 + + +def test_control_plane_api_commits_one_actor_bound_terminal_audit() -> None: + """A successful mutation commits exactly one actor-bound terminal audit. + + The core transaction owns terminal audit (ADR-104 §4; FM3 invariant 4); the + route appends none. Exactly one operation-scoped audit exists and it carries + the authenticated actor. + """ + + target = create_stub_target() + control_plane = RuntimeControlPlane(target) + app = create_control_plane_app(control_plane, security=_test_security(target.name)) + headers = { + "x-raes-client-verified": "true", + "x-raes-client-identity": "backend-service", + } + + with TestClient(app) as client: + response = client.post( + "/operations/provisioning", + json={"operations": [], "diagnostics": [], "realization_authority": []}, + headers=headers, + ) + operation_id = response.json()["operation_id"] + operation_audits = [event for event in control_plane.audit_log() if event.operation_id == operation_id] + + assert response.status_code == 200 + assert response.json()["accepted"] is True + assert len(operation_audits) == 1 + assert operation_audits[0].identity == "backend-service" + assert operation_audits[0].allowed is True + + +def test_create_control_plane_app_rejects_multi_worker_configuration( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """P2 is one owning application worker; multi-worker service posture fails closed.""" + + monkeypatch.setenv("WEB_CONCURRENCY", "2") + target = create_stub_target() + + with pytest.raises(RuntimeError, match="unsupported for a local control-plane store"): + create_control_plane_app(RuntimeControlPlane(target), security=_test_security(target.name)) + + def test_slow_backend_submission_does_not_block_unrelated_http_reads( monkeypatch: pytest.MonkeyPatch, ) -> None: @@ -941,6 +1143,140 @@ async def exercise() -> None: _run(exercise()) +def test_participant_status_projection_does_not_contend_with_mutations( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A side-effect-free API-408 projection must not wait on backend mutation latency. + + With no crossing-policy resolver configured the status view records no + crossing evidence, so it is a pure read: it runs on the non-contending + ``run`` path and carries its observed snapshot revision even while a slow + mutation owns the mutation authority (ADR-104 §2 P2; issue-1188 preflight). + """ + + target = create_stub_target() + control_plane = RuntimeControlPlane(target) + # One mutation slot: while a mutation holds it, the mutate() path fails closed + # with 503, so a pure projection surviving proves it is on the run() path. + app = create_control_plane_app(control_plane, security=_test_security(target.name, max_pending_mutations=1)) + mutate_headers = { + "x-raes-client-verified": "true", + "x-raes-client-identity": "backend-service", + } + read_headers = {"authorization": "Bearer test-operator-token"} + entered = Event() + release = Event() + real_submit = control_plane.submit_provisioning + + def blocking_submit( + submitted_plan: ProvisioningPlan, + *, + base_snapshot: RuntimeSnapshot | None = None, + idempotency_key: str = "", + request_fingerprint: str = "", + identity: object | None = None, + ) -> OperationReceipt: + entered.set() + if not release.wait(timeout=5): + raise TimeoutError("test backend was not released") + return real_submit( + submitted_plan, + base_snapshot=base_snapshot, + idempotency_key=idempotency_key, + request_fingerprint=request_fingerprint, + identity=identity, + ) + + async def exercise() -> None: + transport = httpx.ASGITransport(app=app) + async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client: + initialized = await client.post( + "/participants/participant.alice/episodes/initialize", + json={}, + headers=mutate_headers, + ) + assert initialized.status_code == 200 + monkeypatch.setattr(control_plane, "submit_provisioning", blocking_submit) + submission = asyncio.create_task( + client.post( + "/operations/provisioning", + json={"operations": [], "diagnostics": [], "realization_authority": []}, + headers=mutate_headers, + ) + ) + try: + assert await asyncio.to_thread(entered.wait, 2) + assert not submission.done() + status = await asyncio.wait_for( + client.get("/participants/participant.alice/status", headers=read_headers), + timeout=1, + ) + assert status.status_code == 200 + assert status.headers["X-RAES-Snapshot-Revision"] + finally: + release.set() + response = await asyncio.wait_for(submission, timeout=2) + assert response.status_code == 200 + + _run(exercise()) + + +def test_governed_participant_status_view_commits_crossing_before_disclosure() -> None: + """A governed API-408 egress stays a read-shaped mutation. + + With a crossing-policy resolver configured, the status view records a + RUN-319 crossing occurrence before disclosure (the mutation/evidence path a + pure ``run`` read would never take) and an audience-unbound reader is + refused. This is CP-8's "governed egress remains a separately asserted + mutation contract" (issue-1188 preflight). + """ + + from participant_crossing_fixtures import ( + PARTICIPANT, + StaticCrossingResolver, + action_plane, + evidence, + policy_capable_target, + ) + from participant_crossing_fixtures import identity as _governed_identity + + class _ViewEvidenceResolver(StaticCrossingResolver): + """A resolver that supplies trusted egress evidence to the HTTP adapter.""" + + def resolve_participant_view_evidence(self, **_kwargs: object): + return evidence() + + target = policy_capable_target("participant_egress_projection", "participant_transformation") + control_plane = action_plane(_ViewEvidenceResolver(), target=target) + security = ControlPlaneSecurityConfig( + trust_proxy_identity_headers=False, + bearer_tokens={ + "audience-token": _governed_identity(audience_bound=True), + "unbound-token": _governed_identity(audience_bound=False), + }, + ) + app = create_control_plane_app(control_plane, security=security) + + with TestClient(app) as client: + before = len(control_plane.snapshot.participant_crossing_history.get(PARTICIPANT, ())) + governed = client.get( + f"/participants/{PARTICIPANT}/status", + headers={"authorization": "Bearer audience-token"}, + ) + after = len(control_plane.snapshot.participant_crossing_history.get(PARTICIPANT, ())) + forbidden = client.get( + f"/participants/{PARTICIPANT}/status", + headers={"authorization": "Bearer unbound-token"}, + ) + + assert governed.status_code == 200 + assert governed.headers["X-RAES-Snapshot-Revision"] + # A pure read records no crossing history; growth proves the governed egress + # committed its RUN-319 evidence through the mutation path before disclosure. + assert after > before + assert forbidden.status_code == 403 + + def test_control_plane_rejects_mutation_queue_overload( monkeypatch: pytest.MonkeyPatch, ) -> None: @@ -2159,7 +2495,9 @@ def test_terminate_route_rejects_invalid_terminal_reason(self): ) assert response.status_code == 400 - assert "invalid terminal_reason" in response.json()["detail"] + assert response.json()["detail"] == "invalid terminal_reason" + assert "exploded" not in response.text + assert "ParticipantEpisodeTerminalReason" not in response.text def test_restart_route_resumes_after_termination(self): client = self._build_client() From 6c93367e8308a8525320aa66f65581df08e646b0 Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Sat, 19 Sep 2026 04:35:21 +0200 Subject: [PATCH 2/3] fix(runtime): reconcile served-profile evidence bundle digests --- .../formal-semantic-validation/bundles/retest-v32.json | 2 +- .../formal-semantic-validation/execution-snapshot-v32.json | 2 +- docs/research/specification-coverage/analysis-v32.json | 2 +- ...es-standardized-specification-coverage-issue-1015-v32.json | 4 ++-- .../specification-coverage/execution-snapshot-v32.json | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/research/formal-semantic-validation/bundles/retest-v32.json b/docs/research/formal-semantic-validation/bundles/retest-v32.json index fd909d362..87e61530e 100644 --- a/docs/research/formal-semantic-validation/bundles/retest-v32.json +++ b/docs/research/formal-semantic-validation/bundles/retest-v32.json @@ -118,5 +118,5 @@ "protocol_sha256": "abf94093e344bf495dfb04e8b0c5985c0beaab8ebb17a75e15c8674fa81b1a7c", "revision": "33.0.0", "snapshot_path": "docs/research/formal-semantic-validation/execution-snapshot-v32.json", - "snapshot_sha256": "9c9ecae2fa8341cb65a5dc2b6760b49d2ef65ecd5777a5466c55bcca8c0bbd53" + "snapshot_sha256": "39cba98aa01b4fbf67396d165afed45669faab8a6141ecf14bba7e77ccc38d0d" } diff --git a/docs/research/formal-semantic-validation/execution-snapshot-v32.json b/docs/research/formal-semantic-validation/execution-snapshot-v32.json index 3759e1c81..49597cc29 100644 --- a/docs/research/formal-semantic-validation/execution-snapshot-v32.json +++ b/docs/research/formal-semantic-validation/execution-snapshot-v32.json @@ -645,7 +645,7 @@ "source_state": { "base_revision": "9f004b08ffc0c2c3a2f30a1c0b72924ac233a7ce", "checkout_state": "modified", - "implementation_digest": "a096375193ad36c8ca5be5828d7a2ffaed4cedcceea72c5bc841d47e0e54b607", + "implementation_digest": "199358669a22d9943e51961ac472d359dafac936ad7bc02c1d37de9140b9102e", "profile": "python-reference-source/v2" }, "versions": { diff --git a/docs/research/specification-coverage/analysis-v32.json b/docs/research/specification-coverage/analysis-v32.json index 4dd11b6f2..616650101 100644 --- a/docs/research/specification-coverage/analysis-v32.json +++ b/docs/research/specification-coverage/analysis-v32.json @@ -90,5 +90,5 @@ } ], "snapshot_id": "raes-standardized-specification-coverage-issue-1015-v32", - "snapshot_sha256": "c448f7d6f02235c3c0c420c3f652b36f73e85790dc2b146b4a0885b40952d67a" + "snapshot_sha256": "f7fa04f02d903a7b3d75b6a365c923370e36b641a9923a4dc98534bfdcb63a2a" } diff --git a/docs/research/specification-coverage/bundles/raes-standardized-specification-coverage-issue-1015-v32.json b/docs/research/specification-coverage/bundles/raes-standardized-specification-coverage-issue-1015-v32.json index a4300609f..853e500e8 100644 --- a/docs/research/specification-coverage/bundles/raes-standardized-specification-coverage-issue-1015-v32.json +++ b/docs/research/specification-coverage/bundles/raes-standardized-specification-coverage-issue-1015-v32.json @@ -1,10 +1,10 @@ { "analysis_path": "docs/research/specification-coverage/analysis-v32.json", - "analysis_sha256": "76bbf22ac511e8acae14a1259f98a420efbb6030abe6203e155754313f41cd1d", + "analysis_sha256": "19e894fad61a938951d9b9dbcf3680de7c92be018cab36123e949db0145dfc73", "bundle_id": "raes-standardized-specification-coverage", "protocol_path": "docs/research/specification-coverage/protocol-v1.json", "protocol_sha256": "e97a19e643e94c9e589dca823a63c6ce49d3329fe2a3cb888ab630838ed93125", "revision": "32.0.0", "snapshot_path": "docs/research/specification-coverage/execution-snapshot-v32.json", - "snapshot_sha256": "7eb1ecd8c2d29155d8cce76e77b9d44ce9ad73becb44c0676ae43cdb536fa157" + "snapshot_sha256": "1c5c05318d39f9187ba140058d84730677f68aaa54620664dd1a44f84e2d1faf" } diff --git a/docs/research/specification-coverage/execution-snapshot-v32.json b/docs/research/specification-coverage/execution-snapshot-v32.json index d685556f4..a74d4eb6d 100644 --- a/docs/research/specification-coverage/execution-snapshot-v32.json +++ b/docs/research/specification-coverage/execution-snapshot-v32.json @@ -688,7 +688,7 @@ "source_state": { "base_revision": "9f004b08ffc0c2c3a2f30a1c0b72924ac233a7ce", "checkout_state": "modified", - "implementation_digest": "a096375193ad36c8ca5be5828d7a2ffaed4cedcceea72c5bc841d47e0e54b607", + "implementation_digest": "199358669a22d9943e51961ac472d359dafac936ad7bc02c1d37de9140b9102e", "profile": "python-reference-source/v2" } } From 8f8055181ade42de5a1e1d8854ddc06bb81b506b Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Sat, 19 Sep 2026 04:55:50 +0200 Subject: [PATCH 3/3] test(runtime): resolve sonar S5778 in multi-worker admission test --- .../python/tests/test_runtime_control_plane_api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/implementations/python/tests/test_runtime_control_plane_api.py b/implementations/python/tests/test_runtime_control_plane_api.py index 1bf364f29..99e183b41 100644 --- a/implementations/python/tests/test_runtime_control_plane_api.py +++ b/implementations/python/tests/test_runtime_control_plane_api.py @@ -1074,9 +1074,11 @@ def test_create_control_plane_app_rejects_multi_worker_configuration( monkeypatch.setenv("WEB_CONCURRENCY", "2") target = create_stub_target() + control_plane = RuntimeControlPlane(target) + security = _test_security(target.name) with pytest.raises(RuntimeError, match="unsupported for a local control-plane store"): - create_control_plane_app(RuntimeControlPlane(target), security=_test_security(target.name)) + create_control_plane_app(control_plane, security=security) def test_slow_backend_submission_does_not_block_unrelated_http_reads(