diff --git a/.github/workflows/canonical-versioning.yml b/.github/workflows/canonical-versioning.yml new file mode 100644 index 0000000..aa985e1 --- /dev/null +++ b/.github/workflows/canonical-versioning.yml @@ -0,0 +1,21 @@ +name: Canonical versioning + +on: + push: + branches: + - '**' + +permissions: + contents: read + +jobs: + validate-version-source: + name: Validate committed canonical version source + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v5 + - name: Read-only source consistency + run: | + python3 -m pip install --quiet . + python3 tools/qualification/advance_platform_build.py --source-root . --check + python3 tools/qualification/platform_version_consistency.py --source-root . diff --git a/.github/workflows/engineering-platform-validation.yml b/.github/workflows/engineering-platform-validation.yml index a90a578..c998571 100644 --- a/.github/workflows/engineering-platform-validation.yml +++ b/.github/workflows/engineering-platform-validation.yml @@ -63,19 +63,25 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v5 - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7 with: {python-version: '3.12'} - - name: Build and install next canonical Engineering Platform wheel + - name: Build and install committed canonical Engineering Platform wheel run: | python3 -m pip install build coverage - python3 tools/qualification/build_platform_wheel.py --source-root . --install-python "$(command -v python3)" + version="$(python3 -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')" + python3 tools/qualification/build_platform_wheel.py --source-root . --version "$version" --install-python "$(command -v python3)" - name: Import, bytecode and qualification smoke run: | python3 -c "import engineering_platform, engineering_platform.execution_host" python3 -m compileall -q src/engineering_platform PYTHONPATH=src python3 tools/qualification/console_route_ownership_guard.py --source-root src - python3 tools/qualification/platform_version_consistency.py --source-root . + python3 tools/qualification/platform_version_consistency.py --source-root . --installed-python "$(command -v python3)" python3 -m unittest discover -s tests -p 'test_*.py' - name: Qualify installed P-TRANSPORT 3×2 ingress matrix run: python3 tools/qualification/p_transport_installed_ingress_matrix.py --source-root . + - name: Qualify deterministic Genesis and Managed execution E2E + # The E2E script creates an isolated CENTRAL data root and a local bare + # Git origin for Managed. CI therefore exercises both execution modes + # without mutating a GitHub repository. + run: python3 tools/qualification/p_deterministic_execution_e2e.py --source-root . - name: Qualify HTTP JSON API, OpenAPI and Postman contract run: PYTHONPATH=src python3 tools/qualification/http_json_postman_contract.py --source-root . - name: Enforce Engineering Platform coverage contract diff --git a/.github/workflows/ep-server-production-release.yml b/.github/workflows/ep-server-production-release.yml index 4b75793..ce8cd92 100644 --- a/.github/workflows/ep-server-production-release.yml +++ b/.github/workflows/ep-server-production-release.yml @@ -98,9 +98,12 @@ jobs: python3 -m pip install "dist/engineering_platform-${{ needs.release-context.outputs.version }}-py3-none-any.whl" python3 -m compileall -q src/engineering_platform PYTHONPATH=src python3 tools/qualification/console_route_ownership_guard.py --source-root src - python3 tools/qualification/platform_version_consistency.py --source-root . + python3 tools/qualification/platform_version_consistency.py --source-root . --installed-python "$(command -v python3)" python3 -m unittest discover -s tests -p 'test_*.py' python3 tools/qualification/p_transport_installed_ingress_matrix.py --source-root . + # This uses an isolated CENTRAL root and local bare Git origin; the + # release gate never writes to an external GitHub repository. + python3 tools/qualification/p_deterministic_execution_e2e.py --source-root . PYTHONPATH=src python3 tools/qualification/http_json_postman_contract.py --source-root . - name: Enforce production coverage and security gates run: | diff --git a/docs/development/ENGINEERING_PLATFORM_ROADMAP.md b/docs/development/ENGINEERING_PLATFORM_ROADMAP.md index bc734c8..e11989e 100644 --- a/docs/development/ENGINEERING_PLATFORM_ROADMAP.md +++ b/docs/development/ENGINEERING_PLATFORM_ROADMAP.md @@ -279,6 +279,13 @@ one bounded Forge Action still enter through HTTP, receive durable identity, be admitted, mutate its repository, validate/review/repair/finalize, retain terminal evidence, and reconcile after Forge restart? +`EP_RUN_QUALITY_ASSURANCE_V1` is a bounded EP execution-contract increment: +reuse the existing quality lifecycle step for pinned, independent read-only +quality and security assurance, structured findings/readback and one shared +three-round repair budget. It is not a second orchestrator, generalized Agent +topology, or a claim of installed qualification until exact-head delivery and +installed evidence are retained. + | Umbrella gate | Exact first-loop capability | Evidence state at this proposal | First-loop disposition | | --- | --- | --- | --- | | Phase-3 package/install | A clean installed Server/CENTRAL/runtime that can run the canary | Historical dependency authority says incomplete; no current installed-proof claim is made here | `AUTONOMY_CRITICAL` bounded capability; full historical phase needs reconciliation | diff --git a/docs/development/LOCAL_AGENT_RUNNER.md b/docs/development/LOCAL_AGENT_RUNNER.md index 48b9445..eae8ebf 100644 --- a/docs/development/LOCAL_AGENT_RUNNER.md +++ b/docs/development/LOCAL_AGENT_RUNNER.md @@ -40,6 +40,20 @@ is a separate Engineering Platform release. The private dashboard displays them with the corresponding live components, while its status bar displays the Engineering Platform version and Git commit. +`BOOTSTRAP_RELEASE_CADENCE_V2` (`engineering-platform-bootstrap-release-cadence-v2`) +makes CI read-only. One bounded engineering increment defaults to `PATCH`; +documentation-only work is explicit `NO_BUMP`; `MINOR` is an explicit +capability/release boundary; and `MAJOR`/`EXACT` require applicable authority. +Version preparation happens before final qualification through the protected +delivery seam. Repair, requalification and main merge reuse that operation and +never create a secondary allocation. Existing V1 receipts remain historical. + +The event policy is shared with Forge and Workspace and is canonically defined +by Forge Platform in [Canonical product versioning](https://github.com/pcvantol/forge-platform/blob/main/docs/architecture/CANONICAL_PRODUCT_VERSIONING.md). +Engineering Platform retains its richer package/manifest projection and its +own release-publishing authority; the shared policy grants neither publication +nor deployment authority. + At runner startup, `engineering-execution-host` reads the manifest and rejects an unsupported platform major version, older runner, older Bootstrap Contract, unsupported checkpoint/memory/report format or unsupported Codex CLI. Diagnostics state the diff --git a/docs/engineering/EP_PRODUCER_READBACK_CONTRACT.md b/docs/engineering/EP_PRODUCER_READBACK_CONTRACT.md index 04866ed..fc1707d 100644 --- a/docs/engineering/EP_PRODUCER_READBACK_CONTRACT.md +++ b/docs/engineering/EP_PRODUCER_READBACK_CONTRACT.md @@ -1,6 +1,6 @@ -# EP producer readback contract v1.1 +# EP producer readback contract v1.2 -`v1.1` is the consumer-visible, authenticated readback contract for a +`v1.2` is the consumer-visible, authenticated readback contract for a canonical EP submission. It belongs to the existing EP Server HTTP JSON API; it is not a second API, consumer database, queue, or execution authority. @@ -20,12 +20,19 @@ storage. ## Response identity and evidence -The JSON response is schema version `1.1` and contains the immutable canonical +The JSON response is schema version `1.2` and contains the immutable canonical submission ID, project/repository IDs, producer provenance, submitted correlation/mission/engineering-action IDs, and a server-computed `accepted_request_digest`. `run` is `null` until CENTRAL has claimed the accepted submission; once present, its run ID and lifecycle state are canonical. +`disposition` is separate from the execution result. It records the current +submission state, monotone revision, worker eligibility and, where an operator +command exists, its operation/event reference, verified actor reference, +reason and timestamp. `DECLINED` is terminal for the submission while keeping +`run: null` and `result.outcome: NOT_STARTED`; EP never fabricates an execution +receipt, commit or terminal artifact for a declined-but-unclaimed submission. + Forge requests place the versioned, exact execution identity under `constraints.forge_execution`: host, repository, correlation, mission and revision, intent and revision, action, runtime-prompt ID/digest, and retry @@ -52,7 +59,31 @@ is present in the checkpoint's verified commit evidence. `VALIDATION_ONLY`, they are not fabricated into successful delivery. The machine-readable response shape is -[`producer-readback-v1.schema.json`](../../src/engineering_platform/schemas/producer-readback-v1.schema.json). +[`producer-readback-v1.2.schema.json`](../../src/engineering_platform/schemas/producer-readback-v1.2.schema.json). + +## Operations Console queue disposition + +The Console-only mutation boundary is separate from the producer API: + +``` +POST /api/queue-disposition?project={project_id} +Authorization: Bearer +``` + +The exact JSON v1.0 request has `contract_version`, a unique `operation_id`, +`submission_id`, `expected_state`, integer `expected_revision`, `disposition` +and a bounded operator `reason`. It rejects duplicate JSON names, unknown or +missing fields, stale state/revision, unknown submissions and invalid state +transitions. Replaying the same operation ID and exact command is idempotent; +using that ID for a different command is a conflict. + +Authentication and queue authority are deliberately distinct. A scoped +producer credential receives `403 OPERATOR_CAPABILITY_REQUIRED`; no credential +receives `401 UNAUTHENTICATED`. `QUEUE_HOLD_RESUME` permits defer, quarantine +and resume. `QUEUE_DECLINE` permits terminal decline, including the direct +`QUARANTINED -> DECLINED` path. The worker claim and this command arbitrate in +one CENTRAL transaction, so a claimed submission returns `409` and is never +cancelled by a queue action. ## Forge consumer mapping fixture diff --git a/docs/engineering/EXECUTION_HOST_ARCHITECTURE.md b/docs/engineering/EXECUTION_HOST_ARCHITECTURE.md index 2a8d646..68a1d7a 100644 --- a/docs/engineering/EXECUTION_HOST_ARCHITECTURE.md +++ b/docs/engineering/EXECUTION_HOST_ARCHITECTURE.md @@ -28,10 +28,17 @@ Genesis target. A Genesis run only evaluates its target profile; a Managed run only evaluates its repository profile. JSON status files are projections, not an ownership or lifecycle authority. -Lifecycle phase identifiers are compatibility contracts. Their presentation is -mode-aware: the shared `REPAIR_AGENT` phase is projected as pull-request check -repair for Managed work and autonomous quality repair for Genesis. This is a -display-only distinction; no checkpoint or transaction state is translated. +Lifecycle phase identifiers are compatibility contracts. `QUALITY_CONTROL_AGENT` +is the shared post-implementation assurance boundary: it invokes independent +read-only Quality and Security reviewers on one pinned candidate. It cannot +write the repository, create a PR, or approve an implementer's work. Findings +are immutable, versioned checkpoint evidence; a missing, malformed, or +candidate-mismatched required review is `UNRESOLVED`, never a pass. Only the +shared `REPAIR_AGENT` role may correct accepted blockers. Its run-wide, +persistent budget is three rounds total, spanning local validation, assurance, +hosted checks and finalization; a new SHA, phase, resume or PR does not reset +it. The Console projects the same stored review identities and repair rounds +for live and historical runs. The immutable profile lists repository, remote, upstream, clean-worktree, branch, workspace authorization, host and capability qualification, providers, @@ -60,7 +67,37 @@ previous lease history. EP serializes mutating work at the repository/execution-scope boundary: no more than one mutating execution may own a scope at once. FIFO is the default queue ordering within that scope, but admission and selection remain policy-driven; -FIFO is not a second planning authority. The active mutation lease starts with +FIFO is not a second planning authority. + +## CENTRAL queue operator handling + +CENTRAL owns the durable queue record after admission; Forge remains the +authority for the originating Action intent. Consequently, an Operations +Console operator must never delete a queued submission or silently make it +disappear. Queue handling is an explicit, per-submission, reasoned state +transition with an append-only audit event and producer-visible readback. + +The state machine is `QUEUED -> DEFERRED|QUARANTINED|DECLINED`, +`DEFERRED -> QUEUED`, and `QUARANTINED -> QUEUED|DECLINED`. `DECLINED` has no +outgoing transition. Direct `QUARANTINED -> DECLINED` never creates an +intermediate `QUEUED` state or a worker-eligible window. A declined submission +is terminal but retained with its correlation and reason. A lifecycle worker +selects only admitted `QUEUED` submissions; queue disposition never cancels a +claimed run. + +Forge observes these dispositions through the canonical producer-readback +contract and reconciles its own Action state. EP does not invoke Forge +internals, mutate Forge storage, or infer cancellation. Until a versioned +Forge callback contract exists, readback is the required reconciliation path. +Queue commands carry a versioned operation ID, expected state and monotone +revision, and are arbitrated with the worker claim in CENTRAL. Origin checking +is CSRF protection only, never operator authentication. The authenticated +consumer must additionally hold an active, project-scoped capability: +`QUEUE_HOLD_RESUME` for defer/quarantine/resume and `QUEUE_DECLINE` for the +terminal decline transition. Consumer credentials do not imply either grant. +The installation owner grants or revokes this narrow capability with the +Server CLI; it is not a producer-facing or dashboard-managed role system. +The active mutation lease starts with the accepted execution and is retained through provider work, validation, delivery, finalization and reconciliation. It is released only after terminal or governed recovery evidence establishes that the scope is safe for later diff --git a/docs/engineering/EXECUTION_HOST_OPERATIONS.md b/docs/engineering/EXECUTION_HOST_OPERATIONS.md index 4d3cf5e..760e8c2 100644 --- a/docs/engineering/EXECUTION_HOST_OPERATIONS.md +++ b/docs/engineering/EXECUTION_HOST_OPERATIONS.md @@ -127,12 +127,13 @@ terminal date, and a repeated recovery cannot add a second run or count. ## Controlled provider-interruption qualification proof For the dedicated, one-shot recovery proof, an operator may arm only an -already-admitted, non-terminal run before it reaches `QUALITY_CONTROL_AGENT`: +already-admitted, non-terminal run before it reaches `EXECUTE_AGENT`: ```sh python3 -m tools.engineering.provider_recovery arm-controlled-interruption \ --repo /Users/pcvantol/Documents/GitHub/djconnect \ - --run-id --phase QUALITY_CONTROL_AGENT + --run-id --phase EXECUTE_AGENT \ + --central-database /epdata.sqlite ``` Inspect the exact control with `controlled-interruption-status` and cancel an @@ -144,6 +145,70 @@ once consumed it cannot be disarmed or fired again. Prompt text and submission provenance cannot create this control, and it never creates a retry submission or new root run. +For qualification, run the control together with the recovery tests rather +than treating an armed marker as a passing result. The required evidence is: +`ARMED` before the boundary, one `CONSUMED` control artifact at +`EXECUTE_AGENT`, one recovery/retry lineage, and one terminal outcome. +The deterministic installed E2E lives in +`tools/qualification/p_deterministic_execution_e2e.py`. It uses an isolated +CENTRAL root and local Git fixture to exercise Genesis, Managed, and a third +controlled-recovery lane. The third lane pauses only its deterministic +qualification adapter after the canonical `INITIALIZE` checkpoint, arms the +real run-bound control through the installed CLI, and verifies `CONSUMED`, one +same-run `RECOVERED` lineage, terminal assurance evidence, and the run in the +dashboard's project-scoped history. The dedicated provider-recovery unit suite +additionally qualifies unsafe and ambiguous recovery branches. + +### Installed qualification runtime composition + +The installed E2E also exercises the real host-preflight runtime checks. Its +temporary CENTRAL root owns a minimal executable under +`managed-codex-cli/bin/codex`; it reports the fixed qualification runtime +version and is written into that root's `server.json` before the Server starts. +This is a composition fixture, not a provider fallback: the deterministic +qualification agent remains the only component that performs the test work. +The launcher exists so `runtime_executable` and `runtime_invocation` are +verified through the same configured EP-managed runtime boundary as a normal +installation. It is removed with the temporary qualification root and never +creates or relies on an account-wide Codex installation. + +Consequently, CI always runs Genesis, Managed and armed recovery with a real +preflight. The separate external GitHub profile below additionally proves the +remote-write adapter and merge boundaries; it is not invoked by ordinary CI. + +### Explicit external Managed GitHub qualification + +The default deterministic qualification never contacts GitHub. To prove the +Managed hand-off against an approved dummy repository, an operator must supply +all three values deliberately: + +```sh +python3 tools/qualification/p_deterministic_execution_e2e.py --source-root . \ + --managed-repository /absolute/path/to/clean-dummy-checkout \ + --managed-github-repository owner/approved-dummy-repository \ + --allow-managed-github-writes +``` + +The command rejects a dirty checkout, a non-matching `origin`, unavailable +GitHub access, or a missing explicit write flag. It commits and pushes an +implementation branch, creates and verifies its PR, then acts as the explicit +operator only for that named disposable fixture: it merges the implementation +PR, verifies the host-created `codex/finalize-` Finalization PR, +merges it, and requires the canonical run to reach `COMPLETE`. It repeats the +same real remote handoff and finalization for the armed recovery lane, proving +the same-run `RECOVERED` lineage rather than merely an armed marker. Remote +branches are deleted by the GitHub merge operation; the fixture repository and +the optional persistent local qualification root are retained for inspection. +This profile is intentionally excluded from CI and release gates. + +## CENTRAL project lanes + +CENTRAL retains a FIFO lane per project. It permits at most one active, +blocked-with-open-resolution, or retry-pending run in that project. The +Lifecycle Worker may dispatch one eligible run from each different project in +parallel, so a Genesis and a Managed run in separate projects may overlap; a +second run in either same project cannot. + ## Local repository validation gate Validation is selected from the actual bounded-branch diff. Documentation and diff --git a/package-lock.json b/package-lock.json index afc5d91..cc1b387 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "engineering-platform-browser-validation", - "version": "2.1.6", + "version": "2.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "engineering-platform-browser-validation", - "version": "2.1.6", + "version": "2.3.0", "devDependencies": { "@playwright/test": "1.62.1" } diff --git a/package.json b/package.json index 9fff394..cb78a80 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "engineering-platform-browser-validation", "private": true, - "version": "2.1.6", + "version": "2.3.0", "scripts": { "test:engineering-dashboard": "PYTHONPATH=src python3 -m engineering_platform.dashboard_browser_validation", "test:engineering-dashboard-logic": "node --test tests/engineering/dashboard_status_store.test.mjs tests/engineering/ui_localization_contract.test.mjs", diff --git a/pyproject.toml b/pyproject.toml index 31ecd88..9f6c513 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "engineering-platform" -version = "2.1.6" +version = "2.3.0" description = "Local-first Engineering Platform execution operations runtime" readme = "README.md" requires-python = ">=3.11" diff --git a/src/engineering_platform/ENGINEERING_PLATFORM_CONFIG.json b/src/engineering_platform/ENGINEERING_PLATFORM_CONFIG.json index 0cb659b..d0dc4a0 100644 --- a/src/engineering_platform/ENGINEERING_PLATFORM_CONFIG.json +++ b/src/engineering_platform/ENGINEERING_PLATFORM_CONFIG.json @@ -3,7 +3,7 @@ "platform": { "id": "engineering-platform", "name": "Engineering Platform", - "version": "2.1.6", + "version": "2.3.0", "generation": 2, "documentation_namespace": "engineering-platform", "capability_registry_version": 1 diff --git a/src/engineering_platform/ENGINEERING_PLATFORM_VERSION.json b/src/engineering_platform/ENGINEERING_PLATFORM_VERSION.json index fd517cd..5b1ed1d 100644 --- a/src/engineering_platform/ENGINEERING_PLATFORM_VERSION.json +++ b/src/engineering_platform/ENGINEERING_PLATFORM_VERSION.json @@ -1,15 +1,15 @@ { "bootstrap_contract": "2026.12", "checkpoint_format": 1, - "dashboard_version": "2.1.6", + "dashboard_version": "2.3.0", "handoff_protocol": 1, "memory_format": 2, "minimum_codex_cli": "0.146.0", "inbox_protocol": 1, - "platform_version": "2.1.6", + "platform_version": "2.3.0", "report_format": 2, - "runner_version": "2.1.6", + "runner_version": "2.3.0", "status_model": 1, "storage_schema": 41, - "watcher_version": "2.1.6" + "watcher_version": "2.3.0" } diff --git a/src/engineering_platform/agent_state.py b/src/engineering_platform/agent_state.py index 6541f53..f0c46e7 100644 --- a/src/engineering_platform/agent_state.py +++ b/src/engineering_platform/agent_state.py @@ -135,6 +135,12 @@ class TransactionState: agent_execution_seconds: float | None = None validation_evidence: tuple[dict[str, str], ...] = () quality_evidence: tuple[dict[str, str], ...] = () + # Versioned post-implementation assurance. These records are immutable + # observations; a later review appends a new record instead of changing an + # earlier finding into a pass. + assurance_profile: dict[str, str] | None = None + assurance_reviews: tuple[dict[str, object], ...] = () + assurance_resolutions: tuple[dict[str, str], ...] = () repair_iterations: int = 0 repair_audit: tuple[dict[str, str], ...] = () local_validation_iterations: int = 0 @@ -172,6 +178,9 @@ def from_dict(cls, raw: object) -> "TransactionState": "agent_execution_seconds": None, "validation_evidence": (), "quality_evidence": (), + "assurance_profile": None, + "assurance_reviews": (), + "assurance_resolutions": (), "repair_iterations": 0, "repair_audit": (), "local_validation_iterations": 0, @@ -194,6 +203,10 @@ def from_dict(cls, raw: object) -> "TransactionState": raw = {**raw, "validation_evidence": tuple(raw["validation_evidence"])} if isinstance(raw.get("quality_evidence"), list): raw = {**raw, "quality_evidence": tuple(raw["quality_evidence"])} + if isinstance(raw.get("assurance_reviews"), list): + raw = {**raw, "assurance_reviews": tuple(raw["assurance_reviews"])} + if isinstance(raw.get("assurance_resolutions"), list): + raw = {**raw, "assurance_resolutions": tuple(raw["assurance_resolutions"])} if isinstance(raw.get("repair_audit"), list): raw = {**raw, "repair_audit": tuple(raw["repair_audit"])} if isinstance(raw.get("local_validation_audit"), list): @@ -274,17 +287,24 @@ def from_dict(cls, raw: object) -> "TransactionState": ) ): raise StateError("checkpoint local validation audit is invalid or unsafe") + repair_audit_fields = audit_fields | {"repair_id", "origin", "input_candidate_sha", "dispatch_id"} if ( not isinstance(state.repair_audit, tuple) or len(state.repair_audit) > 3 or any( - not isinstance(item, dict) or set(item) != audit_fields + not isinstance(item, dict) or set(item) not in (audit_fields, repair_audit_fields) or not all(isinstance(value, str) and value and len(value) <= MAX_DIAGNOSTIC_LENGTH and value == redact_diagnostic(value) for value in item.values()) or not item["iteration"].isdigit() or int(item["iteration"]) < 1 or item["outcome"] not in {"planned", "submitted_for_recheck", "agent_failed", "agent_timed_out"} or (item["commit_sha"] != "not_recorded" and not re.fullmatch(r"[0-9a-f]{40}", item["commit_sha"])) + or (set(item) == repair_audit_fields and ( + not re.fullmatch(r"repair:[A-Za-z0-9_.:-]+:[1-3]", item["repair_id"]) + or item["origin"] not in {"validation", "quality", "security", "hosted", "finalization"} + or (item["input_candidate_sha"] != "not_recorded" and not re.fullmatch(r"[0-9a-f]{40}", item["input_candidate_sha"])) + )) for item in state.repair_audit ) + or len({item.get("repair_id", f"legacy:{item['iteration']}") for item in state.repair_audit}) != len(state.repair_audit) ): raise StateError("checkpoint repair audit is invalid or unsafe") if ( @@ -357,6 +377,64 @@ def from_dict(cls, raw: object) -> "TransactionState": ) ): raise StateError("checkpoint quality evidence is invalid or unsafe") + profile_fields = {"version", "digest", "candidate_sha"} + current_profile_fields = profile_fields | {"criteria_digest"} + if state.assurance_profile is not None and ( + not isinstance(state.assurance_profile, dict) + or set(state.assurance_profile) not in (profile_fields, current_profile_fields) + or not all(isinstance(value, str) and value for value in state.assurance_profile.values()) + or not re.fullmatch(r"sha256:[0-9a-f]{64}", state.assurance_profile["digest"]) + or not re.fullmatch(r"[0-9a-f]{40}", state.assurance_profile["candidate_sha"]) + ): + raise StateError("checkpoint assurance profile is invalid") + review_fields = {"reviewer", "status", "candidate_sha", "profile_digest", "invocation_id", "findings"} + current_review_fields = review_fields | {"contract_version", "started_at", "completed_at"} + finding_fields = {"id", "fingerprint", "category", "criterion", "observation", "severity", "confidence", "blocking", "disposition"} + current_finding_fields = finding_fields | {"evidence_ref"} + if ( + not isinstance(state.assurance_reviews, tuple) + or len(state.assurance_reviews) > 16 + or any( + not isinstance(review, dict) or set(review) not in (review_fields, current_review_fields) + or review.get("reviewer") not in {"quality", "security"} + or review.get("status") not in {"PASS", "FAIL", "UNRESOLVED"} + or not isinstance(review.get("candidate_sha"), str) or not re.fullmatch(r"[0-9a-f]{40}", review["candidate_sha"]) + or not isinstance(review.get("profile_digest"), str) or not re.fullmatch(r"sha256:[0-9a-f]{64}", review["profile_digest"]) + or not isinstance(review.get("invocation_id"), str) or not review["invocation_id"] + or not isinstance(review.get("findings"), list) or len(review["findings"]) > 12 + or any( + not isinstance(finding, dict) or set(finding) not in (finding_fields, current_finding_fields) + or not all(isinstance(value, str) and value and len(value) <= 240 and value == redact_diagnostic(value, limit=240) for key, value in finding.items() if key != "blocking") + or finding.get("severity") not in {"LOW", "MEDIUM", "HIGH", "CRITICAL"} + or finding.get("confidence") not in {"LOW", "MEDIUM", "HIGH"} + or finding.get("disposition") not in {"OPEN", "RESOLVED", "REJECTED", "NON_BLOCKING"} + or not isinstance(finding.get("blocking"), bool) + or ("evidence_ref" in finding and (not isinstance(finding["evidence_ref"], str) or not finding["evidence_ref"])) + for finding in review["findings"] + ) + or (set(review) == current_review_fields and ( + review.get("contract_version") != "1.0" + or not isinstance(review.get("started_at"), str) + or not isinstance(review.get("completed_at"), str) + )) + for review in state.assurance_reviews + ) + ): + raise StateError("checkpoint assurance review evidence is invalid") + resolution_fields = {"finding_id", "disposition", "resolution_ref", "candidate_sha"} + if ( + not isinstance(state.assurance_resolutions, tuple) + or len(state.assurance_resolutions) > 128 + or any( + not isinstance(item, dict) or set(item) != resolution_fields + or not all(isinstance(value, str) and value and len(value) <= MAX_DIAGNOSTIC_LENGTH and value == redact_diagnostic(value) for value in item.values()) + or item["disposition"] not in {"RESOLVED", "REJECTED", "RISK_ACCEPTED"} + or not re.fullmatch(r"[0-9a-f]{40}", item["candidate_sha"]) + for item in state.assurance_resolutions + ) + or len({item["finding_id"] for item in state.assurance_resolutions}) != len(state.assurance_resolutions) + ): + raise StateError("checkpoint assurance resolutions are invalid or unsafe") if not isinstance(state.terminal, bool) or state.terminal != (state.phase in {"COMPLETE", "BLOCKED", "FAILED"}): raise StateError("checkpoint terminal flag conflicts with phase") return state diff --git a/src/engineering_platform/assets/dashboard.css b/src/engineering_platform/assets/dashboard.css index 1fa94de..a67ad20 100644 --- a/src/engineering_platform/assets/dashboard.css +++ b/src/engineering_platform/assets/dashboard.css @@ -871,7 +871,7 @@ html:not([data-theme="light"]) .dashboard-modal-shell{--modal-header-surface:col .predecessor-retry::before{content:"↻";font:700 17px/1 system-ui;margin-right:8px;vertical-align:-1px} .execution-dismiss::before{content:"⊘"} #componentLogs .log-table{table-layout:auto;width:100%}#componentLogs .log-table th:last-child,#componentLogs .log-table td:last-child{width:100%} -#queueList .queue-item{grid-template-columns:1.25rem minmax(0,1fr) auto}.queue-defer{background:#3b281b;border:1px solid #f0b66a;border-radius:8px;color:#fff0dc;font:600 12px system-ui;min-height:32px;padding:5px 9px}.queue-defer:hover:not(:disabled){background:var(--house-style)!important;border-color:var(--house-style)!important;color:#201812!important}.queue-defer:disabled{cursor:wait;opacity:.7}html[data-theme="light"] .queue-defer{background:#fff8ef;border-color:#d68b23;color:#643a13}html[data-theme="light"] .queue-defer:hover:not(:disabled){background:var(--house-style)!important;border-color:var(--house-style)!important;color:#201812!important} +#queueList .queue-item{grid-template-columns:1.25rem minmax(0,1fr) auto}.queue-item__actions{align-items:flex-end;display:flex;flex-direction:column;gap:6px;justify-self:end}.queue-defer{background:#3b281b;border:1px solid #f0b66a;border-radius:8px;color:#fff0dc;font:600 12px system-ui;min-height:32px;padding:5px 9px}.queue-defer:hover:not(:disabled){background:var(--house-style)!important;border-color:var(--house-style)!important;color:#201812!important}.queue-defer:disabled{cursor:wait;opacity:.7}.queue-defer--destructive{background:#3a2028;border-color:#ff718f;color:#ffd9e1}.queue-defer--destructive:hover:not(:disabled){background:#ff718f!important;border-color:#ff718f!important;color:#23131a!important}html[data-theme="light"] .queue-defer{background:#fff8ef;border-color:#d68b23;color:#643a13}html[data-theme="light"] .queue-defer:hover:not(:disabled){background:var(--house-style)!important;border-color:var(--house-style)!important;color:#201812!important}html[data-theme="light"] .queue-defer--destructive{background:#fff1f4;border-color:#ff718f;color:#b32649}html[data-theme="light"] .queue-defer--destructive:hover:not(:disabled){background:#ff718f!important;border-color:#ff718f!important;color:#23131a!important} /* Shared semantic action variants prevent individual surfaces from drifting. */ .dashboard-action{align-items:center;border:1px solid;border-radius:50%;box-sizing:border-box;display:inline-flex;font:18px/1 system-ui;height:32px;justify-content:center;min-height:32px;min-width:32px;padding:0;width:32px} diff --git a/src/engineering_platform/assets/dashboard.js b/src/engineering_platform/assets/dashboard.js index 0f025d1..6af7c5c 100644 --- a/src/engineering_platform/assets/dashboard.js +++ b/src/engineering_platform/assets/dashboard.js @@ -106,6 +106,11 @@ function reviewerStatusLabel(value, fallback = t("format.not_available")) { const normalized = reviewerKey(raw) === "uitgevoerd" ? "completed" : reviewerKey(raw); return t(`reviewer.status.${normalized}`, {}, raw.replaceAll("_", " ")); } +function assuranceStatusLabel(value, fallback = t("format.not_available")) { + const raw = String(value || "").trim(); + if (!raw) return fallback; + return t(`lifecycle.assurance_status.${reviewerKey(raw)}`, {}, raw); +} function reviewerCapabilityLabel(value, fallback = t("format.not_available")) { const raw = String(value || "").trim(); return raw ? enumLabel(raw.toUpperCase(), raw) : fallback; @@ -914,6 +919,7 @@ function queueItems(x, queueDepth) { const row = document.createElement("li"), number = document.createElement("span"), body = document.createElement("div"), + actions = document.createElement("div"), title = document.createElement("span"), meta = document.createElement("div"), modified = Date.parse(item.modified_at || ""), @@ -928,6 +934,7 @@ function queueItems(x, queueDepth) { number.textContent = String(index + 1); title.className = "queue-item__title"; meta.className = "queue-item__meta"; + actions.className = "queue-item__actions"; title.textContent = displayTitle; meta.textContent = t("queue.filename", { filename, @@ -935,25 +942,73 @@ function queueItems(x, queueDepth) { ? locale.dateTime(new Date(modified)) : t("format.timestamp_unavailable"), }); - const defer = item.queue_source === "CENTRAL" ? null : document.createElement("button"); + const central = item.queue_source === "CENTRAL"; + const mutable = !central || ["QUEUED", "DEFERRED", "QUARANTINED"].includes(item.queue_state); + const defer = mutable ? document.createElement("button") : null; if (defer) { defer.className = "queue-defer"; defer.type = "button"; - defer.textContent = t("queue.defer_action"); - defer.title = t("queue.defer_action"); - defer.setAttribute("aria-label", t("queue.defer_action")); + const held = central && ["DEFERRED", "QUARANTINED"].includes(item.queue_state); + const actionKey = held ? "queue.resume" : "queue.defer"; + defer.textContent = t(`${actionKey}_action`); + defer.title = t(`${actionKey}_action`); + defer.setAttribute("aria-label", t(`${actionKey}_action`)); defer.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); - deferQueueItem(item, defer); + if (item.queue_source === "CENTRAL") { + queueDisposition(item, held ? "QUEUED" : "DEFERRED", held ? "Operator resumed this submission from Operations Console." : "Operator deferred this submission from Operations Console.", defer); + } else deferQueueItem(item, defer); }); } body.append(title, meta); row.append(number, body); - if (defer) row.append(defer); + if (defer) actions.append(defer); + if (item.queue_source === "CENTRAL" && ["QUEUED", "QUARANTINED"].includes(item.queue_state)) { + (item.queue_state === "QUEUED" + ? [["QUARANTINED", "queue.quarantine", "Operator quarantined this submission from Operations Console."], + ["DECLINED", "queue.decline", "Operator declined this submission from Operations Console."]] + : [["DECLINED", "queue.decline", "Operator declined this quarantined submission from Operations Console."]] + ).forEach(([disposition, actionKey, reason]) => { + const action = document.createElement("button"); + action.className = `queue-defer${disposition === "DECLINED" ? " queue-defer--destructive" : ""}`; + action.type = "button"; + action.textContent = t(`${actionKey}_action`); + action.title = t(`${actionKey}_action`); + action.setAttribute("aria-label", t(`${actionKey}_action`)); + action.addEventListener("click", (event) => { + event.preventDefault(); event.stopPropagation(); + queueDisposition(item, disposition, reason, action); + }); + actions.append(action); + }); + } + if (actions.childElementCount) row.append(actions); container.append(row); }); } +function queueDisposition(item, disposition, reason, button) { + const submissionId = String(item?.submission_id || item?.filename || ""); + if (!submissionId) return; + const actionKey = { DEFERRED: "queue.defer", QUEUED: "queue.resume", QUARANTINED: "queue.quarantine", DECLINED: "queue.decline" }[disposition]; + if (!actionKey) return; + confirmDashboardAction( + t(`${actionKey}_title`), t(`${actionKey}_description`, { title: submissionId }), t(`${actionKey}_action`), + { destructive: disposition === "DECLINED" }, + ) + .then((confirmed) => { + if (!confirmed) return; + button.disabled = true; + const operationId = crypto.randomUUID(); + return fetch("/api/queue-disposition", { method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ contract_version: "1.0", operation_id: operationId, submission_id: submissionId, + expected_state: item.queue_state, expected_revision: item.disposition_revision, disposition, reason }) }) + .then(async (response) => ({ ok: response.ok, body: await response.json().catch(() => ({})) })) + .then((result) => { if (!result.ok) throw Error(result.body.error || t("queue.defer_failed")); return refreshDashboard(); }) + .catch((error) => showDashboardError(error.message, t("queue.defer_failed"))) + .finally(() => { button.disabled = false; }); + }); +} function deferQueueItem(item, button) { const filename = String(item?.filename || ""); if (!filename) return; @@ -1971,6 +2026,30 @@ function lifecycleQualityEvidence(step) { void localizeDynamicEvidence(dynamicRows); return section; } +function lifecycleAssuranceEvidence(step) { + const reviews = Array.isArray(step?.assurance_reviews) ? step.assurance_reviews : []; + if (!reviews.length) return null; + const section = document.createElement("section"); + section.className = "lifecycle-detail-modal__quality-evidence"; + const rounds = step?.repair_rounds || {}; + section.append(Object.assign(document.createElement("h3"), { textContent: t("lifecycle.detail_assurance") })); + if (Number.isFinite(Number(rounds.used))) section.append(Object.assign(document.createElement("p"), { + className: "estimate-meta", textContent: t("lifecycle.repair_rounds", { used: rounds.used, maximum: rounds.maximum || 3 }), + })); + const list = document.createElement("ol"); list.className = "lifecycle-detail-modal__phase-list"; + for (const review of reviews) { + if (!review || typeof review !== "object") continue; + const findings = Array.isArray(review.findings) ? review.findings : []; + const item = document.createElement("li"); + const role = String(review.reviewer || ""); const status = String(review.status || "UNRESOLVED"); + item.append(Object.assign(document.createElement("strong"), { textContent: `${reviewerLabel(role, role)} · ${assuranceStatusLabel(status, status)}` })); + const summary = findings.map((finding) => String(finding?.observation || "").trim()).filter(Boolean).join("; "); + item.append(Object.assign(document.createElement("span"), { textContent: summary || t("lifecycle.assurance_no_findings") })); + list.append(item); + } + if (!list.childElementCount) return null; + section.append(list); return section; +} function lifecycleRepairEvidence(step) { const audit = Array.isArray(step?.repair_audit) ? step.repair_audit : []; if (!audit.length) return null; @@ -2060,6 +2139,8 @@ function openLifecycleDetail(step, trigger) { content.append(phaseTiming); const qualityEvidence = lifecycleQualityEvidence(step); if (qualityEvidence) content.append(qualityEvidence); + const assuranceEvidence = lifecycleAssuranceEvidence(step); + if (assuranceEvidence) content.append(assuranceEvidence); const repairEvidence = lifecycleRepairEvidence(step); if (repairEvidence) content.append(repairEvidence); if (!modal.open) modal.showModal(); diff --git a/src/engineering_platform/assets/dashboard_locales.mjs b/src/engineering_platform/assets/dashboard_locales.mjs index b1bc984..9abc5b3 100644 --- a/src/engineering_platform/assets/dashboard_locales.mjs +++ b/src/engineering_platform/assets/dashboard_locales.mjs @@ -377,6 +377,15 @@ export const DASHBOARD_MESSAGES = { "queue.defer_description": "Move {title} out of the active queue? It is retained in Inbox/_deferred and will not be executed until it is returned manually.", "queue.defer_failed": "The Inbox item could not be deferred safely.", "queue.defer_title": "Defer execution", + "queue.resume_action": "Resume", + "queue.resume_description": "Return {title} to the active queue? It may be executed when it reaches the front of the queue.", + "queue.resume_title": "Resume execution", + "queue.quarantine_action": "Quarantine", + "queue.quarantine_description": "Place {title} in quarantine? It remains preserved and cannot be executed until an operator resumes it.", + "queue.quarantine_title": "Quarantine execution", + "queue.decline_action": "Decline", + "queue.decline_description": "Decline {title}? This closes the submission and it cannot be resumed from the queue.", + "queue.decline_title": "Decline execution", "queue.filename": "Filename: {filename} · changed: {modified}", "queue.runtime_invocation_blocked": "The Inbox is waiting because the local Codex CLI cannot start. To repair it manually, run: npm install -g @openai/codex@latest", "queue.managed_branch_blocked": "The Inbox is paused because this workspace is on a working branch. The Execution Host may only claim work from main.", @@ -1013,6 +1022,15 @@ export const DASHBOARD_MESSAGES = { "queue.defer_description": "{title} uit de actieve wachtrij halen? Het bestand blijft bewaard in Inbox/_deferred en wordt pas weer uitgevoerd wanneer het handmatig wordt teruggezet.", "queue.defer_failed": "De Inbox-opdracht kon niet veilig worden uitgesteld.", "queue.defer_title": "Uitvoering uitstellen", + "queue.resume_action": "Hervatten", + "queue.resume_description": "{title} terugzetten in de actieve wachtrij? De uitvoering kan starten zodra deze vooraan staat.", + "queue.resume_title": "Uitvoering hervatten", + "queue.quarantine_action": "In quarantaine zetten", + "queue.quarantine_description": "{title} in quarantaine zetten? De inzending blijft bewaard en kan pas na hervatten door een operator worden uitgevoerd.", + "queue.quarantine_title": "Uitvoering in quarantaine zetten", + "queue.decline_action": "Afwijzen", + "queue.decline_description": "{title} afwijzen? Hiermee wordt de inzending gesloten en kan deze niet vanuit de wachtrij worden hervat.", + "queue.decline_title": "Uitvoering afwijzen", "queue.filename": "Bestandsnaam: {filename} · gewijzigd: {modified}", "queue.runtime_invocation_blocked": "De Inbox wacht omdat de lokale Codex CLI niet kan starten. Herstel dit handmatig met: npm install -g @openai/codex@latest", "queue.managed_branch_blocked": "De Inbox is gepauzeerd omdat deze werkmap op een werkbranch staat. De Execution Host mag alleen werk vanaf main claimen.", @@ -1649,6 +1667,15 @@ export const DASHBOARD_MESSAGES = { "queue.defer_description": "{title} aus der aktiven Warteschlange verschieben? Die Datei bleibt in Inbox/_deferred erhalten und wird erst nach einer manuellen Rückgabe ausgeführt.", "queue.defer_failed": "Der Inbox-Auftrag konnte nicht sicher zurückgestellt werden.", "queue.defer_title": "Ausführung zurückstellen", + "queue.resume_action": "Fortsetzen", + "queue.resume_description": "{title} wieder in die aktive Warteschlange stellen? Die Ausführung kann beginnen, sobald sie an der Reihe ist.", + "queue.resume_title": "Ausführung fortsetzen", + "queue.quarantine_action": "In Quarantäne verschieben", + "queue.quarantine_description": "{title} in Quarantäne verschieben? Die Übermittlung bleibt erhalten und kann erst nach einer Operatorfreigabe ausgeführt werden.", + "queue.quarantine_title": "Ausführung in Quarantäne verschieben", + "queue.decline_action": "Ablehnen", + "queue.decline_description": "{title} ablehnen? Dadurch wird die Übermittlung geschlossen und kann nicht mehr aus der Warteschlange fortgesetzt werden.", + "queue.decline_title": "Ausführung ablehnen", "queue.filename": "Dateiname: {filename} · geändert: {modified}", "queue.runtime_invocation_blocked": "Die Inbox wartet, weil die lokale Codex-CLI nicht starten kann. Manuelle Reparatur: npm install -g @openai/codex@latest", "queue.managed_branch_blocked": "Die Inbox ist pausiert, weil dieser Arbeitsbereich auf einem Arbeitsbranch steht. Der Execution Host darf Arbeit nur von main beanspruchen.", @@ -2220,6 +2247,15 @@ export const DASHBOARD_MESSAGES = { "queue.defer_description": "Retirer {title} de la file active ? Le fichier reste conservé dans Inbox/_deferred et ne sera exécuté qu’après un retour manuel.", "queue.defer_failed": "L’élément Inbox n’a pas pu être reporté en toute sécurité.", "queue.defer_title": "Reporter l’exécution", + "queue.resume_action": "Reprendre", + "queue.resume_description": "Remettre {title} dans la file active ? L’exécution pourra démarrer lorsqu’elle arrivera en tête.", + "queue.resume_title": "Reprendre l’exécution", + "queue.quarantine_action": "Mettre en quarantaine", + "queue.quarantine_description": "Mettre {title} en quarantaine ? La soumission reste conservée et ne pourra être exécutée qu’après reprise par un opérateur.", + "queue.quarantine_title": "Mettre l’exécution en quarantaine", + "queue.decline_action": "Refuser", + "queue.decline_description": "Refuser {title} ? La soumission sera clôturée et ne pourra plus être reprise depuis la file.", + "queue.decline_title": "Refuser l’exécution", "queue.filename": "Nom du fichier : {filename} · modifié : {modified}", "queue.runtime_invocation_blocked": "La boîte de réception attend car l’interface CLI Codex locale ne peut pas démarrer. Réparation manuelle : npm install -g @openai/codex@latest", "queue.managed_branch_blocked": "La boîte de réception est en pause car cet espace de travail est sur une branche de travail. L’Execution Host ne peut réclamer du travail que depuis main.", @@ -2791,6 +2827,15 @@ export const DASHBOARD_MESSAGES = { "queue.defer_description": "¿Quitar {title} de la cola activa? El archivo se conserva en Inbox/_deferred y no se ejecutará hasta que se devuelva manualmente.", "queue.defer_failed": "El elemento de Inbox no se pudo aplazar de forma segura.", "queue.defer_title": "Aplazar ejecución", + "queue.resume_action": "Reanudar", + "queue.resume_description": "¿Devolver {title} a la cola activa? La ejecución podrá comenzar cuando llegue al primer puesto.", + "queue.resume_title": "Reanudar ejecución", + "queue.quarantine_action": "Poner en cuarentena", + "queue.quarantine_description": "¿Poner {title} en cuarentena? El envío se conserva y solo podrá ejecutarse tras reanudarlo un operador.", + "queue.quarantine_title": "Poner la ejecución en cuarentena", + "queue.decline_action": "Rechazar", + "queue.decline_description": "¿Rechazar {title}? El envío se cerrará y no podrá reanudarse desde la cola.", + "queue.decline_title": "Rechazar ejecución", "queue.filename": "Nombre de archivo: {filename} · modificado: {modified}", "queue.runtime_invocation_blocked": "La bandeja de entrada está esperando porque la CLI local de Codex no puede iniciarse. Reparación manual: npm install -g @openai/codex@latest", "queue.managed_branch_blocked": "La bandeja de entrada está en pausa porque este espacio de trabajo está en una rama de trabajo. El Execution Host solo puede reclamar trabajo desde main.", @@ -3321,6 +3366,11 @@ Object.assign(DASHBOARD_MESSAGES.nl, {"lifecycle.detail_quality_evidence":"Uitge Object.assign(DASHBOARD_MESSAGES.de, {"lifecycle.detail_quality_evidence":"Durchgeführte Qualitätsverbesserungen","lifecycle.quality_evidence.refactor":"Refaktorierung","lifecycle.quality_evidence.test_coverage":"Testabdeckung","lifecycle.quality_evidence.documentation":"Dokumentation","lifecycle.quality_evidence.validation":"Validierung","lifecycle.quality_evidence.no_change_required":"Keine Änderung erforderlich"}); Object.assign(DASHBOARD_MESSAGES.fr, {"lifecycle.detail_quality_evidence":"Améliorations de qualité réalisées","lifecycle.quality_evidence.refactor":"Refactorisation","lifecycle.quality_evidence.test_coverage":"Couverture de tests","lifecycle.quality_evidence.documentation":"Documentation","lifecycle.quality_evidence.validation":"Validation","lifecycle.quality_evidence.no_change_required":"Aucune modification nécessaire"}); Object.assign(DASHBOARD_MESSAGES.es, {"lifecycle.detail_quality_evidence":"Mejoras de calidad realizadas","lifecycle.quality_evidence.refactor":"Refactorización","lifecycle.quality_evidence.test_coverage":"Cobertura de pruebas","lifecycle.quality_evidence.documentation":"Documentación","lifecycle.quality_evidence.validation":"Validación","lifecycle.quality_evidence.no_change_required":"No se requiere ningún cambio"}); +Object.assign(DASHBOARD_MESSAGES.en, {"lifecycle.detail_assurance":"Quality and security assurance","lifecycle.repair_rounds":"Repair rounds: {used}/{maximum}","lifecycle.assurance_no_findings":"No findings recorded"}); +Object.assign(DASHBOARD_MESSAGES.nl, {"lifecycle.detail_assurance":"Kwaliteits- en beveiligingscontrole","lifecycle.repair_rounds":"Herstelrondes: {used}/{maximum}","lifecycle.assurance_no_findings":"Geen bevindingen vastgelegd"}); +Object.assign(DASHBOARD_MESSAGES.de, {"lifecycle.detail_assurance":"Qualitäts- und Sicherheitsprüfung","lifecycle.repair_rounds":"Reparaturrunden: {used}/{maximum}","lifecycle.assurance_no_findings":"Keine Befunde erfasst"}); +Object.assign(DASHBOARD_MESSAGES.fr, {"lifecycle.detail_assurance":"Assurance qualité et sécurité","lifecycle.repair_rounds":"Cycles de correction : {used}/{maximum}","lifecycle.assurance_no_findings":"Aucune conclusion enregistrée"}); +Object.assign(DASHBOARD_MESSAGES.es, {"lifecycle.detail_assurance":"Garantía de calidad y seguridad","lifecycle.repair_rounds":"Rondas de reparación: {used}/{maximum}","lifecycle.assurance_no_findings":"No se registraron hallazgos"}); Object.assign(DASHBOARD_MESSAGES.en, {"lifecycle.detail_repair_evidence":"Executed pull request check repairs","lifecycle.detail_repair_iteration":"Repair iteration {iteration}","lifecycle.repair_outcome.submitted_for_recheck":"Submitted for recheck","lifecycle.repair_outcome.agent_failed":"Agent repair failed","lifecycle.repair_outcome.agent_timed_out":"Agent repair timed out"}); Object.assign(DASHBOARD_MESSAGES.nl, {"lifecycle.detail_repair_evidence":"Uitgevoerd PR-controleherstel","lifecycle.detail_repair_iteration":"Hersteliteratie {iteration}","lifecycle.repair_outcome.submitted_for_recheck":"Ingediend voor hercontrole","lifecycle.repair_outcome.agent_failed":"Agentherstel mislukt","lifecycle.repair_outcome.agent_timed_out":"Agentherstel heeft de tijdslimiet overschreden"}); Object.assign(DASHBOARD_MESSAGES.de, {"lifecycle.detail_repair_evidence":"Durchgeführte PR-Prüfreparaturen","lifecycle.detail_repair_iteration":"Reparaturiteration {iteration}","lifecycle.repair_outcome.submitted_for_recheck":"Zur erneuten Prüfung eingereicht","lifecycle.repair_outcome.agent_failed":"Agentenreparatur fehlgeschlagen","lifecycle.repair_outcome.agent_timed_out":"Zeitlimit der Agentenreparatur überschritten"}); @@ -3375,6 +3425,11 @@ Object.assign(DASHBOARD_MESSAGES.nl, {"telemetry.phase.quality_control":"Kwalite Object.assign(DASHBOARD_MESSAGES.de, {"telemetry.phase.quality_control":"Qualitätskontrolle"}); Object.assign(DASHBOARD_MESSAGES.fr, {"telemetry.phase.quality_control":"Contrôle qualité"}); Object.assign(DASHBOARD_MESSAGES.es, {"telemetry.phase.quality_control":"Control de calidad"}); +Object.assign(DASHBOARD_MESSAGES.en, {"telemetry.phase.quality_control_agent":"Autonomous quality control","reviewer.quality":"Quality review","reviewer.security":"Security review","lifecycle.assurance_status.pass":"Passed","lifecycle.assurance_status.fail":"Failed","lifecycle.assurance_status.unresolved":"Unresolved"}); +Object.assign(DASHBOARD_MESSAGES.nl, {"telemetry.phase.quality_control_agent":"Autonome kwaliteitscontrole","reviewer.quality":"Kwaliteitsreview","reviewer.security":"Beveiligingsreview","lifecycle.assurance_status.pass":"Geslaagd","lifecycle.assurance_status.fail":"Mislukt","lifecycle.assurance_status.unresolved":"Niet opgehelderd"}); +Object.assign(DASHBOARD_MESSAGES.de, {"telemetry.phase.quality_control_agent":"Autonome Qualitätskontrolle","reviewer.quality":"Qualitätsprüfung","reviewer.security":"Sicherheitsprüfung","lifecycle.assurance_status.pass":"Bestanden","lifecycle.assurance_status.fail":"Fehlgeschlagen","lifecycle.assurance_status.unresolved":"Ungeklärt"}); +Object.assign(DASHBOARD_MESSAGES.fr, {"telemetry.phase.quality_control_agent":"Contrôle qualité autonome","reviewer.quality":"Revue qualité","reviewer.security":"Revue de sécurité","lifecycle.assurance_status.pass":"Réussi","lifecycle.assurance_status.fail":"Échec","lifecycle.assurance_status.unresolved":"Non résolu"}); +Object.assign(DASHBOARD_MESSAGES.es, {"telemetry.phase.quality_control_agent":"Control de calidad autónomo","reviewer.quality":"Revisión de calidad","reviewer.security":"Revisión de seguridad","lifecycle.assurance_status.pass":"Correcto","lifecycle.assurance_status.fail":"Fallido","lifecycle.assurance_status.unresolved":"Sin resolver"}); Object.assign(DASHBOARD_MESSAGES.en, {"workspace_progress.primary_codex_commands":"{count} primary Codex commands executed","telemetry.phase.capability_review":"Capability review","lifecycle.step.reconcile_agent":"End reconciliation","lifecycle.step.wait_for_reconciliation_merge":"Reconciliation merge","operational.stale_run":"Execution no longer active","operational.waiting_for_operator_merge":"Implementation merge","operational.stale_host_ownership":"Execution Host ownership is no longer active; no execution is currently running."}); Object.assign(DASHBOARD_MESSAGES.nl, {"workspace_progress.primary_codex_commands":"{count} primaire Codex-opdrachten uitgevoerd","telemetry.phase.capability_review":"Specialistenreview","lifecycle.step.reconcile_agent":"Eind-reconciliatie","lifecycle.step.wait_for_reconciliation_merge":"Reconciliatie-merge","operational.stale_run":"Uitvoering niet meer actief","operational.waiting_for_operator_merge":"Implementatie-merge","operational.stale_host_ownership":"De uitvoeringseigenaar is niet meer actief; er draait momenteel geen uitvoering."}); Object.assign(DASHBOARD_MESSAGES.de, {"workspace_progress.primary_codex_commands":"{count} primäre Codex-Befehle ausgeführt","telemetry.phase.capability_review":"Fähigkeitsprüfung","lifecycle.step.reconcile_agent":"Endabgleich","lifecycle.step.wait_for_reconciliation_merge":"Abgleich-Merge","operational.stale_run":"Ausführung nicht mehr aktiv","operational.waiting_for_operator_merge":"Implementierungs-Merge","operational.stale_host_ownership":"Die Ausführungsinstanz ist nicht mehr aktiv; derzeit läuft keine Ausführung."}); diff --git a/src/engineering_platform/capability_preflight.py b/src/engineering_platform/capability_preflight.py index 27c46f0..c6c406b 100644 --- a/src/engineering_platform/capability_preflight.py +++ b/src/engineering_platform/capability_preflight.py @@ -118,7 +118,16 @@ def execute(root: Path, prompt: str, *, run_id: str | None = None) -> Capability started, checks = monotonic(), [] requirements = _requirements(prompt) mode = requirements.get("execution_mode", "MANAGED").strip().upper() - required_providers = provider_readiness_failures(root, require_github=mode != "GENESIS") + # Installed deterministic qualification replaces GitHub with the local + # adapter before any lifecycle work. Its admission must therefore test + # the adapter composition, not require an unrelated live GitHub session. + qualification_local_github = ( + os.environ.get("EP_QUALIFICATION_DETERMINISTIC_FLOW") == "1" + and os.environ.get("EP_QUALIFICATION_GITHUB_WRITE_FLOW") != "1" + ) + required_providers = provider_readiness_failures( + root, require_github=mode != "GENESIS" and not qualification_local_github, + ) checks.append( _check( "provider_readiness", diff --git a/src/engineering_platform/capability_review.py b/src/engineering_platform/capability_review.py index dc71b04..f09c204 100644 --- a/src/engineering_platform/capability_review.py +++ b/src/engineering_platform/capability_review.py @@ -29,6 +29,8 @@ "finalization", ) REVIEWER_LABELS = { + "quality": "Quality Reviewer", + "security": "Security Reviewer", "apple_platform": "Apple Platform Reviewer", "windows_platform": "Windows Platform Reviewer", "home_assistant_integration": "Home Assistant Integration Reviewer", @@ -42,6 +44,10 @@ "documentation": "Documentation Reviewer", "finalization": "Finalization Reviewer", } +MANDATORY_REVIEW_OUTPUT_CONTRACT_VERSION = "1.0" +MANDATORY_FINDING_FIELDS = frozenset({ + "id", "observation", "category", "criterion", "severity", "confidence", "evidence_ref", +}) PRODUCT_MATCHERS = { "apple_platform": (("apps/apple/", "engineering-platform-app", "swiftui", "watchos", "macos", "ios"), "Apple platform capability"), "windows_platform": (("apps/windows/", "engineering-platform-windows", "maui", "windows packaging"), "Windows platform capability"), @@ -67,6 +73,11 @@ class ReviewerResult: reviewer: str contribution: str recommendations: tuple[str, ...] = () + findings: tuple[dict[str, str], ...] = () + # In-process deterministic/test adapters construct this typed result only + # after choosing the mandatory contract. Raw provider JSON is still + # required to carry this field by ``CodexCliClient.review``. + contract_version: str | None = MANDATORY_REVIEW_OUTPUT_CONTRACT_VERSION failed: bool = False usage: dict[str, object] = field(default_factory=dict) runtime_metadata: dict[str, object] = field(default_factory=dict) @@ -144,12 +155,15 @@ def invoke(selection: ReviewerSelection) -> ReviewerResult: result = ReviewerResult( selection.reviewer, redact_diagnostic(result.contribution, limit=240), - tuple(redact_diagnostic(value, limit=240) for value in result.recommendations[:3]), + tuple(redact_diagnostic(value, limit=240) for value in result.recommendations), + tuple(dict(item) for item in result.findings if isinstance(item, dict)), + result.contract_version, result.failed, result.usage, result.runtime_metadata, result.churn, result.duration_seconds, + result.usage_snapshots, ) except Exception: # Reviewer failure is advisory and cannot block the transaction. result = ReviewerResult(selection.reviewer, "Reviewer failed; primary review continues.", failed=True) @@ -174,6 +188,28 @@ def reconciled_recommendations(results: tuple[ReviewerResult, ...]) -> tuple[str return tuple(accepted[:8]) +def mandatory_findings(result: ReviewerResult) -> tuple[dict[str, str], ...] | None: + """Return validated mandatory output, never advice-shaped pseudo-evidence.""" + if result.failed or result.contract_version != MANDATORY_REVIEW_OUTPUT_CONTRACT_VERSION: + return None + if not isinstance(result.findings, tuple) or len(result.findings) > 64: + return None + normalized: list[dict[str, str]] = [] + seen: set[str] = set() + for finding in result.findings: + if not isinstance(finding, dict) or set(finding) != MANDATORY_FINDING_FIELDS: + return None + if any(not isinstance(value, str) or not value.strip() or len(value) > 240 for value in finding.values()): + return None + if finding["severity"] not in {"LOW", "MEDIUM", "HIGH", "CRITICAL"} or finding["confidence"] not in {"LOW", "MEDIUM", "HIGH"}: + return None + if finding["id"] in seen: + return None + seen.add(finding["id"]) + normalized.append({key: redact_diagnostic(value, limit=240) for key, value in finding.items()}) + return tuple(normalized) + + def records_for_storage(selections: tuple[ReviewerSelection, ...], results: tuple[ReviewerResult, ...]) -> tuple[dict[str, object], ...]: by_reviewer = {result.reviewer: result for result in results} records: list[dict[str, object]] = [] diff --git a/src/engineering_platform/console_route_ownership.py b/src/engineering_platform/console_route_ownership.py index f0d44d3..818c7a1 100644 --- a/src/engineering_platform/console_route_ownership.py +++ b/src/engineering_platform/console_route_ownership.py @@ -60,6 +60,7 @@ def matches(self, method: str, path: str) -> bool: ConsoleRoute(("GET",), r"/api/prompt-history/[a-z0-9][a-z0-9-]{0,63}/(?:report|chat|details)", PROJECT, "project_history", "Project run detail"), ConsoleRoute(("GET",), r"/api/telemetry/[0-9]{4}-[0-9]{2}-[0-9]{2}", PROJECT, "project_history", "Project telemetry detail"), ConsoleRoute(("POST",), r"/api/execution-(?:dismiss|retry)", PROJECT, "project_execution", "Project execution action"), + ConsoleRoute(("POST",), r"/api/queue-disposition", PROJECT, "project_execution", "Project queue disposition action"), ConsoleRoute(("POST",), r"/api/dashboard-translate", PROJECT, "project_console", "Project Console translation"), ConsoleRoute(("GET",), r"/diagnostics/topology", TRANSPORT_INTERNAL, "transport", "Transport topology diagnostic"), ConsoleRoute(("GET",), r"/(?:healthz|readyz)", TRANSPORT_INTERNAL, "transport", "Transport probe"), diff --git a/src/engineering_platform/execution_executor.py b/src/engineering_platform/execution_executor.py index 47fc14c..8b1732f 100644 --- a/src/engineering_platform/execution_executor.py +++ b/src/engineering_platform/execution_executor.py @@ -16,7 +16,12 @@ from threading import Event, Thread from typing import Callable, Mapping -from .capability_review import ReviewerResult, ReviewerSelection, reviewer_prompt +from .capability_review import ( + MANDATORY_REVIEW_OUTPUT_CONTRACT_VERSION, + ReviewerResult, + ReviewerSelection, + reviewer_prompt, +) from .codex_observability import codex_final_message as _codex_final_message, extract_codex_runtime_metadata, extract_codex_usage from .evidence_projection import ToolProxyEnvironment from .execution_context import additional_workspace_write_roots @@ -382,14 +387,31 @@ def review( schema = { "type": "object", "additionalProperties": False, - "required": ["contribution", "recommendations"], + "required": ["contract_version", "contribution", "recommendations", "findings"], "properties": { + "contract_version": {"type": "string", "const": MANDATORY_REVIEW_OUTPUT_CONTRACT_VERSION}, "contribution": {"type": "string", "maxLength": 240}, "recommendations": { "type": "array", - "maxItems": 3, + "maxItems": 64, "items": {"type": "string", "maxLength": 240}, }, + "findings": { + "type": "array", "maxItems": 64, + "items": { + "type": "object", "additionalProperties": False, + "required": ["id", "observation", "category", "criterion", "severity", "confidence", "evidence_ref"], + "properties": { + "id": {"type": "string", "maxLength": 240}, + "observation": {"type": "string", "maxLength": 240}, + "category": {"type": "string", "maxLength": 240}, + "criterion": {"type": "string", "maxLength": 240}, + "severity": {"type": "string", "enum": ["LOW", "MEDIUM", "HIGH", "CRITICAL"]}, + "confidence": {"type": "string", "enum": ["LOW", "MEDIUM", "HIGH"]}, + "evidence_ref": {"type": "string", "maxLength": 240}, + }, + }, + }, }, } state_directory = root / ".engineering" @@ -444,6 +466,8 @@ def review( selection.reviewer, str(raw["contribution"]), tuple(str(value) for value in raw["recommendations"]), + tuple(dict(value) for value in raw["findings"]), + str(raw["contract_version"]), usage=dict(self.last_usage), runtime_metadata=dict(self.last_runtime_metadata), churn=dict(self.last_churn), duration_seconds=self.last_execution_seconds, usage_snapshots=self.last_usage_snapshots, @@ -528,7 +552,7 @@ def invoke(self, root: Path, prompt: str) -> AgentResult: "codex", "exec", "--sandbox", - MANAGED_EXECUTION_SANDBOX, + getattr(self, "_sandbox_override", MANAGED_EXECUTION_SANDBOX), "-C", str(root), "--json", @@ -605,6 +629,22 @@ def invoke(self, root: Path, prompt: str) -> AgentResult: interruption_reason=interruption, ) from error + def validate(self, root: Path, prompt: str) -> AgentResult: + """Run the local-validation provider turn without repository writes. + + The host, rather than an instruction in the prompt, selects the + sandbox. The override is scoped to this synchronous call so a later + implementation, repair, or publication invocation retains its normal + bounded write capability. + """ + if hasattr(self, "_sandbox_override"): + raise RunnerError("nested validation sandbox override is invalid") + self._sandbox_override = "read-only" + try: + return self.invoke(root, prompt) + finally: + del self._sandbox_override + def _run_invocation( self, command: tuple[str, ...], root: Path, environment: Mapping[str, str] | None = None ) -> subprocess.CompletedProcess[str]: diff --git a/src/engineering_platform/execution_host.py b/src/engineering_platform/execution_host.py index f556edc..de68499 100644 --- a/src/engineering_platform/execution_host.py +++ b/src/engineering_platform/execution_host.py @@ -3,6 +3,7 @@ from __future__ import annotations import argparse +import hashlib from datetime import datetime, timezone from dataclasses import replace import json @@ -22,8 +23,10 @@ from .agent_state import MAX_COMMIT_EVIDENCE_RECORDS, StateError, StateStore, TransactionState, redact_diagnostic, verified_commit_evidence_record from .capability_review import ( + MANDATORY_REVIEW_OUTPUT_CONTRACT_VERSION, ReviewerResult, ReviewerSelection, + mandatory_findings, records_for_storage, run_reviews, select_reviewers, @@ -126,7 +129,10 @@ # A repair remains scoped to its original PR, but it must also have a finite # attempt budget. This prevents a persistently failing required check from # repeatedly invoking the provider without an operator decision. -MAX_PR_CHECK_REPAIR_ATTEMPTS = 3 +# One run-wide operational budget. It is intentionally not scoped to a +# provider, PR, candidate SHA, or lifecycle phase. +MAX_TOTAL_REPAIR_ROUNDS_PER_RUN = 3 +MAX_PR_CHECK_REPAIR_ATTEMPTS = MAX_TOTAL_REPAIR_ROUNDS_PER_RUN MAX_LOCAL_REPOSITORY_VALIDATION_ATTEMPTS = 3 @@ -427,6 +433,7 @@ def _controlled_interruption_requested(self, state: TransactionState) -> bool: """ return consume_controlled_interruption_hook( self.root, run_id=state.run_id, phase=state.phase, + central_database=self.store.central_database, ) def _recovery_state(self, run_id: str) -> dict[str, object] | None: @@ -539,7 +546,18 @@ def _provider_readiness_gate( agent action requires Codex too. The saved original action lets a verified resume continue exactly where it stopped. """ - missing = provider_readiness_failures(self.root, require_github=require_github) + # The installed deterministic qualification composes a local GitHub + # adapter before the runner is created. It must therefore qualify the + # Managed lifecycle against that adapter, rather than demand an + # unrelated interactive GitHub session from the CI runner. + qualification_local_github = ( + os.environ.get("EP_QUALIFICATION_DETERMINISTIC_FLOW") == "1" + and os.environ.get("EP_QUALIFICATION_GITHUB_WRITE_FLOW") != "1" + ) + missing = provider_readiness_failures( + self.root, + require_github=require_github and not qualification_local_github, + ) if not require_codex: missing = tuple(provider for provider in missing if provider != "CODEX") if missing: @@ -984,7 +1002,12 @@ def _record_repair_audit(self, state: TransactionState, *, failed_checks: str, o outcome=outcome, empty_summary="Agent invocation did not return a repair summary.", ) - if state.repair_audit and state.repair_audit[-1].get("iteration") == str(state.repair_iterations) and state.repair_audit[-1].get("outcome") == "planned": + previous = state.repair_audit[-1] if state.repair_audit else None + if previous and previous.get("iteration") == str(state.repair_iterations) and previous.get("outcome") == "planned": + # The reservation identity is immutable. A retry/recovery updates + # the recorded outcome but cannot consume a second repair round. + if "repair_id" in previous: + record.update({key: previous[key] for key in ("repair_id", "origin", "input_candidate_sha", "dispatch_id")}) return replace(state, repair_audit=state.repair_audit[:-1] + (record,)) return replace(state, repair_audit=state.repair_audit + (record,)) @@ -995,10 +1018,12 @@ def _repair_plan(self, state: TransactionState) -> dict[str, str] | None: plan = state.repair_audit[-1] if plan.get("iteration") != str(state.repair_iterations) or plan.get("outcome") != "planned": return None + if "repair_id" in plan and plan["repair_id"] != f"repair:{state.run_id}:{state.repair_iterations}": + return None return plan def _advance_after_repair_agent_result(self, repair: TransactionState, result: AgentResult) -> TransactionState: - """Apply live or recovered Repair success using its persisted plan.""" + """Revalidate and re-review every repaired candidate before delivery.""" plan = self._repair_plan(repair) if plan is None: return self._save_terminal(repair, "BLOCKED", "repair_plan_missing", "Repair result cannot be resumed without its persisted repair plan.") @@ -1010,9 +1035,38 @@ def _advance_after_repair_agent_result(self, repair: TransactionState, result: A self.store.save(repair) if result.terminal_state in {"BLOCKED", "FAILED"}: return self._save_terminal(repair, result.terminal_state, "external_action_required", result.diagnostic) - if result.pull_request != repair.pull_request: + if result.pull_request not in {None, repair.pull_request}: return self._save_terminal(repair, "BLOCKED", "bounded_scope_conflict", "Repair did not preserve the bounded pull request.") - return self._poll(replace(repair, phase="WAIT_FOR_TERMINAL_EVIDENCE", next_action="poll_required_checks"), result) + repaired_result = replace( + result, + branch=result.branch or repair.branch, + pull_request=repair.pull_request, + ) + if repair.execution_mode == "GENESIS": + target = Path(repaired_result.repository_path).expanduser() if repaired_result.repository_path else None + if target is None: + return self._save_terminal(repair, "BLOCKED", "repair_candidate_unavailable", "Genesis repair did not return its local candidate repository." ) + reviewed, repaired_result = self._run_quality_assurance(repair, repaired_result, assurance_root=target) + if reviewed.terminal or reviewed.phase == "REPAIR_AGENT": + return reviewed + return self._reconcile_genesis_result(reviewed, repaired_result) + validated, validation_result = self._run_local_repository_validation(repair, repaired_result) + if validated.terminal: + return validated + if validation_result.terminal_state != "COMPLETE" or self._has_failed_validation_evidence(validation_result): + if validated.repair_iterations >= MAX_TOTAL_REPAIR_ROUNDS_PER_RUN: + return self._save_terminal(validated, "BLOCKED", "repair_budget_exhausted", "Repaired candidate did not pass local validation before the run-wide repair budget was exhausted.") + return self._repair(validated, "local validation failed. Repair the recorded validation findings for the current candidate.") + reviewed, reviewed_result = self._run_quality_assurance( + validated, replace(validation_result, pull_request=repair.pull_request, branch=repair.branch) + ) + if reviewed.terminal or reviewed.phase == "REPAIR_AGENT": + return reviewed + try: + evidence = self.repository.inspect(self.root) + except RunnerError: + return self._save_terminal(reviewed, "BLOCKED", "repair_candidate_unavailable", "Repaired Managed candidate could not be inspected after assurance.") + return self._continue_after_quality_control(reviewed, reviewed_result, evidence) def _record_local_validation_audit(self, state: TransactionState, *, result: AgentResult | None, outcome: str, profile: ValidationProfile) -> TransactionState: """Append one bounded local-validation iteration without sharing PR repair budget.""" @@ -1298,7 +1352,8 @@ def command_boundary(event: str, command_id: str, command: str, exit_code: int | terminal_condition="provider_turn_interrupted", interruption_reason="controlled_qualification_interruption", ) - result = self.agent.invoke(self.root, prompt) + invocation = getattr(self.agent, "validate", self.agent.invoke) if local_validation else self.agent.invoke + result = invocation(self.root, prompt) except KeyboardInterrupt as error: # A managed SIGINT/SIGTERM while the provider is active means no # valid AgentResult exists. Persist the canonical interruption @@ -1491,6 +1546,11 @@ def _invoke_agent_with_timing(self, state: TransactionState, prompt: str, *, rep isinstance(recovery, dict) and recovery.get("state") == "RECOVERED" and recovery.get("lifecycle_phase") == state.phase + # EXECUTE_AGENT contains two distinct product dispatches: + # implementation and the later immutable PR publication. + # A recovered implementation result (necessarily no PR for + # a new run) must never be replayed as publication evidence. + and state.next_action != "publish_first_implementation_pull_request" ): replacement_id = recovery.get("replacement_invocation_id") if ( @@ -1593,7 +1653,7 @@ def _invoke_agent_with_timing(self, state: TransactionState, prompt: str, *, rep def _run_local_repository_validation( self, state: TransactionState, implementation: AgentResult ) -> tuple[TransactionState, AgentResult]: - """Run the bounded, mutable local gate before an implementation PR exists.""" + """Run the read-only local gate before first implementation publication.""" if state.action_intent == "VALIDATION_ONLY": # This gate is a delivery-only boundary. A producer-authorized # qualification run may supply validation evidence but must never @@ -1604,7 +1664,14 @@ def _run_local_repository_validation( # Never rewrite that evidence; new managed prompts are instructed to # stop before PR creation and therefore enter this gate normally. if implementation.pull_request: - return state, implementation + # A PR that predates this assurance contract is preserved as + # immutable lineage evidence. A newly-started run cannot use a + # provider-returned PR to skip the publication gate. + if implementation.pull_request not in {state.implementation_pull_request, state.pull_request}: + return self._save_terminal( + state, "BLOCKED", "implementation_pr_before_assurance", + "A new Managed run returned an implementation pull request before local validation and mandatory assurance.", + ), implementation if not branch: return self._save_terminal( state, "BLOCKED", "local_validation_scope", "Implementation must return one branch and no pull request before local validation." @@ -1614,7 +1681,10 @@ def _run_local_repository_validation( next_action="run_local_repository_validation", local_validation_iterations=0, local_validation_audit=(), ) - for iteration in range(1, MAX_LOCAL_REPOSITORY_VALIDATION_ATTEMPTS + 1): + # The first validation is a measurement, never a corrective provider + # turn. A failed measurement is routed through ``_repair`` by the + # caller, where it consumes the single run-wide repair budget. + for iteration in (1,): try: profile = classify(changed_paths(self.root, "main")) except OSError: @@ -1640,12 +1710,12 @@ def _run_local_repository_validation( write_live_status(self.root, validation, validation.next_action) instruction = f""" -Local repository validation gate — iteration {iteration} of {MAX_LOCAL_REPOSITORY_VALIDATION_ATTEMPTS}: +Local repository validation gate — read-only measurement: - Stay on exactly `{branch}`. Do not merge or change scope. - Diff-derived validation profile: `{profile.tier}`. Required evidence: {"; ".join(profile.commands)}. If the diff is unavailable or scope becomes mixed, use the full required suite. -- You may correct only the bounded production code and its tests, commit and push those corrections, then rerun the required validation. -- If validation still fails, return `WAITING` with a concise safe diagnostic; the host may allow the next bounded iteration. -- Create one draft implementation pull request only after the required local validation passes. Return that same branch and PR number. Never poll remote checks. +- Do not modify files, index, branch, commits, remotes, pull requests, or external state. The host enforces a read-only provider sandbox. +- Execute and report the required controls with concrete validation evidence. Return `COMPLETE` only when they pass; otherwise return `WAITING` or `FAILED` with a concise safe diagnostic. +- Do not create a pull request. First publication is a later host-owned gate after both mandatory reviews pass. """ try: result = self._invoke_agent_with_timing( @@ -1670,33 +1740,25 @@ def _run_local_repository_validation( self.console_detail = error.console_detail validation = self._record_local_validation_audit(validation, result=None, outcome="agent_failed", profile=profile) return self._terminalize_provider_invocation_error(validation, error), implementation - if result.terminal_state in {"BLOCKED", "FAILED"}: - if ( - result.terminal_state == "FAILED" - and not self._is_external_agent_block(result) - and self._has_failed_validation_evidence(result) - ): - validation = self._record_local_validation_audit( - validation, result=result, outcome="validation_failed", profile=profile - ) - if self._is_environmental_validation_instability(result): - return self._save_terminal( - validation, - "BLOCKED", - "validation_infrastructure_recovery_required", - "Required local validation is unstable: a failed required suite and a passing isolated rerun were recorded without an implementation correction. Preserve this run and create a separate validation-infrastructure recovery item.", - ), implementation - continue + # A failed validation command is evidence for the shared repair + # route, not an unavailable validator. Only a provider-blocked + # invocation is terminal at this read-only gate. + if result.terminal_state == "BLOCKED": validation = self._record_local_validation_audit(validation, result=result, outcome="agent_failed", profile=profile) - return self._save_terminal(validation, result.terminal_state, "local_repository_validation_failed", result.diagnostic or "Local repository validation failed."), implementation + return validation, result if result.branch and result.branch != branch: validation = self._record_local_validation_audit(validation, result=result, outcome="agent_failed", profile=profile) return self._save_terminal(validation, "BLOCKED", "local_validation_scope", "Local validation changed the bounded implementation branch."), implementation if result.pull_request: + validation = self._record_local_validation_audit(validation, result=result, outcome="validated", profile=profile) + return self._save_terminal( + validation, "BLOCKED", "implementation_pr_before_assurance", + "Read-only local validation returned a pull request before mandatory assurance.", + ), implementation + if result.terminal_state == "COMPLETE" and result.validation_evidence and not self._has_failed_validation_evidence(result): validation = self._record_local_validation_audit(validation, result=result, outcome="validated", profile=profile) return validation, replace( - result, - branch=branch, + result, branch=branch, pull_request=None, validation_evidence=implementation.validation_evidence + result.validation_evidence, ) validation = self._record_local_validation_audit(validation, result=result, outcome="validation_failed", profile=profile) @@ -1707,87 +1769,231 @@ def _run_local_repository_validation( "validation_infrastructure_recovery_required", "Required local validation is unstable: a failed required suite and a passing isolated rerun were recorded without an implementation correction. Preserve this run and create a separate validation-infrastructure recovery item.", ), implementation - return self._save_terminal(validation, "BLOCKED", "local_validation_attempt_limit_reached", "Required local repository validation did not pass after 3 bounded iterations."), implementation + return validation, result - def _run_autonomous_quality_control( - self, state: TransactionState, implementation: AgentResult + def _run_quality_assurance( + self, state: TransactionState, implementation: AgentResult, *, assurance_root: Path | None = None, ) -> tuple[TransactionState, AgentResult]: - """Run the required post-implementation refactor and quality boundary. + """Run independent, sandboxed quality and security reviews. - The controller is autonomous but cannot widen delivery scope: it may - amend only the current transaction branch and its existing PR. + This deliberately replaces the former mutating "quality control" + provider turn. Reviewers receive a pinned candidate and use the + provider's read-only sandbox; only ``_repair`` can subsequently + mutate the bounded branch. """ quality = replace( state, phase="QUALITY_CONTROL_AGENT", branch=implementation.branch or state.branch, pull_request=implementation.pull_request or state.pull_request, - next_action="autonomous_refactor_and_quality_control", + next_action="quality_and_security_review", ) + candidate_root = assurance_root or self.root + try: + candidate = self._inspect_assurance_candidate(candidate_root, state.execution_mode) + except RunnerError: + return self._save_terminal(quality, "BLOCKED", "assurance_candidate_unavailable", "The assurance candidate could not be inspected."), implementation + if not candidate.clean or not re.fullmatch(r"[0-9a-f]{40}", candidate.head_sha): + return self._save_terminal(quality, "BLOCKED", "assurance_candidate_invalid", "Quality assurance requires one clean, pinned candidate."), implementation + profile_version = f"validation-profile@{VALIDATION_PROFILE_VERSION}" + criteria = Path(quality.prompt_path).read_text(encoding="utf-8") + criteria_digest = "sha256:" + hashlib.sha256(criteria.encode("utf-8")).hexdigest() + # Candidate identity and policy identity are intentionally independent: + # a source revision must not silently select or weaken its own policy. + profile_digest = "sha256:" + hashlib.sha256( + json.dumps({"baseline": profile_version, "criteria_digest": criteria_digest}, sort_keys=True).encode("utf-8") + ).hexdigest() + quality = replace(quality, assurance_profile={ + "version": profile_version, "digest": profile_digest, "candidate_sha": candidate.head_sha, + "criteria_digest": criteria_digest, + }) self.store.save(quality) write_live_status(self.root, quality, quality.next_action) - prompt = assemble_prompt( - Path(quality.prompt_path), quality, - managed_target=self.root if quality.execution_mode == "MANAGED" else None, - ) + """ - -Mandatory autonomous refactor and quality-control stage: -- Inspect the implementation now present on this transaction branch. -- Autonomously make only demonstrable maintainability, clarity, safety, or - test-coverage improvements within the original bounded objective. -- Assess test coverage for every changed behavior. Add or strengthen focused - regression tests whenever existing coverage does not prove that behavior. -- Assess the applicable operator, contract, and implementation documentation. - Update it whenever the bounded change affects documented behavior; only - leave documentation unchanged when the inspection proves it is unaffected. -- Run the relevant focused validation, including the added or affected tests, - and `git diff --check`. -- Preserve the existing transaction branch and pull request. If changes are - needed, commit and push them to that same branch; do not create another PR, - merge, alter authority, or expand scope. -- Return the same pull-request number and branch after the quality boundary. -- In quality_evidence, record only work actually performed in this stage. Use - activity values REFACTOR, TEST_COVERAGE, DOCUMENTATION, VALIDATION, or - NO_CHANGE_REQUIRED and a short safe result for each. Do not include raw - commands, output, prompts, source content, paths, secrets, or reasoning. -""" + evidence = ReviewerEvidence.from_repository(quality.run_id, quality.execution_mode, candidate) + selections = tuple( + ReviewerSelection(role, f"mandatory post-implementation {role} assurance", 1.0) + for role in ("quality", "security") + ) + records: list[dict[str, object]] = [] + # Run sequentially: each is still a distinct sandboxed invocation, and + # this avoids sharing mutable CLI telemetry between parallel calls. + for selection in selections: + assurance_objective = ( + f"Mandatory {selection.reviewer} assurance. Profile {profile_version} ({profile_digest}); " + f"candidate {candidate.head_sha}. Report only concrete, bounded findings against the action acceptance criteria. " + + Path(quality.prompt_path).read_text(encoding="utf-8") + ) + started_at = datetime.now(timezone.utc).isoformat() + result = run_reviews(assurance_root or self.root, (selection,), assurance_objective, self.agent if hasattr(self.agent, "review") else None, evidence=evidence)[0] + completed_at = datetime.now(timezone.utc).isoformat() + try: + unchanged = self._inspect_assurance_candidate(candidate_root, state.execution_mode) + except RunnerError: + unchanged = None + supplied_findings = mandatory_findings(result) + status = "UNRESOLVED" if supplied_findings is None or unchanged is None or not unchanged.clean or unchanged.head_sha != candidate.head_sha else "PASS" + findings = [ + { + "id": f"{quality.run_id}:{selection.reviewer}:{len(quality.assurance_reviews) + len(records) + 1}:{item['id']}", + "fingerprint": hashlib.sha256(json.dumps(item, sort_keys=True).encode("utf-8")).hexdigest()[:32], + "category": item["category"], "criterion": item["criterion"], + "observation": item["observation"], "severity": item["severity"], "confidence": item["confidence"], + "blocking": item["severity"] in {"HIGH", "CRITICAL"}, + "disposition": "OPEN" if item["severity"] in {"HIGH", "CRITICAL"} else "NON_BLOCKING", + "evidence_ref": item["evidence_ref"], + } + for item in (supplied_findings or ()) + ] + if status == "PASS" and any(finding["blocking"] for finding in findings): + status = "FAIL" + records.append({ + "reviewer": selection.reviewer, "status": status, "candidate_sha": candidate.head_sha, + "profile_digest": profile_digest, "invocation_id": f"{quality.run_id}:{selection.reviewer}:{len(quality.assurance_reviews) + len(records)}", + "findings": findings, "contract_version": MANDATORY_REVIEW_OUTPUT_CONTRACT_VERSION, + "started_at": started_at, "completed_at": completed_at, + }) + quality = replace(quality, assurance_reviews=quality.assurance_reviews + tuple(records)) + # A later PASS does not erase an earlier blocker. Only a completed + # reserved repair followed by this exact re-review can append the + # linked resolution evidence; the original finding remains immutable. + if all(record["status"] == "PASS" for record in records) and quality.repair_audit: + repair = quality.repair_audit[-1] + if repair.get("outcome") == "submitted_for_recheck" and repair.get("repair_id"): + resolved = {item["finding_id"] for item in quality.assurance_resolutions} + prior_blockers = [ + finding for review in quality.assurance_reviews[:-len(records)] + for finding in review.get("findings", []) + if isinstance(finding, dict) and finding.get("blocking") and finding.get("disposition") == "OPEN" + and isinstance(finding.get("id"), str) and finding["id"] not in resolved + ] + if prior_blockers: + review_refs = ",".join(str(record["invocation_id"]) for record in records) + resolutions = tuple({ + "finding_id": str(finding["id"]), "disposition": "RESOLVED", + "resolution_ref": f"{repair['repair_id']}|{review_refs}", + "candidate_sha": candidate.head_sha, + } for finding in prior_blockers) + quality = replace(quality, assurance_resolutions=quality.assurance_resolutions + resolutions) + self.store.save(quality) + unresolved = [record for record in records if record["status"] == "UNRESOLVED"] + if unresolved: + return self._save_terminal(quality, "BLOCKED", "mandatory_assurance_unresolved", "A required quality or security review was unavailable, malformed, or candidate-mismatched."), implementation + findings = [finding for record in records for finding in record["findings"]] + blockers = [finding for finding in findings if finding["blocking"] and finding["disposition"] == "OPEN"] + if blockers: + if quality.repair_iterations >= MAX_TOTAL_REPAIR_ROUNDS_PER_RUN: + return self._save_terminal(quality, "BLOCKED", "repair_budget_exhausted", "Mandatory assurance blockers remain after the run-wide repair budget was exhausted."), implementation + blocker_roles = sorted({str(record["reviewer"]) for record in records if any( + finding["blocking"] and finding["disposition"] == "OPEN" for finding in record["findings"] + )}) + summary = "; ".join( + f"{finding['id']}: {finding['criterion']} — {finding['observation']}" + for finding in blockers + ) + repaired = self._repair(quality, f"{'/'.join(blocker_roles)} review findings failed. Repair these bounded findings: {summary}") + return repaired, implementation + return quality, implementation + + def _inspect_assurance_candidate(self, root: Path, execution_mode: str) -> RepositoryEvidence: + """Return a pinned review candidate without weakening Managed evidence. + + Genesis deliberately supports a local-only repository. Its assurance + candidate must therefore be inspectable without Managed's canonical + bootstrap document or an ``origin`` remote. + """ + if execution_mode != "GENESIS": + return self.repository.inspect(root) + if not (root / ".git").exists(): + # Test/dedicated adapters may provide target evidence directly. + return self.repository.inspect(root) + provider = getattr(self.repository, "provider", GitProvider()) try: - result = self._invoke_agent_with_timing(quality, prompt, quality=True) - quality = self._record_agent_execution_time(quality) - quality = self._record_validation_evidence(quality, result) - quality = replace(quality, quality_evidence=result.quality_evidence) - quality = self._record_verified_result_commit( - quality, - result, - phase="QUALITY_CONTROL_AGENT", - description="quality_control_commit_verified", - ) - self._persist_agent_usage(quality.run_id) - except ProviderReadinessBlocked as blocked: - return blocked.state, implementation - except CodexInvocationError as error: - quality = self._record_agent_execution_time(quality) - self.console_detail = error.console_detail - return self._terminalize_provider_invocation_error(quality, error), implementation - return self._advance_after_quality_control_agent_result(quality, implementation, result) + branch = provider.command(root, "git", "branch", "--show-current") + head_sha = provider.command(root, "git", "rev-parse", "HEAD") + clean = not provider.command(root, "git", "status", "--porcelain", "--untracked-files=all") + except RuntimeError as error: + raise RunnerError(str(error)) from error + return RepositoryEvidence(root.name, branch, head_sha, clean, True) - def _advance_after_quality_control_agent_result( - self, quality: TransactionState, implementation: AgentResult, result: AgentResult, - ) -> tuple[TransactionState, AgentResult]: - """Apply live or recovered QC success without provider-session state.""" - if result.terminal_state in {"BLOCKED", "FAILED"}: - return self._save_terminal(quality, result.terminal_state, "autonomous_quality_control_failed", result.diagnostic or "Autonomous quality control did not complete."), implementation - if implementation.pull_request and result.pull_request and result.pull_request != implementation.pull_request: - return self._save_terminal(quality, "BLOCKED", "autonomous_quality_control_scope", "Autonomous quality control returned a different pull request."), implementation - if implementation.branch and result.branch and result.branch != implementation.branch: - return self._save_terminal(quality, "BLOCKED", "autonomous_quality_control_scope", "Autonomous quality control returned a different branch."), implementation - return quality, replace( - implementation, - branch=result.branch or implementation.branch, - pull_request=result.pull_request or implementation.pull_request, - validation_evidence=implementation.validation_evidence + result.validation_evidence, + @staticmethod + def _current_assurance_passes(state: TransactionState) -> bool: + """Require the two mandatory reviews for this exact profile/candidate.""" + profile = state.assurance_profile + if not isinstance(profile, dict): + return False + candidate, digest = profile.get("candidate_sha"), profile.get("digest") + if not isinstance(candidate, str) or not isinstance(digest, str): + return False + current = [ + review for review in state.assurance_reviews + if review.get("candidate_sha") == candidate and review.get("profile_digest") == digest + ] + return all( + any(review.get("reviewer") == role and review.get("status") == "PASS" for review in current) + for role in ("quality", "security") + ) and not any( + finding.get("blocking") and finding.get("disposition") == "OPEN" + for review in current for finding in review.get("findings", []) + if isinstance(finding, dict) ) + def _publish_first_implementation_pull_request( + self, state: TransactionState, implementation: AgentResult, + ) -> tuple[TransactionState, AgentResult]: + """Publish exactly one new Managed implementation PR after assurance. + + This is deliberately a host gate rather than a sentence attached to a + previous provider prompt. The provider receives no authority to edit + the already reviewed candidate; the host pins its SHA before and after + the PR hand-off. + """ + if state.execution_mode == "GENESIS" or state.pull_request or implementation.pull_request: + return state, implementation + if not self._current_assurance_passes(state): + return self._save_terminal(state, "BLOCKED", "implementation_publication_assurance_required", "First implementation PR publication requires current passing quality and security assurance."), implementation + try: + before = self.repository.inspect(self.root) + except RunnerError: + return self._save_terminal(state, "BLOCKED", "implementation_publication_candidate_unavailable", "Reviewed Managed candidate is unavailable for PR publication."), implementation + profile = state.assurance_profile or {} + if not before.clean or before.branch != implementation.branch or before.head_sha != profile.get("candidate_sha"): + return self._save_terminal(state, "BLOCKED", "implementation_publication_candidate_changed", "The reviewed candidate changed before first PR publication."), implementation + publication = replace(state, phase="EXECUTE_AGENT", next_action="publish_first_implementation_pull_request") + self.store.save(publication) + prompt = assemble_prompt(Path(publication.prompt_path), publication, managed_target=self.root) + """ + +First implementation pull-request publication gate: +- The Execution Host has already recorded passing local validation plus independent quality and security assurance for the exact current candidate. +- Do not edit files, index, commits, branch, tests, configuration, or evidence. Do not merge, release, or change repository settings. +- Create exactly one draft implementation pull request for the existing bounded branch and current HEAD. Return that existing branch, the GitHub pull-request number and the unchanged current commit SHA. +""" + try: + published = self._invoke_agent_with_timing(publication, prompt) + publication = self._record_agent_execution_time(publication) + except (CodexInvocationError, ProviderReadinessBlocked) as error: + if isinstance(error, ProviderReadinessBlocked): + return error.state, implementation + return self._terminalize_provider_invocation_error(publication, error), implementation + try: + after = self.repository.inspect(self.root) + except RunnerError: + after = None + failures = [] + if published.terminal_state != "COMPLETE": failures.append("provider_not_complete") + if not published.pull_request: failures.append("missing_pull_request") + if published.branch != before.branch: failures.append("branch_mismatch") + if published.commit_sha != before.head_sha: failures.append("candidate_sha_mismatch") + if after is None: failures.append("candidate_unavailable_after_publication") + elif not after.clean: failures.append("candidate_dirty_after_publication") + elif after.branch != before.branch: failures.append("branch_changed_after_publication") + elif after.head_sha != before.head_sha: failures.append("candidate_changed_after_publication") + if failures: + return self._save_terminal( + publication, "BLOCKED", "implementation_publication_evidence_invalid", + "First implementation PR publication changed or failed to identify the reviewed candidate: " + ", ".join(failures) + ".", + ), implementation + return publication, published + def _reject_historical_agent_pull_request( self, state: TransactionState ) -> TransactionState | None: @@ -1837,6 +2043,12 @@ def _advance_after_primary_agent_result( ) -> TransactionState: """Shared post-provider transition for live and recovered results.""" if state.execution_mode == "GENESIS": + target = Path(result.repository_path).expanduser() if result.repository_path else None + if not target or not target.is_absolute() or target_repository_authorization(self.root, target): + return self._reconcile_genesis_result(state, result) + state, result = self._run_quality_assurance(state, result, assurance_root=target) + if state.terminal or state.phase in {"REPAIR_AGENT", "WAIT_FOR_TERMINAL_EVIDENCE", "WAIT_FOR_OPERATOR_MERGE"}: + return state return self._reconcile_genesis_result(state, result) recoverable_local_failure = self._is_recoverable_implementation_validation_failure(state, result) if state.transaction_kind == "IMPLEMENTATION" and state.action_intent == "MUTATING_DELIVERY" and ( @@ -1846,9 +2058,19 @@ def _advance_after_primary_agent_result( state, result = self._run_local_repository_validation(state, result) if state.terminal: return state - state, result = self._run_autonomous_quality_control(state, result) + if result.terminal_state != "COMPLETE" or self._has_failed_validation_evidence(result): + if state.repair_iterations >= MAX_TOTAL_REPAIR_ROUNDS_PER_RUN: + return self._save_terminal(state, "BLOCKED", "repair_budget_exhausted", "Local validation requires a repair after the run-wide repair budget was exhausted.") + return self._repair(state, "local validation failed. Repair the recorded validation findings for the current candidate.") + state, result = self._run_quality_assurance(state, result) if state.terminal: return state + if state.phase in {"REPAIR_AGENT", "WAIT_FOR_TERMINAL_EVIDENCE", "WAIT_FOR_OPERATOR_MERGE"}: + return state + if state.owner_authorized: + state, result = self._publish_first_implementation_pull_request(state, result) + if state.terminal: + return state return self._continue_after_quality_control(state, result, evidence) def _continue_after_quality_control( @@ -1884,9 +2106,10 @@ def _advance_after_recovered_provider_result( if lifecycle_phase == "EXECUTE_AGENT": return self._advance_after_primary_agent_result(state, result, evidence) if lifecycle_phase == "QUALITY_CONTROL_AGENT": - implementation = AgentResult("COMPLETE", branch=state.branch, pull_request=state.pull_request) - quality, implementation = self._advance_after_quality_control_agent_result(state, implementation, result) - return quality if quality.terminal else self._continue_after_quality_control(quality, implementation, evidence) + # The assurance phase has no mutating provider invocation to + # recover. A legacy interrupted quality-control run remains + # evidence-incomplete rather than being reinterpreted as PASS. + return self._save_terminal(state, "BLOCKED", "legacy_quality_recovery_unresolved", "Interrupted legacy quality control cannot satisfy the required read-only assurance.") if lifecycle_phase == "REPAIR_AGENT": return self._advance_after_repair_agent_result(state, result) if lifecycle_phase == "FINALIZE_AGENT": @@ -1973,7 +2196,11 @@ def run( action_intent="MUTATING_DELIVERY", ) return self._save_terminal(state, "BLOCKED", "execution_context_resolution", str(error)) - evidence = self.repository.inspect(self.root) + # Genesis explicitly supports a local-only host and target. Do not + # inspect it through the Managed repository client before the mode is + # known: that client correctly requires ``origin`` for Managed, but + # would make Genesis impossible before its own local-only preflight. + evidence = self._inspect_assurance_candidate(self.root, context.execution_mode) if state is not None: if state.repository != evidence.repository or Path(state.prompt_path) != prompt_path: raise RunnerError("checkpoint conflicts with current repository or prompt") @@ -2040,6 +2267,12 @@ def run( state = replace(state, action_intent=context.action_intent) # Establish canonical transaction identity before persisting readiness evidence. self.store.save(state) + qualification_control_wait = getattr(self.agent, "wait_for_controlled_interruption_arm", None) + if callable(qualification_control_wait) and state.phase == "INITIALIZE": + # Only the deterministic installed-qualification adapter exposes + # this bounded rendezvous. Production adapters have no such + # method, so an operator control can never delay normal work. + qualification_control_wait(self.root, state) # This envelope is deliberately persisted once and can be resumed # after process restart. It is excluded from bottleneck ranking. self._total_phase = self._resume_phase( @@ -2707,7 +2940,14 @@ def _poll(self, state: TransactionState, result: AgentResult | None = None) -> T return self._save_operator_merge_wait(waiting) def _repair(self, state: TransactionState, objective: str) -> TransactionState: + if state.repair_iterations >= MAX_TOTAL_REPAIR_ROUNDS_PER_RUN: + return self._save_terminal( + state, "BLOCKED", "repair_budget_exhausted", + "The run-wide maximum of three correction rounds has been consumed.", + ) failed_checks = objective.split(" failed.", 1)[0] + origin = next((origin for origin in ("validation", "quality", "security", "hosted", "finalization") if origin in objective.casefold()), "validation") + input_candidate = state.last_verified_sha or (state.assurance_profile or {}).get("candidate_sha") or "not_recorded" repair = replace( state, phase="REPAIR_AGENT", @@ -2720,6 +2960,14 @@ def _repair(self, state: TransactionState, objective: str) -> TransactionState: repair = self._record_repair_audit( repair, failed_checks=failed_checks, objective=objective, result=None, outcome="planned", ) + reservation = dict(repair.repair_audit[-1]) + reservation.update({ + "repair_id": f"repair:{repair.run_id}:{repair.repair_iterations}", + "origin": origin, + "input_candidate_sha": input_candidate, + "dispatch_id": f"{repair.run_id}:repair:{repair.repair_iterations}", + }) + repair = replace(repair, repair_audit=repair.repair_audit[:-1] + (reservation,)) self.store.save(repair) write_live_status(self.root, repair, repair.next_action) try: @@ -2881,6 +3129,14 @@ def _advance_after_finalization_agent_result( "finalization_pr_required", result.diagnostic or "Finalization pull request was not created.", ) + expected_branch = finalization.finalization_branch or finalization.branch + if not expected_branch or result.branch != expected_branch: + return self._save_terminal( + finalization, + "BLOCKED", + "finalization_branch_mismatch", + "Finalization returned a pull request outside the durable Finalization branch.", + ) finalization = replace( finalization, phase="WAIT_FOR_TERMINAL_EVIDENCE", diff --git a/src/engineering_platform/execution_lifecycle.py b/src/engineering_platform/execution_lifecycle.py index 45ff5c6..6eb46a4 100644 --- a/src/engineering_platform/execution_lifecycle.py +++ b/src/engineering_platform/execution_lifecycle.py @@ -382,6 +382,13 @@ def belongs_to_step(step_id: str, phase_id: object, parent_phase_id: object, pha evidence = checkpoint.get("quality_evidence") if isinstance(evidence, (list, tuple)) and evidence: step["quality_evidence"] = list(evidence) + reviews = checkpoint.get("assurance_reviews") + if isinstance(reviews, (list, tuple)) and reviews: + step["assurance_reviews"] = list(reviews) + step["repair_rounds"] = { + "used": _nonnegative_int(checkpoint.get("repair_iterations")), + "maximum": 3, + } if step_id == "LOCAL_REPOSITORY_VALIDATION" and step["state"] not in {"PENDING", "SKIPPED"}: audit = checkpoint.get("local_validation_audit") if isinstance(audit, (list, tuple)) and audit: @@ -444,4 +451,5 @@ def belongs_to_step(step_id: str, phase_id: object, parent_phase_id: object, pha "required_validation_state": qualification.get("required_validation_state", "UNAVAILABLE"), "run_qualification": qualification.get("run_qualification", "UNAVAILABLE"), }, + "repair_rounds": {"used": _nonnegative_int(checkpoint.get("repair_iterations")), "maximum": 3}, } diff --git a/src/engineering_platform/lifecycle_worker.py b/src/engineering_platform/lifecycle_worker.py index 8af9014..dca5602 100644 --- a/src/engineering_platform/lifecycle_worker.py +++ b/src/engineering_platform/lifecycle_worker.py @@ -104,7 +104,10 @@ def eligible_submission_ids(self) -> list[str]: (prior.state IN ('CLAIMED','RUNNING') AND prior.submission_id!=s.submission_id) OR (prior.state IN ('BLOCKED','FAILED') AND prior.operator_resolution='OPEN' AND prior.submission_id!=s.submission_id) - OR (prior.operator_resolution='RETRIED' AND prior.resolution_submission_id!=s.submission_id) + OR (prior.operator_resolution='RETRIED' AND prior.resolution_submission_id!=s.submission_id + AND NOT EXISTS (SELECT 1 FROM ep_parity_lifecycle_dispatches retry + WHERE retry.submission_id=prior.resolution_submission_id + AND retry.operator_resolution='RETRIED')) ) ) ORDER BY s.project_id, diff --git a/src/engineering_platform/parity_context.py b/src/engineering_platform/parity_context.py index 59d4f6b..2754c1a 100644 --- a/src/engineering_platform/parity_context.py +++ b/src/engineering_platform/parity_context.py @@ -90,10 +90,10 @@ def console_queue_projection(self, *, limit: int = 25) -> dict[str, object]: it; terminal records remain durable history rather than queue items. """ rows = self.connection.execute( - """SELECT s.submission_id,s.transport,s.producer_type,s.created_at + """SELECT s.submission_id,s.transport,s.producer_type,s.created_at,s.state,s.disposition_revision FROM ep_submissions s LEFT JOIN ep_parity_lifecycle_dispatches d ON d.submission_id=s.submission_id - WHERE s.project_id=? AND s.state='QUEUED' AND s.admission='ADMITTED' + WHERE s.project_id=? AND s.state IN ('QUEUED','DEFERRED','QUARANTINED') AND s.admission='ADMITTED' AND d.submission_id IS NULL ORDER BY s.created_at,s.submission_id""", (self.context.project_id,), @@ -108,6 +108,8 @@ def console_queue_projection(self, *, limit: int = 25) -> dict[str, object]: "modified_at": str(row[3]), "queue_source": "CENTRAL", "transport": str(row[1]), + "queue_state": str(row[4]), + "disposition_revision": int(row[5]), } for row in rows[:limit] ] diff --git a/src/engineering_platform/parity_lifecycle_dispatcher.py b/src/engineering_platform/parity_lifecycle_dispatcher.py index 280b329..5277514 100644 --- a/src/engineering_platform/parity_lifecycle_dispatcher.py +++ b/src/engineering_platform/parity_lifecycle_dispatcher.py @@ -191,6 +191,20 @@ def _record_provider_free_admission( def _default_runner(repository_root: Path, *, central_database: Path | None = None) -> EngineeringRunner: """Construct the installed historical runner without a watcher or Agent.""" + if os.environ.get("EP_QUALIFICATION_DETERMINISTIC_FLOW") == "1": + from .qualification_runtime import DeterministicQualificationAgent, LocalQualificationGitHub + github: object = LocalQualificationGitHub(repository_root) + if os.environ.get("EP_QUALIFICATION_GITHUB_WRITE_FLOW") == "1": + remote = GitProvider().execute(repository_root, "git", "remote", "get-url", "origin") + match = re.search(r"github\.com[/:]([^/]+/[^/]+?)(?:\.git)?$", remote.stdout.strip()) + expected = os.environ.get("EP_QUALIFICATION_GITHUB_REPOSITORY") + if remote.returncode == 0 and match is not None and match.group(1) == expected: + github = GhCliClient(repository=match.group(1)) + return EngineeringRunner( + repository_root, + StateStore(repository_root / ".engineering" / "engineering-runs", central_database=central_database, emit_local_projection=False), + SubprocessRepositoryClient(), github, DeterministicQualificationAgent(), + ) remote = GitProvider().execute(repository_root, "git", "remote", "get-url", "origin") match = re.search(r"github\.com[/:]([^/]+/[^/]+?)(?:\.git)?$", remote.stdout.strip()) repository = match.group(1) if remote.returncode == 0 and match else None @@ -279,7 +293,10 @@ def _claim(self, submission_id: str) -> tuple[ParityProjectContext, HistoricalCa WHERE project_id=? AND ( state IN ('CLAIMED','RUNNING') OR (state IN ('BLOCKED','FAILED') AND operator_resolution='OPEN') - OR (operator_resolution='RETRIED' AND resolution_submission_id!=?) + OR (operator_resolution='RETRIED' AND resolution_submission_id!=? + AND NOT EXISTS (SELECT 1 FROM ep_parity_lifecycle_dispatches retry + WHERE retry.submission_id=ep_parity_lifecycle_dispatches.resolution_submission_id + AND retry.operator_resolution='RETRIED')) ) LIMIT 1""", (context.project_id, submission_id), ).fetchone() @@ -482,7 +499,12 @@ def dispatch(self, submission_id: str) -> DispatchReceipt: return DispatchReceipt(submission_id, context.project_id, context.repository_id, run_id, "RUNNING", duplicate) try: with _historical_admission_environment(repository_root, self.data_root): - if not duplicate: + # INITIALIZE_ONLY qualification deliberately allocates the + # canonical dispatch before writing the runner input. A + # later normal resume refers to that same dispatch, but must + # still materialize the input exactly once before invoking + # the runner. + if not duplicate or not prompt.is_file(): self._persist_historical_input(repository_root, candidate, run_id, prompt) runner = self.runner_factory(repository_root) state = runner.run( diff --git a/src/engineering_platform/platform_bootstrap.py b/src/engineering_platform/platform_bootstrap.py index 3a2c883..ec2b351 100644 --- a/src/engineering_platform/platform_bootstrap.py +++ b/src/engineering_platform/platform_bootstrap.py @@ -356,6 +356,22 @@ def _provision_workspace_paths(root: Path, workspace: Path) -> dict[str, Path]: paths["runs"] = workspace / "engineering-runs" for path in paths.values(): path.mkdir(mode=0o700, parents=True, exist_ok=True) + # A shared runtime workspace may be represented in a checkout by the + # platform-owned `.engineering` symlink. Keep that implementation detail + # out of Git's untracked-worktree evidence; tracked project files are + # unaffected by an info/exclude rule. + local_workspace = root / WORKSPACE_DIRECTORY + git_directory = root / ".git" + if local_workspace.is_symlink() and git_directory.is_dir() and local_workspace.resolve() == workspace.resolve(): + exclude = git_directory / "info" / "exclude" + try: + existing = exclude.read_text(encoding="utf-8") if exclude.exists() else "" + if ".engineering" not in existing.splitlines(): + exclude.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + prefix = existing.rstrip("\n") + exclude.write_text((prefix + "\n" if prefix else "") + ".engineering\n", encoding="utf-8") + except OSError: + pass return paths diff --git a/src/engineering_platform/platform_version.py b/src/engineering_platform/platform_version.py index 6268be2..603f92f 100644 --- a/src/engineering_platform/platform_version.py +++ b/src/engineering_platform/platform_version.py @@ -12,7 +12,7 @@ SEMVER = re.compile(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$") CONTRACT = re.compile(r"^(\d{4})\.(0[1-9]|1[0-2])$") -CURRENT_PLATFORM_VERSION = "2.1.6" +CURRENT_PLATFORM_VERSION = "2.3.0" MANIFEST_FIELDS = frozenset( { "platform_version", diff --git a/src/engineering_platform/provider_recovery.py b/src/engineering_platform/provider_recovery.py index 94f957d..abb3a83 100644 --- a/src/engineering_platform/provider_recovery.py +++ b/src/engineering_platform/provider_recovery.py @@ -27,7 +27,10 @@ }) ACTIVE_RECOVERY_STATES = frozenset({"RECOVERY_AVAILABLE", "RECOVERY_STARTING", "RECOVERY_IN_PROGRESS"}) TERMINAL_RECOVERY_STATES = RECOVERY_STATES - ACTIVE_RECOVERY_STATES -CONTROLLED_INTERRUPTION_PHASES = frozenset({"QUALITY_CONTROL_AGENT"}) +# Quality assurance is now a read-only reviewer pipeline, not a provider turn. +# The only active provider boundary that can be interrupted and recovered is +# the implementation invocation. +CONTROLLED_INTERRUPTION_PHASES = frozenset({"EXECUTE_AGENT"}) CONTROL_DIRECTORY = Path(".engineering/artifacts/provider-recovery-fault-injection") @@ -64,24 +67,30 @@ def controlled_interruption_status(root: Path, *, run_id: str, phase: str) -> st return "ARMED" if armed.is_file() else "NOT_ARMED" -def _validate_control_target(root: Path, *, run_id: str, phase: str) -> object: +def _validate_control_target(root: Path, *, run_id: str, phase: str, + central_database: Path | None = None) -> object: if phase not in CONTROLLED_INTERRUPTION_PHASES: raise ControlledInterruptionControlError("phase is not supported for controlled interruption") try: - state = StateStore(root / ".engineering" / "engineering-runs").load(run_id) + state = StateStore( + root / ".engineering" / "engineering-runs", + central_database=central_database, + emit_local_projection=False, + ).load(run_id) except StateError as error: raise ControlledInterruptionControlError(str(error)) from error if state.terminal: raise ControlledInterruptionControlError("run is terminal") # The hook is deliberately offered only before its lifecycle boundary. A # phase already entered may have an active provider that cannot be raced. - if state.phase not in PHASES or state.phase in {"QUALITY_CONTROL_AGENT", "REPAIR_AGENT", "FINALIZE_AGENT", "RECONCILE_AGENT", "WAIT_FOR_TERMINAL_EVIDENCE", "WAIT_FOR_OPERATOR_MERGE", "REPOSITORY_CLEANUP"}: + if state.phase not in PHASES or state.phase in {"EXECUTE_AGENT", "QUALITY_CONTROL_AGENT", "REPAIR_AGENT", "FINALIZE_AGENT", "RECONCILE_AGENT", "WAIT_FOR_TERMINAL_EVIDENCE", "WAIT_FOR_OPERATOR_MERGE", "REPOSITORY_CLEANUP"}: raise ControlledInterruptionControlError("target phase is already active or has passed") return state -def arm_controlled_interruption(root: Path, *, run_id: str, phase: str, armed_by: str | None = None, reason: str | None = None) -> dict[str, object]: - _validate_control_target(root, run_id=run_id, phase=phase) +def arm_controlled_interruption(root: Path, *, run_id: str, phase: str, armed_by: str | None = None, + reason: str | None = None, central_database: Path | None = None) -> dict[str, object]: + _validate_control_target(root, run_id=run_id, phase=phase, central_database=central_database) armed, consumed = _control_paths(root, run_id, phase) if consumed.is_file(): raise ControlledInterruptionControlError("controlled interruption is already consumed") @@ -112,7 +121,8 @@ def disarm_controlled_interruption(root: Path, *, run_id: str, phase: str) -> st return "DISARMED" -def consume_controlled_interruption_hook(root: Path, *, run_id: str, phase: str) -> bool: +def consume_controlled_interruption_hook(root: Path, *, run_id: str, phase: str, + central_database: Path | None = None) -> bool: """Durably consume the explicit qualification-only interruption hook. The marker is a run-bound artifact rather than a provider recovery row: @@ -125,7 +135,7 @@ def consume_controlled_interruption_hook(root: Path, *, run_id: str, phase: str) requested = os.environ.get("ENGINEERING_PLATFORM_TEST_INTERRUPT_PROVIDER_ONCE") armed, path = _control_paths(root, run_id, phase) durable_armed = armed.is_file() - if (requested != f"{run_id}:{phase}" and not durable_armed) or load_recovery_state(root, run_id) is not None: + if (requested != f"{run_id}:{phase}" and not durable_armed) or load_recovery_state(root, run_id, central_database=central_database) is not None: return False artifact_id = f"provider-recovery-fault-injection:{run_id}:{phase}" directory = root / CONTROL_DIRECTORY @@ -159,6 +169,7 @@ def consume_controlled_interruption_hook(root: Path, *, run_id: str, phase: str) root, path, artifact_id=artifact_id, artifact_type="CONTROLLED_PROVIDER_INTERRUPTION", content_type="application/json", created_at=str(payload["consumed_at"]), run_id=run_id, + central_database=central_database, ) except Exception: # The exclusive marker remains intentionally: after an uncertain @@ -174,11 +185,16 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument("--run-id", required=True) parser.add_argument("--phase", required=True) parser.add_argument("--reason") + parser.add_argument("--central-database", type=Path, + help="Explicit CENTRAL lifecycle database for an installed Server run.") args = parser.parse_args(argv) root = args.repo.resolve() try: if args.command == "arm-controlled-interruption": - payload = arm_controlled_interruption(root, run_id=args.run_id, phase=args.phase, reason=args.reason) + payload = arm_controlled_interruption( + root, run_id=args.run_id, phase=args.phase, reason=args.reason, + central_database=args.central_database, + ) print(json.dumps({"status": "ARMED", **payload}, sort_keys=True)) elif args.command == "controlled-interruption-status": print(json.dumps({"status": controlled_interruption_status(root, run_id=args.run_id, phase=args.phase), "run_id": args.run_id, "phase": args.phase}, sort_keys=True)) diff --git a/src/engineering_platform/qualification_runtime.py b/src/engineering_platform/qualification_runtime.py new file mode 100644 index 0000000..b143d23 --- /dev/null +++ b/src/engineering_platform/qualification_runtime.py @@ -0,0 +1,189 @@ +"""Deterministic, local-only provider seams for installed qualification. + +Never selected in a normal runtime. The installed qualification executable +opts in explicitly, so no real Codex or GitHub write can escape its fixture. +""" +from __future__ import annotations + +from pathlib import Path +import json +import os +import re +import subprocess +import time + +from .capability_review import MANDATORY_REVIEW_OUTPUT_CONTRACT_VERSION, ReviewerResult +from .execution_models import AgentResult, PullRequestEvidence + + +class DeterministicQualificationAgent: + def __init__(self) -> None: + self._process_callback = None + + def set_process_callback(self, callback: object) -> None: + self._process_callback = callback + + def wait_for_controlled_interruption_arm(self, _root: Path, state: object) -> None: + """Offer the installed recovery E2E one bounded, non-production arm window.""" + ready = os.environ.get("EP_QUALIFICATION_CONTROL_ARM_READY_FILE") + if not ready or not Path(ready).with_suffix(Path(ready).suffix + ".enable").is_file(): + return + ready_path = Path(ready) + ready_path.write_text(json.dumps({"run_id": getattr(state, "run_id", None), "phase": "EXECUTE_AGENT"}), encoding="utf-8") + continue_path = ready_path.with_suffix(ready_path.suffix + ".continue") + deadline = time.monotonic() + 15 + while time.monotonic() < deadline: + if continue_path.is_file(): + return + time.sleep(.02) + raise RuntimeError("QUALIFICATION_CONTROL_ARM_TIMED_OUT") + + def invoke(self, root: Path, prompt: str) -> AgentResult: + if callable(self._process_callback): + self._process_callback({"pid": os.getpid(), "process_group": os.getpgrp()}) + if "sole automatic post-finalization reconciliation" in prompt.lower(): + sha = subprocess.run(("git", "-C", str(root), "rev-parse", "HEAD"), check=True, text=True, capture_output=True).stdout.strip() + return AgentResult("COMPLETE", terminal_condition="repository_reconciled", commit_sha=sha) + if "execution mode: genesis" in prompt.lower(): + target = next( + (line.split(":", 1)[1].strip() for line in prompt.splitlines() + if line.strip().lower().startswith("target repository:")), + "", + ) + target_root = Path(target).resolve() + sha = subprocess.run(("git", "-C", str(target_root), "rev-parse", "HEAD"), check=True, text=True, capture_output=True).stdout.strip() + return AgentResult("COMPLETE", terminal_condition="local_commit_reconciled", repository_path=str(target_root), commit_sha=sha) + is_finalization = "finalization pr on exactly" in prompt.lower() + publication = "first implementation pull-request publication gate" in prompt.lower() + if self._github_write_target(root) and (is_finalization or publication): + return self._create_github_managed_handoff( + root, + finalization=is_finalization, + publication=publication, + prompt=prompt, + ) + if not is_finalization and not publication: + # The implementation candidate is a real local branch before it + # is reviewed, but its remote PR is deliberately absent until the + # host's first-publication gate. + current = subprocess.run(("git", "-C", str(root), "branch", "--show-current"), check=True, text=True, capture_output=True).stdout.strip() + if current != "qualification-managed": + subprocess.run(("git", "-C", str(root), "switch", "-c", "qualification-managed"), check=True, text=True, capture_output=True) + if self._github_write_target(root): + subprocess.run(("git", "-C", str(root), "push", "--set-upstream", "origin", "qualification-managed"), check=True, text=True, capture_output=True) + sha = subprocess.run(("git", "-C", str(root), "rev-parse", "HEAD"), check=True, text=True, capture_output=True).stdout.strip() + branch = self._finalization_branch(prompt) if is_finalization else "qualification-managed" + return AgentResult("COMPLETE", branch=branch, pull_request=1 if (is_finalization or publication) else None, commit_sha=sha) + + @staticmethod + def _finalization_branch(prompt: str) -> str: + match = re.search( + r"finalization pr on exactly `([^`]+)`", + prompt, + flags=re.IGNORECASE, + ) + if not match: + raise RuntimeError("QUALIFICATION_FINALIZATION_BRANCH_UNAVAILABLE") + return match.group(1) + + @staticmethod + def _github_write_target(root: Path) -> bool: + """Limit the external seam to its exact approved origin only.""" + if os.environ.get("EP_QUALIFICATION_GITHUB_WRITE_FLOW") != "1": + return False + expected = os.environ.get("EP_QUALIFICATION_GITHUB_REPOSITORY", "") + remote = subprocess.run(("git", "-C", str(root), "remote", "get-url", "origin"), text=True, capture_output=True) # nosec B603 + if remote.returncode: + return False + value = remote.stdout.strip().removesuffix(".git") + return value.removeprefix("https://github.com/").removeprefix("git@github.com:") == expected + + @staticmethod + def _create_github_managed_handoff( + root: Path, *, finalization: bool = False, publication: bool = False, prompt: str = "" + ) -> AgentResult: + """Create one bounded dummy-repository branch and GitHub PR. + + This seam is reachable only through the explicit external qualification + command. It intentionally stops at the normal human merge boundary: + the production lifecycle must not auto-merge merely because this is a + dummy repository. + """ + repository = os.environ.get("EP_QUALIFICATION_GITHUB_REPOSITORY", "") + branch = os.environ.get("EP_QUALIFICATION_GITHUB_BRANCH", "") + if finalization: + # The host checkpoints the only permitted Finalization branch + # before it invokes a provider. The external qualification seam + # must exercise that contract exactly; inventing a fixture branch + # makes a successful remote PR unrecoverable by the host. + branch = DeterministicQualificationAgent._finalization_branch(prompt) + elif "controlled recovery qualification" in prompt.lower(): + # A second Managed transaction in the same fixture must retain a + # distinct remote handoff identity after the armed interruption. + branch = branch + "-recovery" + if not repository or not branch: + raise RuntimeError("QUALIFICATION_GITHUB_WRITE_CONFIGURATION_INVALID") + + def run(*args: str) -> str: + return subprocess.run(args, check=True, text=True, capture_output=True).stdout.strip() # nosec B603 + + existing = run("git", "-C", str(root), "branch", "--show-current") + if existing != branch: + run("git", "-C", str(root), "switch", "-c", branch) + if not publication: + proof = root / ".engineering-platform" / ("managed-github-e2e-finalization-proof.json" if finalization else "managed-github-e2e-proof.json") + proof.write_text(json.dumps({ + "branch": branch, + "kind": "EP_MANAGED_GITHUB_E2E", + "stage": "FINALIZATION" if finalization else "IMPLEMENTATION", + "version": 1, + }, sort_keys=True) + "\n", encoding="utf-8") + run("git", "-C", str(root), "add", str(proof.relative_to(root))) + run("git", "-C", str(root), "commit", "-m", "test: record managed GitHub qualification handoff") + run("git", "-C", str(root), "push", "--set-upstream", "origin", branch) + title = "test: managed GitHub qualification finalization" if finalization else "test: managed GitHub qualification" + run("gh", "pr", "create", "--repo", repository, "--head", branch, "--base", "main", "--title", title, "--body", "Explicitly authorized Engineering Platform dummy-repository qualification.") + number = int(run("gh", "pr", "view", branch, "--repo", repository, "--json", "number", "--jq", ".number")) + sha = run("git", "-C", str(root), "rev-parse", "HEAD") + return AgentResult("COMPLETE", branch=branch, pull_request=number, commit_sha=sha) + + def available(self) -> bool: return True + # Keep the public provider-version contract valid so the normal installed + # compatibility gate remains part of qualification. + def version(self) -> str: return "0.153.4" + def validate(self, root: Path, prompt: str) -> AgentResult: + """Qualification validation is explicitly non-mutating. + + The installed test composition exercises the same host gate as the + production adapter but cannot manufacture a commit or PR while that + gate is active. + """ + branch = subprocess.run(("git", "-C", str(root), "branch", "--show-current"), check=True, text=True, capture_output=True).stdout.strip() + sha = subprocess.run(("git", "-C", str(root), "rev-parse", "HEAD"), check=True, text=True, capture_output=True).stdout.strip() + return AgentResult( + "COMPLETE", branch=branch, commit_sha=sha, + validation_evidence=({"command": "deterministic installed validation", "result": "passed"},), + ) + def review(self, _root: Path, selection: object, _objective: str, evidence: object = None) -> ReviewerResult: + return ReviewerResult( + getattr(selection, "reviewer"), "Deterministic read-only assurance passed.", + findings=(), contract_version=MANDATORY_REVIEW_OUTPUT_CONTRACT_VERSION, + ) + + +class LocalQualificationGitHub: + """A local PR/check adapter: no network, push, or GitHub mutation.""" + def __init__(self, root: Path) -> None: + self.root, self.calls = root, 0 + + def pull_request(self, number: int) -> PullRequestEvidence: + self.calls += 1 + sha = subprocess.run(("git", "-C", str(self.root), "rev-parse", "HEAD"), check=True, text=True, capture_output=True).stdout.strip() + if self.calls == 1: + return PullRequestEvidence(number, "OPEN", True, True, head_branch="qualification-managed", base_branch="main") + return PullRequestEvidence(number, "MERGED", True, True, merge_commit=sha, head_branch="qualification-managed", base_branch="main") + + def pull_request_for_head_branch(self, _branch: str): return None + def normalize_markdown_body(self, _number: int) -> bool: return False + def ready(self, _number: int) -> None: return None + def merge(self, _number: int) -> None: return None diff --git a/src/engineering_platform/schemas/producer-readback-v1.2.schema.json b/src/engineering_platform/schemas/producer-readback-v1.2.schema.json new file mode 100644 index 0000000..3e2fbd4 --- /dev/null +++ b/src/engineering_platform/schemas/producer-readback-v1.2.schema.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://engineering-platform.local/schemas/producer-readback-v1.2.schema.json", + "title": "Engineering Platform producer readback v1.2 disposition extension", + "type": "object", + "additionalProperties": false, + "required": ["contract_version", "submission", "producer", "correlation", "provenance", "disposition", "run", "result", "evidence"], + "properties": { + "contract_version": {"const": "1.2"}, + "submission": {"type": "object", "additionalProperties": false, "required": ["id", "project_id", "repository_id", "state", "admission", "transport", "created_at", "accepted_request_digest"]}, + "producer": {"type": "object", "additionalProperties": false, "required": ["id", "type", "version"]}, + "correlation": {"type": "object", "additionalProperties": false, "required": ["correlation_id", "mission_id", "engineering_action_id"]}, + "provenance": {"type": "object", "additionalProperties": false, "required": ["status", "forge_execution"]}, + "disposition": { + "type": "object", + "additionalProperties": false, + "required": ["state", "terminal", "execution_eligible", "revision", "operation_id", "event_reference", "reason", "actor_reference", "recorded_at"], + "properties": { + "state": {"enum": ["QUEUED", "DEFERRED", "QUARANTINED", "DECLINED"]}, + "terminal": {"type": "boolean"}, + "execution_eligible": {"type": "boolean"}, + "revision": {"type": "integer", "minimum": 0}, + "operation_id": {"type": ["string", "null"]}, + "event_reference": {"type": ["string", "null"]}, + "reason": {"type": "string"}, + "actor_reference": {"type": "string"}, + "recorded_at": {"type": ["string", "null"]} + } + }, + "run": {"type": ["object", "null"]}, + "result": {"type": "object", "additionalProperties": false, "required": ["outcome", "terminal", "delivery_qualified"]}, + "evidence": {"type": "object", "additionalProperties": false, "required": ["status", "terminal_artifact", "repository"]} + } +} diff --git a/src/engineering_platform/server.py b/src/engineering_platform/server.py index dc4b908..a76e0bd 100644 --- a/src/engineering_platform/server.py +++ b/src/engineering_platform/server.py @@ -92,7 +92,7 @@ # bootstrap is deliberately separate from the retired predecessor migration # machinery: it creates a clean installation only and never accepts a source # database path. -SERVER_STORE_SCHEMA_VERSION = 53 +SERVER_STORE_SCHEMA_VERSION = 56 SERVER_ENVIRONMENT_DATA_ROOT = "EP_SERVER_DATA_ROOT" FILE_INBOX_DIRECTORY = "file-inbox" HTTP_JSON_OPENAPI_PATH = "/v1/openapi.json" @@ -221,7 +221,7 @@ def _http_json_openapi_document() -> dict[str, object]: "schema": {"type": "string"}}, ], "responses": { - "200": {"description": "Canonical project-scoped producer readback v1.1"}, + "200": {"description": "Canonical project-scoped producer readback v1.2"}, "401": {"description": "Missing or invalid consumer credential"}, "404": {"description": "Submission absent from the authenticated project"}, }, @@ -515,6 +515,8 @@ class ServerConfigurationError(ValueError): "ep_submissions", "ep_submission_events", "ep_submission_prompt_history", + "ep_queue_disposition_operations", + "ep_operator_capabilities", "ep_parity_lifecycle_dispatches", "ep_receipt_run_provenance", "ep_external_producer_bindings", @@ -703,7 +705,7 @@ def _migrate_schema_43(connection: sqlite3.Connection) -> None: transport TEXT NOT NULL CHECK(transport IN ('HTTP','CLI','FILE_INBOX','LEGACY_FILE')), prompt TEXT NOT NULL, prompt_digest TEXT NOT NULL, constraints TEXT NOT NULL, idempotency_key TEXT, correlation_id TEXT, mission_id TEXT, engineering_action_id TEXT, - state TEXT NOT NULL CHECK(state IN ('QUEUED','REJECTED')), admission TEXT NOT NULL, + state TEXT NOT NULL CHECK(state IN ('QUEUED','REJECTED','DEFERRED','QUARANTINED','DECLINED')), admission TEXT NOT NULL, created_at TEXT NOT NULL)""") connection.execute("CREATE INDEX ep_submissions_project_lookup ON ep_submissions(project_id,state,created_at DESC)") connection.execute("CREATE UNIQUE INDEX ep_submissions_idempotency_lookup ON ep_submissions(project_id,idempotency_key) WHERE idempotency_key IS NOT NULL") @@ -920,7 +922,7 @@ def _migrate_schema_51(connection: sqlite3.Connection) -> None: prompt TEXT NOT NULL, prompt_digest TEXT NOT NULL, constraints TEXT NOT NULL, idempotency_key TEXT, correlation_id TEXT, mission_id TEXT, engineering_action_id TEXT, transport_receipt_id TEXT, transport_received_at TEXT, - state TEXT NOT NULL CHECK(state IN ('QUEUED','REJECTED')), admission TEXT NOT NULL, + state TEXT NOT NULL CHECK(state IN ('QUEUED','REJECTED','DEFERRED','QUARANTINED','DECLINED')), admission TEXT NOT NULL, created_at TEXT NOT NULL)""") connection.execute("""INSERT INTO ep_submissions( submission_id,project_id,repository_id,producer_id,producer_type,producer_version,transport,prompt,prompt_digest,constraints,idempotency_key,correlation_id,mission_id,engineering_action_id,transport_receipt_id,transport_received_at,state,admission,created_at) @@ -1167,6 +1169,78 @@ def _migrate_schema_53(connection: sqlite3.Connection) -> None: connection.execute("UPDATE ep_installations SET schema_version=53") +def _migrate_schema_54(connection: sqlite3.Connection) -> None: + """Widen CENTRAL submission state for audited operator handling.""" + connection.execute("ALTER TABLE ep_installations RENAME TO ep_installations_schema53") + connection.execute("CREATE TABLE ep_installations (instance_id TEXT PRIMARY KEY, created_at TEXT NOT NULL, schema_version INTEGER NOT NULL CHECK(schema_version IN (41,42,43,44,45,46,47,48,49,50,51,52,53,54)))") + connection.execute("INSERT INTO ep_installations SELECT instance_id,created_at,54 FROM ep_installations_schema53") + connection.execute("DROP TABLE ep_installations_schema53") + for table in ("ep_submission_events", "ep_submission_prompt_history", "ep_parity_lifecycle_dispatches", "ep_submissions"): + connection.execute(f"ALTER TABLE {table} RENAME TO {table}_schema53") + connection.execute("""CREATE TABLE ep_submissions ( + submission_id TEXT PRIMARY KEY, project_id TEXT NOT NULL REFERENCES ep_project_registrations(project_id), + repository_id TEXT NOT NULL REFERENCES ep_repository_registrations(repository_id), producer_id TEXT NOT NULL, + producer_type TEXT NOT NULL, producer_version TEXT, transport TEXT NOT NULL CHECK(transport IN ('HTTP','CLI','FILE_INBOX','DEPENDABOT','LEGACY_FILE')), + prompt TEXT NOT NULL, prompt_digest TEXT NOT NULL, constraints TEXT NOT NULL, idempotency_key TEXT, correlation_id TEXT, + mission_id TEXT, engineering_action_id TEXT, transport_receipt_id TEXT, transport_received_at TEXT, + state TEXT NOT NULL CHECK(state IN ('QUEUED','REJECTED','DEFERRED','QUARANTINED','DECLINED')), admission TEXT NOT NULL, created_at TEXT NOT NULL)""") + # Do not use ``SELECT *`` here: an interrupted/newer installation can + # retain later additive columns while its recorded schema is still being + # recovered. Schema-54 owns exactly these predecessor columns. + connection.execute("""INSERT INTO ep_submissions( + submission_id,project_id,repository_id,producer_id,producer_type, + producer_version,transport,prompt,prompt_digest,constraints, + idempotency_key,correlation_id,mission_id,engineering_action_id, + transport_receipt_id,transport_received_at,state,admission,created_at + ) SELECT + submission_id,project_id,repository_id,producer_id,producer_type, + producer_version,transport,prompt,prompt_digest,constraints, + idempotency_key,correlation_id,mission_id,engineering_action_id, + transport_receipt_id,transport_received_at,state,admission,created_at + FROM ep_submissions_schema53""") + connection.execute("CREATE TABLE ep_submission_events (event_id INTEGER PRIMARY KEY, submission_id TEXT NOT NULL REFERENCES ep_submissions(submission_id), event_kind TEXT NOT NULL, payload TEXT NOT NULL, recorded_at TEXT NOT NULL)") + connection.execute("INSERT INTO ep_submission_events SELECT * FROM ep_submission_events_schema53") + connection.execute("CREATE TABLE ep_submission_prompt_history (submission_id TEXT PRIMARY KEY REFERENCES ep_submissions(submission_id), prompt_digest TEXT NOT NULL, recorded_at TEXT NOT NULL)") + connection.execute("INSERT INTO ep_submission_prompt_history SELECT * FROM ep_submission_prompt_history_schema53") + connection.execute("""CREATE TABLE ep_parity_lifecycle_dispatches (submission_id TEXT PRIMARY KEY REFERENCES ep_submissions(submission_id), project_id TEXT NOT NULL REFERENCES ep_project_registrations(project_id), repository_id TEXT NOT NULL REFERENCES ep_repository_registrations(repository_id), run_id TEXT NOT NULL UNIQUE REFERENCES ep_execution_runs(run_id), state TEXT NOT NULL CHECK(state IN ('CLAIMED','RUNNING','COMPLETE','BLOCKED','FAILED')), prompt_path TEXT NOT NULL, claimed_at TEXT NOT NULL, updated_at TEXT NOT NULL, operator_resolution TEXT NOT NULL DEFAULT 'NONE' CHECK(operator_resolution IN ('NONE','OPEN','DISMISSED','RETRIED')), resolution_submission_id TEXT REFERENCES ep_submissions(submission_id))""") + connection.execute("INSERT INTO ep_parity_lifecycle_dispatches SELECT * FROM ep_parity_lifecycle_dispatches_schema53") + for table in ("ep_submission_events_schema53", "ep_submission_prompt_history_schema53", "ep_parity_lifecycle_dispatches_schema53", "ep_submissions_schema53"): + connection.execute(f"DROP TABLE {table}") + connection.execute("CREATE INDEX ep_submissions_project_lookup ON ep_submissions(project_id,state,created_at DESC)") + connection.execute("CREATE UNIQUE INDEX ep_submissions_idempotency_lookup ON ep_submissions(project_id,idempotency_key) WHERE idempotency_key IS NOT NULL") + connection.execute("CREATE INDEX ep_parity_lifecycle_dispatches_run_lookup ON ep_parity_lifecycle_dispatches(run_id,state)") + connection.execute("INSERT OR IGNORE INTO engineering_schema_migrations(version) VALUES(54)") + connection.execute("UPDATE engineering_metadata SET value='54' WHERE key='installation.schema_version'") + connection.execute("UPDATE ep_installations SET schema_version=54") + + +def _migrate_schema_55(connection: sqlite3.Connection) -> None: + """Add durable CAS and idempotency evidence for CENTRAL queue commands.""" + connection.execute("ALTER TABLE ep_installations RENAME TO ep_installations_schema54") + connection.execute("CREATE TABLE ep_installations (instance_id TEXT PRIMARY KEY, created_at TEXT NOT NULL, schema_version INTEGER NOT NULL CHECK(schema_version IN (41,42,43,44,45,46,47,48,49,50,51,52,53,54,55)))") + connection.execute("INSERT INTO ep_installations SELECT instance_id,created_at,55 FROM ep_installations_schema54") + connection.execute("DROP TABLE ep_installations_schema54") + columns = {str(row[1]) for row in connection.execute("PRAGMA table_info(ep_submissions)")} + if "disposition_revision" not in columns: + connection.execute("ALTER TABLE ep_submissions ADD COLUMN disposition_revision INTEGER NOT NULL DEFAULT 0") + connection.execute("CREATE TABLE IF NOT EXISTS ep_queue_disposition_operations (operation_id TEXT PRIMARY KEY, project_id TEXT NOT NULL, submission_id TEXT NOT NULL REFERENCES ep_submissions(submission_id), actor_reference TEXT NOT NULL, command_digest TEXT NOT NULL, from_state TEXT NOT NULL, to_state TEXT NOT NULL, previous_revision INTEGER NOT NULL, resulting_revision INTEGER NOT NULL, event_id INTEGER NOT NULL REFERENCES ep_submission_events(event_id), recorded_at TEXT NOT NULL)") + connection.execute("INSERT OR IGNORE INTO engineering_schema_migrations(version) VALUES(55)") + connection.execute("UPDATE engineering_metadata SET value='55' WHERE key='installation.schema_version'") + connection.execute("UPDATE ep_installations SET schema_version=55") + + +def _migrate_schema_56(connection: sqlite3.Connection) -> None: + """Add explicitly granted, project-scoped queue operator capabilities.""" + connection.execute("ALTER TABLE ep_installations RENAME TO ep_installations_schema55") + connection.execute("CREATE TABLE ep_installations (instance_id TEXT PRIMARY KEY, created_at TEXT NOT NULL, schema_version INTEGER NOT NULL CHECK(schema_version IN (41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56)))") + connection.execute("INSERT INTO ep_installations SELECT instance_id,created_at,56 FROM ep_installations_schema55") + connection.execute("DROP TABLE ep_installations_schema55") + connection.execute("CREATE TABLE IF NOT EXISTS ep_operator_capabilities (consumer_id TEXT NOT NULL, project_id TEXT NOT NULL, capability TEXT NOT NULL CHECK(capability IN ('QUEUE_HOLD_RESUME','QUEUE_DECLINE')), granted_at TEXT NOT NULL, revoked_at TEXT, PRIMARY KEY(consumer_id,project_id,capability))") + connection.execute("INSERT OR IGNORE INTO engineering_schema_migrations(version) VALUES(56)") + connection.execute("UPDATE engineering_metadata SET value='56' WHERE key='installation.schema_version'") + connection.execute("UPDATE ep_installations SET schema_version=56") + + def validate_store(data_root: Path, identity: RuntimeIdentity) -> dict[str, object]: """Return a deterministic fail-closed current-schema structural report.""" path = data_root / SERVER_DATABASE_FILENAME @@ -1232,14 +1306,14 @@ def initialize(data_root: Path, *, bind_host: str = "127.0.0.1", bind_port: int existing_tables = _table_names(existing) if existing_tables: current_schema = _schema_version(existing) - if current_schema not in {41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, SERVER_STORE_SCHEMA_VERSION}: + if current_schema not in {41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, SERVER_STORE_SCHEMA_VERSION}: raise ServerConfigurationError( f"EP Server store is not a valid official schema-{SERVER_STORE_SCHEMA_VERSION} installation." ) if current_schema == SERVER_STORE_SCHEMA_VERSION: validate_store(data_root, identity) return identity - if current_schema in {42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52}: + if current_schema in {42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55}: with sqlite3.connect(database_path) as connection: # Schema-49 rebuilds the submission parent table # to widen its immutable transport constraint. @@ -1262,11 +1336,18 @@ def initialize(data_root: Path, *, bind_host: str = "127.0.0.1", bind_port: int _migrate_schema_49(connection) if current_schema in {42, 43, 44, 45, 46, 47, 48, 49}: _migrate_schema_50(connection) - if current_schema != 51: + if current_schema in {42, 43, 44, 45, 46, 47, 48, 49, 50}: _migrate_schema_51(connection) - if current_schema != 52: + if current_schema in {42, 43, 44, 45, 46, 47, 48, 49, 50, 51}: _migrate_schema_52(connection) - _migrate_schema_53(connection) + if current_schema in {42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52}: + _migrate_schema_53(connection) + if current_schema < 54: + _migrate_schema_54(connection) + if current_schema < 55: + _migrate_schema_55(connection) + if current_schema < 56: + _migrate_schema_56(connection) connection.execute("COMMIT") connection.execute("PRAGMA legacy_alter_table=OFF") validate_store(data_root, identity) @@ -1293,6 +1374,9 @@ def initialize(data_root: Path, *, bind_host: str = "127.0.0.1", bind_port: int _migrate_schema_51(connection) _migrate_schema_52(connection) _migrate_schema_53(connection) + _migrate_schema_54(connection) + _migrate_schema_55(connection) + _migrate_schema_56(connection) connection.execute("COMMIT") connection.execute("PRAGMA legacy_alter_table=OFF") connection.execute("PRAGMA foreign_keys=ON") @@ -2513,6 +2597,40 @@ def _authenticated_consumer(connection: sqlite3.Connection, token: object, proje return str(row[0]) if row else None +def _operator_capability(connection: sqlite3.Connection, token: object, project_id: str, capability: str) -> str | None: + """Resolve an explicit project capability; admission credentials are insufficient.""" + actor = _authenticated_consumer(connection, token, project_id) + if actor is None: + return None + grant = connection.execute("SELECT 1 FROM ep_operator_capabilities WHERE consumer_id=? AND project_id=? AND capability=? AND revoked_at IS NULL", (actor, project_id, capability)).fetchone() + return actor if grant is not None else "" + + +def _same_origin(headers: Mapping[str, str]) -> bool: + """Accept only an absent or same-host HTTP(S) browser origin. + + Origin is CSRF protection, never an authentication substitute. The + capability check at the mutation boundary remains authoritative. + """ + origin, host = headers.get("Origin", "") or "", headers.get("Host", "") or "" + return origin in {"", f"http://{host}", f"https://{host}"} + + +def _strict_json_object(raw: bytes) -> dict[str, object]: + """Decode one JSON object while rejecting duplicate member names.""" + def no_duplicates(pairs: list[tuple[str, object]]) -> dict[str, object]: + result: dict[str, object] = {} + for key, value in pairs: + if key in result: + raise ValueError("duplicate JSON member") + result[key] = value + return result + value = json.loads(raw.decode("utf-8"), object_pairs_hook=no_duplicates) + if not isinstance(value, dict): + raise ValueError("JSON object required") + return value + + def _admit_server_owned_file_inbox( data_root: Path, envelope: dict[str, object], receipt_id: str, received_at: str, ) -> dict[str, object]: @@ -2567,7 +2685,13 @@ def _send(self, status_code: int, payload: dict[str, object], instance_id: str | if route is not None: self.send_header("EP-Console-Route-Owner", route.owner) self.end_headers() - self.wfile.write(encoded) + try: + self.wfile.write(encoded) + except (BrokenPipeError, ConnectionResetError): + # Health probes may abandon a response while the Server finishes + # rendering it. The request has no mutation authority; avoid a + # traceback that obscures qualification diagnostics. + return def _send_ndjson(self, entries: list[dict[str, object]]) -> None: encoded = ("\n".join(json.dumps(entry, sort_keys=True) for entry in entries) + ("\n" if entries else "")).encode("utf-8") @@ -2579,7 +2703,10 @@ def _send_ndjson(self, entries: list[dict[str, object]]) -> None: if route is not None: self.send_header("EP-Console-Route-Owner", route.owner) self.end_headers() - self.wfile.write(encoded) + try: + self.wfile.write(encoded) + except (BrokenPipeError, ConnectionResetError): + return def _send_console_asset(self, request: SplitResult) -> bool: """Serve installed Console assets without selecting a project/root.""" @@ -3269,6 +3396,45 @@ def _delegate_dashboard(self, method: str) -> None: return self._send(200, result) return + if method == "do_POST" and request.path == "/api/queue-disposition": + if not _same_origin(self.headers): + self._send(403, {"error": "INVALID_ORIGIN"}) + return + if not isinstance(selected, str) or selected not in project_ids: + self._send(409, {"error": "CONSOLE_PROJECT_UNAVAILABLE"}) + return + try: + length = int(self.headers.get("Content-Length", "0")) + if not 2 <= length <= 1024: + raise ValueError + payload = _strict_json_object(self.rfile.read(length)) + expected = {"contract_version", "operation_id", "submission_id", "expected_state", "expected_revision", "disposition", "reason"} + if (not isinstance(payload, dict) or set(payload) != expected + or payload.get("contract_version") != "1.0" + or not all(isinstance(payload.get(field), str) for field in ("operation_id", "submission_id", "expected_state", "disposition", "reason")) + or not isinstance(payload.get("expected_revision"), int) or isinstance(payload.get("expected_revision"), bool)): + raise ValueError + with sqlite3.connect(self.server.data_root / SERVER_DATABASE_FILENAME) as connection: # type: ignore[attr-defined] + connection.execute("BEGIN IMMEDIATE") + token = self.headers.get("Authorization", "")[7:] if self.headers.get("Authorization", "").startswith("Bearer ") else None + capability = "QUEUE_DECLINE" if payload["disposition"] == "DECLINED" else "QUEUE_HOLD_RESUME" + actor = _operator_capability(connection, token, selected, capability) + if actor is None: + raise submission_service.SubmissionError("UNAUTHENTICATED", 401) + if not actor: + raise submission_service.SubmissionError("OPERATOR_CAPABILITY_REQUIRED", 403) + result = submission_service.operator_queue_disposition( + connection, project_id=selected, submission_id=payload["submission_id"], + disposition=payload["disposition"], reason=payload["reason"], + expected_state=payload["expected_state"], expected_revision=payload["expected_revision"], + operation_id=payload["operation_id"], actor_reference=actor, + ) + self._send(200, result) + except submission_service.SubmissionError as error: + self._send(error.status, {"error": error.code}) + except (ValueError, UnicodeDecodeError, json.JSONDecodeError): + self._send(400, {"error": "INVALID_REQUEST"}) + return if isinstance(selected, str) and selected in project_ids: # No supported CENTRAL Console route may fall through to the # retained dashboard handler. New routes must be added above @@ -3359,7 +3525,7 @@ def do_GET(self) -> None: # noqa: N802 except sqlite3.Error: self._send(503, {"error": "CENTRAL_UNAVAILABLE"}) return - artifact = re.fullmatch(r"/v1/projects/([^/]+)/artifacts/(terminal-evidence:[^/]+)", request.path) + artifact = re.fullmatch(r"/v1/projects/([^/]+)/artifacts/((?:terminal-evidence|assurance-findings):[^/]+)", request.path) if artifact: project_id, artifact_id = artifact.groups() authorization = self.headers.get("Authorization", "") @@ -3645,7 +3811,7 @@ def health(data_root: Path) -> dict[str, object]: def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(prog="engineering-platform-server", description="Manage the standalone Engineering Platform Server foundation") - parser.add_argument("command", choices=("init", "start", "serve", "stop", "status", "health", "service-install", "service-uninstall", "relay-install", "relay-uninstall", "pairing-create", "agent-status", "agent-revoke", "agent-reset", "topology", "submission-diagnose", "bootstrap-topology", "register-topology", "provision-declaration", "issue-consumer-credential", "bind-repository", "rebind-repository", "unbind-repository", "resolve-repository", "register-producer-binding", "list-producer-bindings", "deactivate-producer-binding")) + parser.add_argument("command", choices=("init", "start", "serve", "stop", "status", "health", "service-install", "service-uninstall", "relay-install", "relay-uninstall", "pairing-create", "agent-status", "agent-revoke", "agent-reset", "topology", "submission-diagnose", "bootstrap-topology", "register-topology", "provision-declaration", "issue-consumer-credential", "grant-operator-capability", "revoke-operator-capability", "bind-repository", "rebind-repository", "unbind-repository", "resolve-repository", "register-producer-binding", "list-producer-bindings", "deactivate-producer-binding")) parser.add_argument("--data-root", type=Path, default=default_data_root()) parser.add_argument("--bind-host", default="127.0.0.1") parser.add_argument("--bind-port", type=int, default=8765) @@ -3661,6 +3827,7 @@ def build_parser() -> argparse.ArgumentParser: parser.add_argument("--external-resource-identity") parser.add_argument("--binding-id") parser.add_argument("--reason") + parser.add_argument("--capability", choices=("QUEUE_HOLD_RESUME", "QUEUE_DECLINE")) return parser @@ -3742,6 +3909,20 @@ def main(argv: list[str] | None = None) -> int: from .submission_service import issue_consumer_credential with sqlite3.connect(args.data_root / SERVER_DATABASE_FILENAME) as connection: result = issue_consumer_credential(connection, consumer_id=args.consumer_id, project_id=args.project_id) + elif args.command in {"grant-operator-capability", "revoke-operator-capability"}: + if not args.project_id or not args.consumer_id or not args.capability: + raise ServerConfigurationError("--project-id, --consumer-id and --capability are required for queue operator capability management.") + initialize(args.data_root) + with sqlite3.connect(args.data_root / SERVER_DATABASE_FILENAME) as connection: + if args.command == "grant-operator-capability": + registered = connection.execute("SELECT 1 FROM ep_consumer_registrations WHERE consumer_id=? AND project_id=? AND status='ACTIVE'", (args.consumer_id, args.project_id)).fetchone() + if registered is None: + raise ServerConfigurationError("ACTIVE_SCOPED_CONSUMER_REQUIRED") + connection.execute("INSERT INTO ep_operator_capabilities(consumer_id,project_id,capability,granted_at,revoked_at) VALUES(?,?,?,?,NULL) ON CONFLICT(consumer_id,project_id,capability) DO UPDATE SET granted_at=excluded.granted_at,revoked_at=NULL", (args.consumer_id, args.project_id, args.capability, _utcnow())) + result = {"result": "GRANTED", "consumer_id": args.consumer_id, "project_id": args.project_id, "capability": args.capability} + else: + changed = connection.execute("UPDATE ep_operator_capabilities SET revoked_at=? WHERE consumer_id=? AND project_id=? AND capability=? AND revoked_at IS NULL", (_utcnow(), args.consumer_id, args.project_id, args.capability)).rowcount + result = {"result": "REVOKED" if changed else "NOT_ACTIVE", "consumer_id": args.consumer_id, "project_id": args.project_id, "capability": args.capability} elif args.command == "register-producer-binding": if not all((args.producer_type, args.external_resource_type, args.external_resource_identity, args.project_id, args.repository_id, args.reason)): raise ServerConfigurationError("--producer-type, --external-resource-type, --external-resource-identity, --project-id, --repository-id and --reason are required for producer binding registration.") diff --git a/src/engineering_platform/submission_service.py b/src/engineering_platform/submission_service.py index 9115133..90cc9d7 100644 --- a/src/engineering_platform/submission_service.py +++ b/src/engineering_platform/submission_service.py @@ -11,6 +11,7 @@ from datetime import datetime, timezone import hashlib import json +import unicodedata import secrets import sqlite3 from pathlib import Path @@ -24,6 +25,12 @@ MAX_CONSTRAINT_BYTES = 8192 VALID_TRANSPORTS = frozenset({"HTTP", "CLI", "FILE_INBOX", "DEPENDABOT", "LEGACY_FILE"}) VALID_EXECUTION_MODES = frozenset({"MANAGED", "GENESIS"}) +OPERATOR_QUEUE_STATES = frozenset({"DEFERRED", "QUARANTINED", "DECLINED"}) +OPERATOR_QUEUE_TRANSITIONS = { + "QUEUED": frozenset({"DEFERRED", "QUARANTINED", "DECLINED"}), + "DEFERRED": frozenset({"QUEUED"}), + "QUARANTINED": frozenset({"QUEUED", "DECLINED"}), +} # This is the complete B8D lifecycle. The final value deliberately says what # CENTRAL has *not* done: admission makes a submission eligible for a later @@ -46,6 +53,62 @@ def __init__(self, code: str, status: int = 400) -> None: self.code, self.status = code, status +def operator_queue_disposition(connection: sqlite3.Connection, *, project_id: str, + submission_id: str, disposition: str, reason: str, + expected_state: str | None = None, + expected_revision: int | None = None, + operation_id: str | None = None, + actor_reference: str = "LEGACY_NOT_RECORDED") -> dict[str, object]: + """Apply one auditable CENTRAL queue disposition; never delete intent.""" + if (not isinstance(submission_id, str) or not isinstance(disposition, str) + or not isinstance(reason, str) or not isinstance(actor_reference, str)): + raise SubmissionError("INVALID_QUEUE_DISPOSITION") + normalized_reason = unicodedata.normalize("NFC", reason).strip() + if (disposition not in OPERATOR_QUEUE_STATES | {"QUEUED"} or not normalized_reason + or len(normalized_reason) > 500 or any(ord(char) < 32 and char not in "\t" for char in normalized_reason)): + raise SubmissionError("INVALID_QUEUE_DISPOSITION") + command_digest = hashlib.sha256(json.dumps( + [project_id, submission_id, expected_state, expected_revision, disposition, normalized_reason], + separators=(",", ":"), + ).encode()).hexdigest() + if operation_id is not None: + if not isinstance(operation_id, str) or not operation_id or len(operation_id) > 128: + raise SubmissionError("INVALID_QUEUE_DISPOSITION") + prior = connection.execute( + "SELECT command_digest,to_state,resulting_revision,recorded_at FROM ep_queue_disposition_operations WHERE operation_id=?", + (operation_id,), + ).fetchone() + if prior is not None: + if str(prior[0]) != command_digest: + raise SubmissionError("OPERATION_ID_CONFLICT", 409) + return {"submission_id": submission_id, "state": str(prior[1]), "reason": normalized_reason, + "recorded_at": str(prior[3]), "resulting_revision": int(prior[2]), "operation_id": operation_id, + "replayed": True} + row = connection.execute( + "SELECT state,admission,disposition_revision FROM ep_submissions WHERE project_id=? AND submission_id=?", (project_id, submission_id) + ).fetchone() + if row is None: + raise SubmissionError("SUBMISSION_NOT_FOUND", 404) + current, admission, revision = str(row[0]), str(row[1]), int(row[2]) + if admission != "ADMITTED" or (expected_state is not None and expected_state != current) or (expected_revision is not None and expected_revision != revision): + raise SubmissionError("QUEUE_DISPOSITION_CONFLICT", 409) + if connection.execute("SELECT 1 FROM ep_parity_lifecycle_dispatches WHERE submission_id=?", (submission_id,)).fetchone() is not None: + raise SubmissionError("QUEUE_DISPOSITION_CONFLICT", 409) + if disposition not in OPERATOR_QUEUE_TRANSITIONS.get(current, frozenset()): + raise SubmissionError("QUEUE_DISPOSITION_CONFLICT", 409) + now = _now() + changed = connection.execute("UPDATE ep_submissions SET state=?,disposition_revision=disposition_revision+1 WHERE project_id=? AND submission_id=? AND state=? AND disposition_revision=?", (disposition, project_id, submission_id, current, revision)).rowcount + if changed != 1: + raise SubmissionError("QUEUE_DISPOSITION_CONFLICT", 409) + event = connection.execute( + "INSERT INTO ep_submission_events(submission_id,event_kind,payload,recorded_at) VALUES(?,?,?,?)", + (submission_id, "OPERATOR_QUEUE_" + disposition, json.dumps({"state": disposition, "reason": normalized_reason, "actor_reference": actor_reference, "from_state": current, "previous_revision": revision, "resulting_revision": revision + 1, "operation_id": operation_id}, sort_keys=True), now), + ) + if operation_id is not None: + connection.execute("INSERT INTO ep_queue_disposition_operations(operation_id,project_id,submission_id,actor_reference,command_digest,from_state,to_state,previous_revision,resulting_revision,event_id,recorded_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)", (operation_id, project_id, submission_id, actor_reference, command_digest, current, disposition, revision, revision + 1, event.lastrowid, now)) + return {"submission_id": submission_id, "state": disposition, "reason": normalized_reason, "recorded_at": now, "previous_revision": revision, "resulting_revision": revision + 1, "operation_id": operation_id} + + def _lifecycle_payload(*, transport: str, producer_id: str) -> dict[str, str]: """Return the one durable lifecycle meaning shared by every adapter.""" return { @@ -334,8 +397,8 @@ def issue_consumer_credential(connection: sqlite3.Connection, *, consumer_id: st return {"credential_id": credential_id, "consumer_id": consumer_id, "project_id": project_id, "credential": token} -PRODUCER_READBACK_CONTRACT_VERSION = "1.1" -TERMINAL_EVIDENCE_CONTRACT_VERSION = "1.1" +PRODUCER_READBACK_CONTRACT_VERSION = "1.2" +TERMINAL_EVIDENCE_CONTRACT_VERSION = "1.2" _TERMINAL_OUTCOMES = frozenset({"COMPLETE", "BLOCKED", "FAILED"}) @@ -379,6 +442,69 @@ def _terminal_artifact_id(run_id: str) -> str: return f"terminal-evidence:{run_id}" +def _findings_artifact_id(run_id: str) -> str: + return f"assurance-findings:{run_id}" + + +def _current_assurance(checkpoint: object) -> tuple[str, list[dict[str, object]], list[dict[str, object]]]: + """Project final assurance from one complete current review set. + + Earlier review records are deliberately not overwritten: they remain + historical observations in the findings artifact. Only the most recent + complete quality/security pair bound to the checkpoint's exact candidate + and policy can qualify terminal delivery. + """ + profile = getattr(checkpoint, "assurance_profile", None) + reviews = list(getattr(checkpoint, "assurance_reviews", ())) + if not isinstance(profile, dict): + return "NOT_RECORDED", [], reviews + candidate, digest = profile.get("candidate_sha"), profile.get("digest") + if not isinstance(candidate, str) or not isinstance(digest, str): + return "UNRESOLVED", [], reviews + current = [ + review for review in reviews + if isinstance(review, dict) + and review.get("candidate_sha") == candidate + and review.get("profile_digest") == digest + ] + latest = { + role: next((review for review in reversed(current) if review.get("reviewer") == role), None) + for role in ("quality", "security") + } + if any(review is None or review.get("status") == "UNRESOLVED" for review in latest.values()): + return "UNRESOLVED", current, reviews + if any(review.get("status") != "PASS" for review in latest.values() if isinstance(review, dict)): + return "FAIL", current, reviews + findings = [finding for review in latest.values() if isinstance(review, dict) for finding in review.get("findings", []) if isinstance(finding, dict)] + current_open = any(finding.get("blocking") and finding.get("disposition") == "OPEN" for finding in findings) + resolved = { + item.get("finding_id") for item in getattr(checkpoint, "assurance_resolutions", ()) + if isinstance(item, dict) and item.get("disposition") in {"RESOLVED", "REJECTED", "RISK_ACCEPTED"} + } + historical_open = any( + finding.get("blocking") and finding.get("disposition") == "OPEN" and finding.get("id") not in resolved + for review in reviews if isinstance(review, dict) + for finding in review.get("findings", []) if isinstance(finding, dict) + ) + return ("FAIL" if current_open or historical_open else "PASS"), current, reviews + + +def _write_immutable_artifact(target: Path, payload: bytes) -> None: + """Create immutable evidence once; conflicting terminal rewrites fail closed.""" + if target.exists(): + try: + if target.read_bytes() == payload: + return + except OSError: + pass + raise SubmissionError("TERMINAL_EVIDENCE_IMMUTABLE_CONFLICT", 500) + target.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + temporary = target.with_suffix(target.suffix + ".tmp") + temporary.write_bytes(payload) + temporary.chmod(0o600) + temporary.replace(target) + + def _repository_revision(state: object, outcome: str) -> tuple[str | None, bool]: """Return a run-bound delivery revision, never an ambient checkout HEAD.""" if outcome != "COMPLETE": @@ -441,6 +567,23 @@ def write_terminal_evidence( revision, delivery_qualified = _repository_revision(checkpoint, outcome) artifact_id = _terminal_artifact_id(run_id) report_id = f"report:{run_id}" + assurance_status, current_reviews, reviews = _current_assurance(checkpoint) + findings = [finding for review in reviews for finding in review.get("findings", [])] + findings_id = _findings_artifact_id(run_id) if checkpoint.assurance_profile is not None else None + if findings_id is not None: + findings_payload = { + "artifact_type": "EP_ASSURANCE_FINDINGS", "contract_version": "1.0", + "run_id": run_id, "project_id": str(row[1]), "repository_id": str(row[2]), + "profile": checkpoint.assurance_profile, "reviews": reviews, + "resolutions": list(checkpoint.assurance_resolutions), + } + findings_target = data_root / "artifacts" / "projects" / str(row[1]) / "runs" / run_id / "assurance-findings-v1.json" + findings_bytes = _canonical_json_bytes(findings_payload) + _write_immutable_artifact(findings_target, findings_bytes) + record_artifact(repository_root, findings_target, artifact_id=findings_id, artifact_type="EP_ASSURANCE_FINDINGS", + content_type="application/json", created_at=_now(), run_id=run_id, submission_id=str(row[0]), + mission_id=str(row[10]) if row[10] is not None else None, producer_id=str(row[4]), + central_database=database, artifact_root=data_root / "artifacts") payload = { "artifact_type": "EP_TERMINAL_EVIDENCE", "contract_version": TERMINAL_EVIDENCE_CONTRACT_VERSION, "submission": {"id": str(row[0]), "project_id": str(row[1]), "repository_id": str(row[2]), @@ -456,13 +599,17 @@ def write_terminal_evidence( "validation": list(checkpoint.validation_evidence), "quality": list(checkpoint.quality_evidence), "repair": list(checkpoint.repair_audit), "finalization": checkpoint.latest_repository_evidence, }, + "assurance": { + "status": assurance_status, + "profile": checkpoint.assurance_profile, + "quality_review": next((review.get("status") for review in reversed(current_reviews) if review.get("reviewer") == "quality"), "NOT_RECORDED"), + "security_review": next((review.get("status") for review in reversed(current_reviews) if review.get("reviewer") == "security"), "NOT_RECORDED"), + "repair_rounds": {"used": checkpoint.repair_iterations, "maximum": 3}, + "findings": {"open_blocking": sum(1 for review in current_reviews for finding in review.get("findings", []) if finding.get("blocking") and finding.get("disposition") == "OPEN"), "open_non_blocking": sum(1 for review in current_reviews for finding in review.get("findings", []) if not finding.get("blocking") and finding.get("disposition") in {"OPEN", "NON_BLOCKING"}), "artifact": None if findings_id is None else {"id": findings_id, "digest_algorithm": "sha256", "digest": hashlib.sha256(findings_bytes).hexdigest()}}, + }, } target = data_root / "artifacts" / "projects" / str(row[1]) / "runs" / run_id / "terminal-evidence-v1.json" - target.parent.mkdir(mode=0o700, parents=True, exist_ok=True) - temporary = target.with_suffix(".json.tmp") - temporary.write_bytes(_canonical_json_bytes(payload)) - temporary.chmod(0o600) - temporary.replace(target) + _write_immutable_artifact(target, _canonical_json_bytes(payload)) record_artifact( repository_root, target, artifact_id=artifact_id, artifact_type="EP_TERMINAL_EVIDENCE", content_type="application/json", created_at=_now(), run_id=run_id, @@ -486,7 +633,7 @@ def producer_readback( row = connection.execute( """SELECT s.repository_id,s.producer_id,s.producer_type,s.producer_version, s.prompt_digest,s.constraints,s.correlation_id,s.mission_id, - s.engineering_action_id,s.state,s.admission,s.transport,s.created_at, + s.engineering_action_id,s.state,s.admission,s.transport,s.created_at,s.disposition_revision, d.run_id,d.state,d.operator_resolution,d.updated_at FROM ep_submissions AS s LEFT JOIN ep_parity_lifecycle_dispatches AS d @@ -499,9 +646,20 @@ def producer_readback( ( repository_id, producer_id, producer_type, producer_version, prompt_digest, raw_constraints, correlation_id, mission_id, engineering_action_id, submission_state, - admission, transport, created_at, run_id, dispatch_state, + admission, transport, created_at, disposition_revision, run_id, dispatch_state, operator_resolution, updated_at, ) = row + disposition_row = connection.execute( + "SELECT o.operation_id,o.event_id,o.actor_reference,e.payload,o.recorded_at FROM ep_queue_disposition_operations o JOIN ep_submission_events e ON e.event_id=o.event_id WHERE o.project_id=? AND o.submission_id=? ORDER BY o.recorded_at DESC LIMIT 1", + (project_id, submission_id), + ).fetchone() + disposition = {"state": str(submission_state), "terminal": str(submission_state) == "DECLINED", "execution_eligible": str(submission_state) == "QUEUED", "revision": int(disposition_revision), "operation_id": None, "event_reference": None, "reason": "NOT_RECORDED", "actor_reference": "NOT_RECORDED", "recorded_at": None} + if disposition_row is not None: + try: + reason = json.loads(str(disposition_row[3])).get("reason", "NOT_RECORDED") + except json.JSONDecodeError: + reason = "NOT_RECORDED" + disposition.update({"operation_id": str(disposition_row[0]), "event_reference": "event:" + str(disposition_row[1]), "actor_reference": str(disposition_row[2]), "reason": reason, "recorded_at": str(disposition_row[4])}) constraints = _read_constraints(raw_constraints) if constraints is None: # Existing data is retained, but cannot be represented as qualified @@ -603,19 +761,19 @@ def producer_readback( "engineering_action_id": engineering_action_id, }, "provenance": {"status": provenance_status, "forge_execution": constraints.get("forge_execution")}, - "run": run, "result": result, "evidence": evidence, + "disposition": disposition, "run": run, "result": result, "evidence": evidence, } def producer_evidence_artifact( connection: sqlite3.Connection, *, project_id: str, artifact_id: str, ) -> bytes | None: - """Return only a verified, project-scoped terminal evidence payload.""" + """Return a verified, project-scoped terminal or assurance artifact.""" row = connection.execute( """SELECT a.digest_algorithm,a.digest,a.storage_location FROM execution_artifact_records a JOIN ep_parity_lifecycle_dispatches d ON d.run_id=a.run_id - WHERE d.project_id=? AND a.artifact_id=? AND a.artifact_type='EP_TERMINAL_EVIDENCE'""", + WHERE d.project_id=? AND a.artifact_id=? AND a.artifact_type IN ('EP_TERMINAL_EVIDENCE','EP_ASSURANCE_FINDINGS')""", (project_id, artifact_id), ).fetchone() if row is None or row[0] != "sha256" or not isinstance(row[1], str): diff --git a/src/engineering_platform/templates/workspace-config.json b/src/engineering_platform/templates/workspace-config.json index 7ad3f49..d6ebb6f 100644 --- a/src/engineering_platform/templates/workspace-config.json +++ b/src/engineering_platform/templates/workspace-config.json @@ -3,7 +3,7 @@ "platform": { "id": "engineering-platform", "name": "Engineering Platform", - "version": "2.1.6", + "version": "2.3.0", "generation": 2, "documentation_namespace": "engineering-platform", "capability_registry_version": 1 diff --git a/tests/engineering/dashboard.spec.mjs b/tests/engineering/dashboard.spec.mjs index 6038828..455bb8e 100644 --- a/tests/engineering/dashboard.spec.mjs +++ b/tests/engineering/dashboard.spec.mjs @@ -3290,8 +3290,12 @@ test.describe("Engineering Status browser smoke", () => { steps: [ { id: "execute", presentation_key: "lifecycle.step.execute_agent", state: "COMPLETED" }, { id: "quality", presentation_key: "lifecycle.step.quality_control_agent", state: "ACTIVE", - timing: { started_at: "2026-08-16T14:00:00Z", spans: [{ phase: "QUALITY_CONTROL", duration_ms: 1000, outcome: "ACTIVE" }] }, - quality_evidence: [{ activity: "TEST_COVERAGE", result: "Gerichte regressietest toegevoegd." }] }, + timing: { started_at: "2026-08-16T14:00:00Z", spans: [{ phase: "QUALITY_CONTROL_AGENT", duration_ms: 1000, outcome: "ACTIVE" }] }, + quality_evidence: [{ activity: "TEST_COVERAGE", result: "Gerichte regressietest toegevoegd." }], + assurance_reviews: [ + { reviewer: "quality", status: "PASS", findings: [] }, + { reviewer: "security", status: "PASS", findings: [] }, + ] }, ], }, }, {})); @@ -3302,10 +3306,14 @@ test.describe("Engineering Status browser smoke", () => { const modal = page.locator("#lifecycleDetailModal"); await expect(modal).toBeVisible(); await expect(modal).toContainText(DASHBOARD_MESSAGES.nl["lifecycle.step.quality_control_agent"]); - await expect(modal).toContainText(DASHBOARD_MESSAGES.nl["telemetry.phase.quality_control"]); + await expect(modal).toContainText(DASHBOARD_MESSAGES.nl["telemetry.phase.quality_control_agent"]); await expect(modal).toContainText(DASHBOARD_MESSAGES.nl["lifecycle.detail_quality_evidence"]); await expect(modal).toContainText(DASHBOARD_MESSAGES.nl["lifecycle.quality_evidence.test_coverage"]); await expect(modal).toContainText("Gerichte regressietest toegevoegd."); + await expect(modal).toContainText(DASHBOARD_MESSAGES.nl["reviewer.quality"]); + await expect(modal).toContainText(DASHBOARD_MESSAGES.nl["reviewer.security"]); + await expect(modal).toContainText(DASHBOARD_MESSAGES.nl["lifecycle.assurance_status.pass"]); + await expect(modal).not.toContainText("QUALITY_CONTROL_AGENT"); await expect(modal.locator(".lifecycle-detail-modal__status-indicator")).toHaveClass(/indicator--blue/); }); @@ -10091,8 +10099,13 @@ test.describe("Engineering Status browser smoke", () => { await expect(page.locator("#queueList")).not.toContainText("Later uitvoeren"); }); - test("renders CENTRAL FIFO items without offering a legacy file deferral", async ({ page }) => { + test("maps CENTRAL queue actions to their matching confirmation modal", async ({ page }) => { + await page.route("**/api/dashboard-snapshot", (route) => route.fulfill({ json: { + status: { watcher_state: "WATCHER_IDLE", queue_depth: 0, queue_items: [] }, + component_versions: {}, telemetry: [], duration_estimate: {}, build_commit: "", + } })); await page.goto(dashboardUrl, { waitUntil: "domcontentloaded" }); + await selectDashboardLocale(page, "nl"); await page.locator("#autoRefresh").uncheck(); await page.evaluate(() => queueItems([ { @@ -10102,11 +10115,54 @@ test.describe("Engineering Status browser smoke", () => { producer_type: "CLI", action_intent: "UNSPECIFIED", modified_at: "2026-08-02T10:01:00Z", - queue_source: "CENTRAL", + queue_source: "CENTRAL", disposition_revision: 3, + queue_state: "DEFERRED", }, ], 1)); await expect(page.locator("#queueList .queue-item")).toHaveCount(1); - await expect(page.locator("#queueList .queue-defer")).toHaveCount(0); + await page.locator("#queueItems").evaluate((element) => { element.open = true; }); + await expect(page.locator("#queueList .queue-item__actions")).toHaveCount(1); + const messages = DASHBOARD_MESSAGES[await page.locator("html").getAttribute("lang")]; + expect(await page.locator("#queueList button").allTextContents()).toEqual([messages["queue.resume_action"]]); + const resume = page.getByRole("button", { name: messages["queue.resume_action"], exact: true }); + await expect(resume).toHaveCount(1); + await expect(page.getByRole("button", { name: messages["queue.defer_action"], exact: true })).toHaveCount(0); + await resume.click(); + await expect(page.locator("#confirmationModalTitle")).toHaveText(messages["queue.resume_title"]); + await expect(page.locator("#confirmationModalText")).toHaveText( + messages["queue.resume_description"].replace("{title}", "sub-central-fifo"), + ); + await expect(page.locator("#confirmationModalConfirm")).toHaveText(messages["queue.resume_action"]); + await page.locator("#confirmationModalCancel").click(); + + await page.evaluate(() => queueItems([{ + submission_id: "sub-central-fifo", filename: "sub-central-fifo", + title_kind: "producer_submission", producer_type: "CLI", action_intent: "UNSPECIFIED", + modified_at: "2026-08-02T10:01:00Z", queue_source: "CENTRAL", disposition_revision: 4, queue_state: "QUEUED", + }], 1)); + for (const [actionKey, titleKey] of [ + ["queue.defer_action", "queue.defer_title"], + ["queue.quarantine_action", "queue.quarantine_title"], + ["queue.decline_action", "queue.decline_title"], + ]) { + const action = messages[actionKey], title = messages[titleKey]; + await page.getByRole("button", { name: action, exact: true }).click(); + await expect(page.locator("#confirmationModalTitle")).toHaveText(title); + await expect(page.locator("#confirmationModalConfirm")).toHaveText(action); + await page.locator("#confirmationModalCancel").click(); + } + await expect(page.locator("#queueList .queue-item__actions .queue-defer")).toHaveCount(3); + const decline = page.getByRole("button", { name: messages["queue.decline_action"], exact: true }); + await expect(decline).toHaveClass(/queue-defer--destructive/); + await expect(decline).toHaveAttribute("title", messages["queue.decline_action"]); + await expect(decline).toHaveAttribute("aria-label", messages["queue.decline_action"]); + + await page.evaluate(() => queueItems([{ + submission_id: "sub-central-fifo", filename: "sub-central-fifo", title_kind: "producer_submission", + producer_type: "CLI", action_intent: "UNSPECIFIED", modified_at: "2026-08-02T10:01:00Z", + queue_source: "CENTRAL", disposition_revision: 5, queue_state: "QUARANTINED", + }], 1)); + expect(await page.locator("#queueList button").allTextContents()).toEqual([messages["queue.resume_action"], messages["queue.decline_action"]]); }); test("keeps a waiting Inbox item when deferring is cancelled", async ({ page }) => { diff --git a/tests/engineering/postman/ep-server-http-surface.postman_collection.json b/tests/engineering/postman/ep-server-http-surface.postman_collection.json new file mode 100644 index 0000000..951ef29 --- /dev/null +++ b/tests/engineering/postman/ep-server-http-surface.postman_collection.json @@ -0,0 +1,1118 @@ +{ + "info": { + "name": "EP Server complete HTTP surface", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "Console shell", + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/" + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(200));" + ] + } + } + ] + }, + { + "name": "Console JavaScript asset", + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/assets/dashboard.js" + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(200));" + ] + } + } + ] + }, + { + "name": "Console favicon", + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/favicon.ico" + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(200));" + ] + } + } + ] + }, + { + "name": "Platform health", + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/health" + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(503));" + ] + } + } + ] + }, + { + "name": "Platform projection", + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/api/platform-status" + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(200));" + ] + } + } + ] + }, + { + "name": "Dashboard snapshot", + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/api/dashboard-snapshot" + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(200));" + ] + } + } + ] + }, + { + "name": "Status alias", + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/api/status" + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(200));" + ] + } + } + ] + }, + { + "name": "Platform component details", + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/api/components/ep_server/details" + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(200));" + ] + } + } + ] + }, + { + "name": "Invalid component details", + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/api/components/not_a_component/details" + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(409));" + ] + } + } + ] + }, + { + "name": "Component restart rejects malformed body", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/api/components/ep_server/restart", + "body": { + "mode": "raw", + "raw": "{}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(409));" + ] + } + } + ] + }, + { + "name": "Platform logs", + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/api/logs/all" + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(200));" + ] + } + } + ] + }, + { + "name": "Platform logs reject malformed clear", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/api/logs/all", + "body": { + "mode": "raw", + "raw": "{}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(400));" + ] + } + } + ] + }, + { + "name": "Provider readiness", + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/api/provider-login-status" + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(200));" + ] + } + } + ] + }, + { + "name": "Provider repair rejects malformed body", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/api/provider-login/repair", + "body": { + "mode": "raw", + "raw": "{}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(409));" + ] + } + } + ] + }, + { + "name": "Provider logout rejects malformed body", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/api/provider-login/logout", + "body": { + "mode": "raw", + "raw": "{}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(409));" + ] + } + } + ] + }, + { + "name": "Execution runtime", + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/api/execution-runtime-status" + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(200));" + ] + } + } + ] + }, + { + "name": "Runtime repair rechecks the installed runtime", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/api/execution-runtime/repair", + "body": { + "mode": "raw", + "raw": "{}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(200));" + ] + } + } + ] + }, + { + "name": "Provider capacity", + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/api/provider-capacity" + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(200));" + ] + } + } + ] + }, + { + "name": "Provider capacity configuration", + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/api/provider-capacity/configuration" + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(200));" + ] + } + } + ] + }, + { + "name": "Provider capacity rejects malformed update", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/api/provider-capacity/configuration", + "body": { + "mode": "raw", + "raw": "{}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(409));" + ] + } + } + ] + }, + { + "name": "GitHub rate-limit diagnostics", + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/api/github-rate-limit" + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(200));" + ] + } + } + ] + }, + { + "name": "Console configuration", + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/api/configuration" + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(200));" + ] + } + } + ] + }, + { + "name": "Console configuration rejects malformed update", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/api/configuration", + "body": { + "mode": "raw", + "raw": "{}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(409));" + ] + } + } + ] + }, + { + "name": "Process metrics", + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/api/process-metrics" + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(200));" + ] + } + } + ] + }, + { + "name": "Usage diagnostics", + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/api/usage" + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(200));" + ] + } + } + ] + }, + { + "name": "Host-admin diagnostics", + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/api/host-admin/diagnostics" + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(200));" + ] + } + } + ] + }, + { + "name": "Central data export", + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/api/central-data/export" + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(200));" + ] + } + } + ] + }, + { + "name": "Central relocation rejects bad request", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/api/central-data/relocate", + "body": { + "mode": "raw", + "raw": "{}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(409));" + ] + } + } + ] + }, + { + "name": "Central import requires confirmation", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/api/central-data/import", + "body": { + "mode": "raw", + "raw": "{}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(409));" + ] + } + } + ] + }, + { + "name": "Central database configuration", + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/api/central-database/configuration" + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(200));" + ] + } + } + ] + }, + { + "name": "Central database configuration rejects bad request", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/api/central-database/configuration", + "body": { + "mode": "raw", + "raw": "{}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(400));" + ] + } + } + ] + }, + { + "name": "Project history", + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/api/prompt-history?project=postman-project" + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(200));" + ] + } + } + ] + }, + { + "name": "Missing project report", + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/api/prompt-history/missing-run/report?project=postman-project" + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(404));" + ] + } + } + ] + }, + { + "name": "Missing project chat", + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/api/prompt-history/missing-run/chat?project=postman-project" + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(404));" + ] + } + } + ] + }, + { + "name": "Missing project detail", + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/api/prompt-history/missing-run/details?project=postman-project" + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(404));" + ] + } + } + ] + }, + { + "name": "Missing telemetry day", + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/api/telemetry/2026-01-01?project=postman-project" + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(404));" + ] + } + } + ] + }, + { + "name": "Execution action rejects malformed request", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Origin", + "value": "{{baseUrl}}" + } + ], + "url": "{{baseUrl}}/api/execution-dismiss?project=postman-project", + "body": { + "mode": "raw", + "raw": "{}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(400));" + ] + } + } + ] + }, + { + "name": "Queue action rejects malformed request", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Origin", + "value": "{{baseUrl}}" + } + ], + "url": "{{baseUrl}}/api/queue-disposition?project=postman-project", + "body": { + "mode": "raw", + "raw": "{}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(400));" + ] + } + } + ] + }, + { + "name": "Translation route is fail-closed pending project mutation support", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Origin", + "value": "{{baseUrl}}" + } + ], + "url": "{{baseUrl}}/api/dashboard-translate?project=postman-project", + "body": { + "mode": "raw", + "raw": "{}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(405));" + ] + } + } + ] + }, + { + "name": "Retired runtime directory", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/api/runtime-directory/open", + "body": { + "mode": "raw", + "raw": "{}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(410));" + ] + } + } + ] + }, + { + "name": "Retired Inbox configuration", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/api/configuration/inbox-location", + "body": { + "mode": "raw", + "raw": "{}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(410));" + ] + } + } + ] + }, + { + "name": "Retired component logs", + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/api/logs/inbox" + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(410));" + ] + } + } + ] + }, + { + "name": "Operations projects", + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/v1/operations/projects" + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(200));" + ] + } + } + ] + }, + { + "name": "Topology diagnostics", + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/diagnostics/topology" + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(200));" + ] + } + } + ] + }, + { + "name": "Health probe", + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/healthz" + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(200));" + ] + } + } + ] + }, + { + "name": "Readiness probe", + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/readyz" + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(200));" + ] + } + } + ] + }, + { + "name": "Submission requires credential", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/v1/projects/postman-project/submissions", + "body": { + "mode": "raw", + "raw": "{}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(401));" + ] + } + } + ] + }, + { + "name": "Submission readback requires credential", + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/v1/projects/postman-project/submissions/missing" + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(401));" + ] + } + } + ] + }, + { + "name": "Evidence artifact requires credential", + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/v1/projects/postman-project/artifacts/terminal-evidence:missing" + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(401));" + ] + } + } + ] + }, + { + "name": "Agent pairing rejects malformed request", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/v1/agent/pair", + "body": { + "mode": "raw", + "raw": "{}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(400));" + ] + } + } + ] + }, + { + "name": "Agent registration requires credential", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": "{{baseUrl}}/v1/agent/register", + "body": { + "mode": "raw", + "raw": "{}" + } + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(401));" + ] + } + } + ] + }, + { + "name": "Unknown route", + "request": { + "method": "GET", + "header": [], + "url": "{{baseUrl}}/not-an-ep-route" + }, + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expected status', () => pm.response.to.have.status(404));" + ] + } + } + ] + } + ] +} diff --git a/tests/engineering/test_coverage_runtime_boundaries.py b/tests/engineering/test_coverage_runtime_boundaries.py index 410198d..aa97fa7 100644 --- a/tests/engineering/test_coverage_runtime_boundaries.py +++ b/tests/engineering/test_coverage_runtime_boundaries.py @@ -1223,18 +1223,18 @@ def test_local_api_service_is_retired_without_affecting_contract_fixtures(self) def test_controlled_provider_interruption_is_run_bound_single_use_and_redacted(self) -> None: root = self.root.parent / "recovery-control" - state = SimpleNamespace(terminal=False, phase="EXECUTE_AGENT") + state = SimpleNamespace(terminal=False, phase="INITIALIZE") with patch("engineering_platform.provider_recovery.StateStore") as states: states.return_value.load.return_value = state - armed = provider_recovery.arm_controlled_interruption(root, run_id="run-a", phase="QUALITY_CONTROL_AGENT", armed_by="user\nsecret", reason="reason\nsecret") + armed = provider_recovery.arm_controlled_interruption(root, run_id="run-a", phase="EXECUTE_AGENT", armed_by="user\nsecret", reason="reason\nsecret") self.assertEqual(armed["state"], "ARMED") - self.assertEqual(provider_recovery.controlled_interruption_status(root, run_id="run-a", phase="QUALITY_CONTROL_AGENT"), "ARMED") - self.assertEqual(provider_recovery.disarm_controlled_interruption(root, run_id="run-a", phase="QUALITY_CONTROL_AGENT"), "DISARMED") - self.assertEqual(provider_recovery.disarm_controlled_interruption(root, run_id="run-a", phase="QUALITY_CONTROL_AGENT"), "NOT_ARMED") + self.assertEqual(provider_recovery.controlled_interruption_status(root, run_id="run-a", phase="EXECUTE_AGENT"), "ARMED") + self.assertEqual(provider_recovery.disarm_controlled_interruption(root, run_id="run-a", phase="EXECUTE_AGENT"), "DISARMED") + self.assertEqual(provider_recovery.disarm_controlled_interruption(root, run_id="run-a", phase="EXECUTE_AGENT"), "NOT_ARMED") with patch("engineering_platform.provider_recovery.StateStore") as states: states.return_value.load.return_value = SimpleNamespace(terminal=True, phase="EXECUTE_AGENT") with self.assertRaisesRegex(provider_recovery.ControlledInterruptionControlError, "terminal"): - provider_recovery.arm_controlled_interruption(root, run_id="run-b", phase="QUALITY_CONTROL_AGENT") + provider_recovery.arm_controlled_interruption(root, run_id="run-b", phase="EXECUTE_AGENT") def test_recovery_reconciliation_fails_closed_for_absent_or_ambiguous_receipts(self) -> None: with patch("engineering_platform.provider_recovery.load_recovery_state", return_value=None): @@ -1284,18 +1284,18 @@ def test_provider_recovery_watcher_and_control_cli_never_create_unowned_work(sel "engineering_platform.provider_recovery.controlled_interruption_status", return_value="ARMED"), patch( "engineering_platform.provider_recovery.disarm_controlled_interruption", return_value="DISARMED" ), redirect_stdout(output): - self.assertEqual(provider_recovery.main(["arm-controlled-interruption", "--repo", str(self.root), "--run-id", "run-a", "--phase", "QUALITY_CONTROL_AGENT"]), 0) - self.assertEqual(provider_recovery.main(["controlled-interruption-status", "--repo", str(self.root), "--run-id", "run-a", "--phase", "QUALITY_CONTROL_AGENT"]), 0) - self.assertEqual(provider_recovery.main(["disarm-controlled-interruption", "--repo", str(self.root), "--run-id", "run-a", "--phase", "QUALITY_CONTROL_AGENT"]), 0) + self.assertEqual(provider_recovery.main(["arm-controlled-interruption", "--repo", str(self.root), "--run-id", "run-a", "--phase", "EXECUTE_AGENT"]), 0) + self.assertEqual(provider_recovery.main(["controlled-interruption-status", "--repo", str(self.root), "--run-id", "run-a", "--phase", "EXECUTE_AGENT"]), 0) + self.assertEqual(provider_recovery.main(["disarm-controlled-interruption", "--repo", str(self.root), "--run-id", "run-a", "--phase", "EXECUTE_AGENT"]), 0) self.assertIn("DISARMED", output.getvalue()) def test_controlled_interruption_hook_consumes_once_and_records_artifact(self) -> None: - with patch.dict("os.environ", {"ENGINEERING_PLATFORM_TEST_INTERRUPT_PROVIDER_ONCE": "run-a:QUALITY_CONTROL_AGENT"}, clear=False), patch( + with patch.dict("os.environ", {"ENGINEERING_PLATFORM_TEST_INTERRUPT_PROVIDER_ONCE": "run-a:EXECUTE_AGENT"}, clear=False), patch( "engineering_platform.provider_recovery.load_recovery_state", return_value=None ), patch("engineering_platform.provider_recovery.record_artifact") as recorded: - self.assertTrue(provider_recovery.consume_controlled_interruption_hook(self.root, run_id="run-a", phase="QUALITY_CONTROL_AGENT")) + self.assertTrue(provider_recovery.consume_controlled_interruption_hook(self.root, run_id="run-a", phase="EXECUTE_AGENT")) self.assertTrue(recorded.called) - self.assertFalse(provider_recovery.consume_controlled_interruption_hook(self.root, run_id="run-a", phase="QUALITY_CONTROL_AGENT")) + self.assertFalse(provider_recovery.consume_controlled_interruption_hook(self.root, run_id="run-a", phase="EXECUTE_AGENT")) def test_provider_recovery_transition_is_compare_and_swap_and_rejects_unknown_states(self) -> None: with self.assertRaises(ValueError): diff --git a/tests/engineering/test_deterministic_execution_e2e.py b/tests/engineering/test_deterministic_execution_e2e.py new file mode 100644 index 0000000..670e572 --- /dev/null +++ b/tests/engineering/test_deterministic_execution_e2e.py @@ -0,0 +1,61 @@ +"""Regression tests for the installed deterministic execution qualification.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path +import subprocess +import tempfile +import unittest + + +def _qualification_module() -> object: + path = Path(__file__).parents[2] / "tools" / "qualification" / "p_deterministic_execution_e2e.py" + specification = importlib.util.spec_from_file_location("deterministic_execution_e2e", path) + if specification is None or specification.loader is None: + raise RuntimeError("qualification module is unavailable") + module = importlib.util.module_from_spec(specification) + specification.loader.exec_module(module) + return module + + +class DeterministicExecutionE2ETests(unittest.TestCase): + def setUp(self) -> None: + self.module = _qualification_module() + self.temporary = tempfile.TemporaryDirectory() + self.root = Path(self.temporary.name) + self.data = self.root / "central" + self.data.mkdir() + self.configuration = self.data / "server.json" + + def tearDown(self) -> None: + self.temporary.cleanup() + + def test_isolated_runtime_is_executable_and_becomes_server_runtime_authority(self) -> None: + self.configuration.write_text(json.dumps({ + "version": 2, + "bind_host": "127.0.0.1", + "bind_port": 8765, + "managed_codex_cli_prefix": "/unavailable/production-runtime", + }), encoding="utf-8") + + executable = self.module.configure_deterministic_runtime(self.data, self.root) + + self.assertEqual( + subprocess.run((str(executable), "--version"), check=True, text=True, capture_output=True).stdout.strip(), + f"codex {self.module.QUALIFICATION_RUNTIME_VERSION}", + ) + configuration = json.loads(self.configuration.read_text(encoding="utf-8")) + self.assertEqual(configuration["managed_codex_cli_prefix"], str(executable.parent.parent.resolve())) + self.assertEqual(executable, self.root / "managed-codex-cli" / "bin" / "codex") + + def test_isolated_runtime_rejects_an_unexpected_server_configuration_shape(self) -> None: + self.configuration.write_text("{}", encoding="utf-8") + + with self.assertRaisesRegex(RuntimeError, "QUALIFICATION_SERVER_CONFIGURATION_INVALID"): + self.module.configure_deterministic_runtime(self.data, self.root) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/engineering/test_execution_host.py b/tests/engineering/test_execution_host.py index ce83411..86d9c9f 100644 --- a/tests/engineering/test_execution_host.py +++ b/tests/engineering/test_execution_host.py @@ -174,6 +174,10 @@ def available(self) -> bool: def version(self) -> str: return "0.146.0" + def review(self, _: Path, selection: object, __: str, evidence: object = None) -> ReviewerResult: + """Default post-implementation assurance fixture: clean, read-only pass.""" + return ReviewerResult(getattr(selection, "reviewer"), "No blocking finding.") + class ReviewCapableFakeAgent(FakeAgent): def __init__(self, result: AgentResult) -> None: @@ -425,6 +429,11 @@ def invoke(self, root: Path, prompt_text: str) -> AgentResult: self.roots.append(root) self.prompts.append(prompt_text) if "Local repository validation gate" in prompt_text: + return AgentResult( + "COMPLETE", branch, commit_sha=commit, + validation_evidence=({"command": "canonical suite", "result": "passed"},), + ) + if "First implementation pull-request publication gate" in prompt_text: self.pr_create_calls += 1 return AgentResult("COMPLETE", branch, 701, commit_sha=commit) if "Mandatory autonomous refactor" in prompt_text: @@ -1315,7 +1324,7 @@ def github(self, *args: str) -> str: @patch("engineering_platform.execution_host.subprocess.run") def test_codex_client_handles_valid_review_and_invoke_results(self, run: object) -> None: review_message = json.dumps( - {"contribution": "reviewed", "recommendations": ["keep scope"]} + {"contract_version": "1.0", "contribution": "reviewed", "recommendations": ["keep scope"], "findings": []} ) agent_message = json.dumps( { @@ -1336,17 +1345,22 @@ def test_codex_client_handles_valid_review_and_invoke_results(self, run: object) run.side_effect = [ subprocess.CompletedProcess(("codex",), 0, review_output, ""), subprocess.CompletedProcess(("codex",), 0, json.dumps({"type": "item.completed", "item": {"type": "agent_message", "text": agent_message}}), ""), + subprocess.CompletedProcess(("codex",), 0, json.dumps({"type": "item.completed", "item": {"type": "agent_message", "text": agent_message}}), ""), ] with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) client = CodexCliClient(CodexCliProvider()) review = client.review(root, __import__("engineering_platform.capability_review", fromlist=["ReviewerSelection"]).ReviewerSelection("validation", "scope", 1), "objective") result = client.invoke(root, "objective") + validation = client.validate(root, "validation objective") self.assertFalse(review.failed) self.assertEqual(review.recommendations, ("keep scope",)) self.assertEqual(review.runtime_metadata["raw_provider_model"], "gpt-5.6-terra") self.assertEqual(review.usage["input_tokens"], 100) self.assertEqual(result.pull_request, 12) + self.assertEqual(validation.pull_request, 12) + self.assertIn("read-only", run.call_args_list[2].args[0]) + self.assertFalse(hasattr(client, "_sandbox_override")) @patch("engineering_platform.execution_host.time.monotonic", side_effect=(10.0, 12.75)) @patch("engineering_platform.execution_host.subprocess.run") @@ -1666,12 +1680,9 @@ def test_new_run_initializes_and_records_canonical_prompt(self) -> None: self.assertIn("do not rerun the development-host bootstrap", agent.prompts[0]) self.assertIn(f"The only repository checkout for this transaction is `{self.root.resolve()}`", agent.prompts[0]) self.assertIn("producer provenance only", agent.prompts[0]) - self.assertEqual(agent.roots, [self.root, self.root]) - self.assertIn("Mandatory autonomous refactor and quality-control stage", agent.prompts[1]) - self.assertIn("Assess test coverage for every changed behavior", agent.prompts[1]) - self.assertIn("Assess the applicable operator, contract, and implementation documentation", agent.prompts[1]) - self.assertIn("In quality_evidence, record only work actually performed", agent.prompts[1]) - self.assertEqual(state.quality_evidence, quality_evidence) + self.assertEqual(agent.roots, [self.root]) + self.assertEqual([review["status"] for review in state.assurance_reviews], ["PASS", "PASS"]) + self.assertEqual(state.assurance_profile["candidate_sha"], "a" * 40) self.assertEqual(repository.synchronize_calls, [self.root]) def test_provider_recovery_preflight_rejects_every_ambiguous_restart_condition(self) -> None: @@ -1717,7 +1728,7 @@ def test_optional_phase_telemetry_and_agent_timing_never_change_run_authority(se with patch("engineering_platform.execution_host._complete_phase", side_effect=execution_host.EngineeringStorageError("offline")): execution_host.complete_phase(self.root, SimpleNamespace()) agent = FakeAgent(AgentResult("WAITING")) - runner = EngineeringRunner(self.root, self.store, FakeRepository(), FakeGitHub([]), agent, lambda _: None) + runner = EngineeringRunner(self.root, self.store, SubprocessRepositoryClient(), FakeGitHub([]), agent, lambda _: None) state = TransactionState("timing-run", "pcvantol/djconnect", str(self.prompt), "EXECUTE_AGENT") for measured in (True, "unknown", -1, 86_401): agent.last_execution_seconds = measured @@ -1725,6 +1736,53 @@ def test_optional_phase_telemetry_and_agent_timing_never_change_run_authority(se agent.last_execution_seconds = 1.2345 self.assertEqual(runner._record_agent_execution_time(state).agent_execution_seconds, 1.234) + def test_provider_process_boundary_records_only_a_verified_recovery_process(self) -> None: + runner = EngineeringRunner(self.root, self.store, FakeRepository(), FakeGitHub([]), FakeAgent(AgentResult("WAITING")), lambda _: None) + state = TransactionState("provider-process", "pcvantol/djconnect", str(self.prompt), "EXECUTE_AGENT") + with patch("engineering_platform.execution_host.write_runner_process") as written, \ + patch.object(runner, "_recovery_state", return_value=None), \ + patch("engineering_platform.execution_host.record_provider_started") as started: + runner._provider_process_boundary(state, {"pid": 42, "process_group": 42}) + written.assert_called_once_with(self.root, state.run_id, {"pid": 42, "process_group": 42}) + started.assert_not_called() + with patch("engineering_platform.execution_host.write_runner_process"), \ + patch.object(runner, "_recovery_state", return_value={"state": "RECOVERY_STARTING", "process_receipt_id": "receipt-1"}), \ + patch("engineering_platform.execution_host.record_provider_started") as started: + runner._provider_process_boundary(state, {"pid": 42, "process_group": 42}) + started.assert_called_once_with(self.root, run_id=state.run_id, receipt_id="receipt-1", pid=42, process_group=42, central_database=runner.store.central_database) + + def test_provider_invocation_records_only_safe_context_and_recovery_telemetry(self) -> None: + """Supplementary provider telemetry retains bounded context without authority.""" + agent = FakeAgent(AgentResult("WAITING")) + agent.last_context_escalations = ( + {"reason": "missing evidence", "boundary_kind": "run", "diagnostic": "safe diagnostic"}, + "untrusted", + ) + agent.last_execution_seconds = 2.0 + runner = EngineeringRunner(self.root, self.store, FakeRepository(), FakeGitHub([]), agent, lambda _: None) + state = TransactionState("provider-context", "pcvantol/djconnect", str(self.prompt), "EXECUTE_AGENT") + self.store.save(state) + with patch("engineering_platform.execution_host.persist_provider_invocation", return_value="provider-1") as persist: + invocation_id = runner._persist_provider_invocation( + state, phase="EXECUTE_AGENT", observed_metadata={"raw_provider_model": "gpt-5.6-terra"}, + ) + self.assertEqual(invocation_id, "provider-1") + recorded = persist.call_args.args[1] + self.assertEqual(recorded.model, "gpt-5.6-terra") + self.assertEqual(recorded.churn["context_scope_effective"], "INVESTIGATION") + self.assertEqual(recorded.churn["context_escalation_count"], 1) + self.assertEqual(recorded.churn["context_escalation_reasons"], "missing evidence") + + recovered = runner._recovery_record( + state, original="provider-1", replacement="provider-2", eligibility="ELIGIBLE", + result="RECOVERED", requested_at="then", started_at="then", completed_at="now", + ) + self.store.save(recovered) + agent.last_execution_seconds = 0.5 + merged = runner._record_agent_execution_time(state) + self.assertEqual(merged.provider_recovery_attempts, recovered.provider_recovery_attempts) + self.assertEqual(merged.agent_execution_seconds, 0.5) + def test_optional_phase_wrappers_degrade_only_telemetry_storage_failures(self) -> None: with patch("engineering_platform.execution_host._start_phase", return_value=SimpleNamespace()) as start: self.assertIsNotNone(execution_host.start_phase(self.root, "phase-run", "VALIDATION")) @@ -1746,6 +1804,95 @@ def test_repair_plans_and_environmental_validation_require_explicit_durable_evid self.assertFalse(runner._is_environmental_validation_instability(AgentResult("FAILED", validation_disposition="environmental_instability", validation_evidence=({"result": "passed"},)))) self.assertTrue(runner._is_environmental_validation_instability(AgentResult("FAILED", validation_disposition="environmental_instability", validation_evidence=({"result": "passed once; timed out once"},)))) + def test_repair_result_requires_the_durable_plan_and_preserves_its_bounded_pr(self) -> None: + """Recovery never infers a repair plan or lets a provider switch PRs.""" + runner = EngineeringRunner(self.root, self.store, FakeRepository(), FakeGitHub([]), FakeAgent(AgentResult("WAITING")), lambda _: None) + missing = TransactionState( + "repair-plan-missing", "pcvantol/djconnect", str(self.prompt), "REPAIR_AGENT", + branch="codex/repair-plan-missing", pull_request=17, repair_iterations=1, + ) + blocked = runner._advance_after_repair_agent_result( + missing, AgentResult("COMPLETE", branch=missing.branch, pull_request=17), + ) + self.assertEqual((blocked.phase, blocked.next_action), ("BLOCKED", "repair_plan_missing")) + self.assertTrue(self.store.load(missing.run_id).terminal) + + planned = missing.__class__(**{ + **missing.__dict__, + "run_id": "repair-plan-conflict", + "repair_audit": ({ + "iteration": "1", "outcome": "planned", "failed_checks": "required check", + "proposed_action": "repair only the required check", + },), + }) + conflict = runner._advance_after_repair_agent_result( + planned, AgentResult("COMPLETE", branch=planned.branch, pull_request=18, diagnostic="changed PR"), + ) + self.assertEqual((conflict.phase, conflict.next_action), ("BLOCKED", "bounded_scope_conflict")) + self.assertEqual(conflict.repair_audit[-1]["outcome"], "submitted_for_recheck") + self.assertEqual(self.store.load(planned.run_id).repair_audit, conflict.repair_audit) + + failed_plan = planned.__class__(**{**planned.__dict__, "run_id": "repair-agent-failed"}) + failed = runner._advance_after_repair_agent_result( + failed_plan, AgentResult("FAILED", branch=failed_plan.branch, pull_request=17, diagnostic="provider failure"), + ) + self.assertEqual((failed.phase, failed.next_action), ("FAILED", "external_action_required")) + self.assertEqual(failed.repair_audit[-1]["outcome"], "agent_failed") + + def test_repair_rereview_paths_fail_closed_for_missing_genesis_or_validation_evidence(self) -> None: + """A repair must return a candidate to the same assurance boundary.""" + runner = EngineeringRunner(self.root, self.store, FakeRepository(), FakeGitHub([]), FakeAgent(AgentResult("WAITING")), lambda _: None) + + def planned(run_id: str, *, mode: str = "MANAGED", rounds: int = 1) -> TransactionState: + return TransactionState( + run_id, "pcvantol/djconnect", str(self.prompt), "REPAIR_AGENT", + branch="codex/repair-rereview", pull_request=17, execution_mode=mode, + repair_iterations=rounds, + repair_audit=({ + "iteration": str(rounds), "outcome": "planned", "failed_checks": "required check", + "proposed_action": "repair bounded finding", + },), + ) + + genesis = planned("genesis-repair-missing", mode="GENESIS") + no_candidate = runner._advance_after_repair_agent_result( + genesis, AgentResult("COMPLETE", branch=genesis.branch), + ) + self.assertEqual((no_candidate.phase, no_candidate.next_action), ("BLOCKED", "repair_candidate_unavailable")) + + target = self.root / "genesis-repair-candidate" + target.mkdir() + review_state = planned("genesis-repair-reviewed", mode="GENESIS") + reviewed_state = review_state.__class__(**{**review_state.__dict__, "phase": "WAIT_FOR_TERMINAL_EVIDENCE"}) + reconciled = TransactionState("genesis-repair-complete", "pcvantol/djconnect", str(self.prompt), "COMPLETE", terminal=True) + with patch.object(runner, "_run_quality_assurance", return_value=(reviewed_state, AgentResult("COMPLETE", repository_path=str(target)))) as review, \ + patch.object(runner, "_reconcile_genesis_result", return_value=reconciled) as reconcile: + advanced = runner._advance_after_repair_agent_result( + review_state, AgentResult("COMPLETE", branch=review_state.branch, repository_path=str(target)), + ) + self.assertIs(advanced, reconciled) + self.assertEqual(review.call_args.kwargs["assurance_root"], target) + reconcile.assert_called_once() + + exhausted = planned("managed-repair-validation-exhausted", rounds=3) + with patch.object( + runner, "_run_local_repository_validation", + side_effect=lambda state, _: (state, AgentResult("FAILED", validation_evidence=({"result": "failed"},))), + ): + blocked = runner._advance_after_repair_agent_result( + exhausted, AgentResult("COMPLETE", branch=exhausted.branch, pull_request=17), + ) + self.assertEqual((blocked.phase, blocked.next_action), ("BLOCKED", "repair_budget_exhausted")) + + inspected = planned("managed-repair-inspection-failure") + post_review = inspected.__class__(**{**inspected.__dict__, "phase": "WAIT_FOR_TERMINAL_EVIDENCE"}) + review_result = AgentResult("COMPLETE", branch=inspected.branch, pull_request=17) + with patch.object(runner, "_run_local_repository_validation", side_effect=lambda state, _: (state, review_result)), \ + patch.object(runner, "_run_quality_assurance", return_value=(post_review, review_result)), \ + patch.object(runner.repository, "inspect", side_effect=RunnerError("unavailable")): + unavailable = runner._advance_after_repair_agent_result(inspected, review_result) + self.assertEqual((unavailable.phase, unavailable.next_action), ("BLOCKED", "repair_candidate_unavailable")) + def test_validation_failure_and_commit_evidence_helpers_refuse_unverified_inputs(self) -> None: runner = EngineeringRunner(self.root, self.store, FakeRepository(), FakeGitHub([]), FakeAgent(AgentResult("WAITING")), lambda _: None) self.assertFalse(runner._has_failed_validation_evidence(AgentResult("FAILED"))) @@ -2015,7 +2162,7 @@ def test_managed_run_keeps_a_producer_target_from_overriding_host_checkout(self) self.root, self.store, FakeRepository(), FakeGitHub([]), agent, lambda _: None ).run(self.prompt, run_id="managed-target-boundary") - self.assertEqual(agent.roots, [self.root, self.root]) + self.assertEqual(agent.roots, [self.root]) self.assertIn( f"The only repository checkout for this transaction is `{self.root.resolve()}`", agent.prompts[0], @@ -2439,21 +2586,86 @@ def test_execute_agent_phase_is_published_before_agent_invocation(self) -> None: agent = LiveStatusFakeAgent(AgentResult("COMPLETE")) runner = EngineeringRunner(self.root, self.store, FakeRepository(), FakeGitHub([]), agent, lambda _: None) runner.run(self.prompt, run_id="live-phase-run") - self.assertEqual(agent.live_phase, "QUALITY_CONTROL_AGENT") - self.assertEqual(agent.live_action, "autonomous_refactor_and_quality_control") + self.assertEqual(agent.live_phase, "EXECUTE_AGENT") + self.assertEqual(agent.live_action, "invoke_agent") self.assertEqual(agent.activity_action, "Codex bewerkt bestanden") - def test_autonomous_quality_control_cannot_replace_the_implementation_pr(self) -> None: + def test_quality_assurance_does_not_create_or_replace_the_implementation_pr(self) -> None: agent = SequencedFakeAgent([ AgentResult("COMPLETE", "codex/implementation", 701), AgentResult("COMPLETE", "codex/implementation", 702), ]) state = EngineeringRunner( - self.root, self.store, FakeRepository(), FakeGitHub([]), agent, lambda _: None + self.root, self.store, FakeRepository(), FakeGitHub([ + PullRequestEvidence(701, "OPEN", True, True, head_branch="codex/implementation", base_branch="main"), + ]), agent, lambda _: None ).run(self.prompt, run_id="quality-scope-run") - self.assertEqual(state.phase, "BLOCKED") - self.assertEqual(state.next_action, "autonomous_quality_control_scope") + self.assertEqual(state.phase, "WAIT_FOR_OPERATOR_MERGE") + self.assertEqual(len(agent.prompts), 1) + self.assertEqual([review["status"] for review in state.assurance_reviews], ["PASS", "PASS"]) + + def test_first_implementation_publication_is_a_separate_post_assurance_dispatch(self) -> None: + """The product gate, not provider wording, owns first PR creation.""" + sha = "a" * 40 + profile = {"version": "validation-profile@1.0", "digest": "sha256:" + "b" * 64, + "criteria_digest": "sha256:" + "c" * 64, "candidate_sha": sha} + reviews = tuple({"reviewer": role, "status": "PASS", "candidate_sha": sha, + "profile_digest": profile["digest"], "invocation_id": f"{role}-1", + "findings": [], "contract_version": "1.0", "started_at": "now", "completed_at": "now"} + for role in ("quality", "security")) + agent = SequencedFakeAgent([AgentResult("COMPLETE", "main", 71, commit_sha=sha)]) + runner = EngineeringRunner(self.root, self.store, FakeRepository(), FakeGitHub([]), agent, lambda _: None) + state = TransactionState("publication-gate", "pcvantol/djconnect", str(self.prompt), "QUALITY_CONTROL_AGENT", + branch="main", owner_authorized=True, assurance_profile=profile, assurance_reviews=reviews) + published, result = runner._publish_first_implementation_pull_request( + state, AgentResult("COMPLETE", "main", commit_sha=sha) + ) + self.assertFalse(published.terminal) + self.assertEqual(result.pull_request, 71) + self.assertIn("First implementation pull-request publication gate", agent.prompts[0]) + + def test_malformed_mandatory_review_is_unresolved_not_an_empty_pass(self) -> None: + class MalformedReviewer(FakeAgent): + def review(self, _: Path, selection: object, __: str, evidence: object = None) -> ReviewerResult: + return ReviewerResult(getattr(selection, "reviewer"), "malformed", contract_version=None) + + runner = EngineeringRunner(self.root, self.store, FakeRepository(), FakeGitHub([]), MalformedReviewer(AgentResult("COMPLETE")), lambda _: None) + state = TransactionState("malformed-review", "pcvantol/djconnect", str(self.prompt), "EXECUTE_AGENT", branch="main") + blocked, _ = runner._run_quality_assurance(state, AgentResult("COMPLETE", "main", commit_sha="a" * 40)) + self.assertTrue(blocked.terminal) + self.assertEqual(blocked.next_action, "mandatory_assurance_unresolved") + + def test_reserved_repair_revalidates_and_rereviews_before_returning_to_pr_evidence(self) -> None: + """One repair consumes one durable round and cannot bypass assurance.""" + sha = "a" * 40 + branch = "codex/repair-rereview" + agent = SequencedFakeAgent([ + AgentResult("COMPLETE", branch, commit_sha=sha), + AgentResult("COMPLETE", branch, commit_sha=sha, + validation_evidence=({"command": "canonical suite", "result": "passed"},)), + ]) + github = FakeGitHub([PullRequestEvidence(71, "OPEN", True, True, head_branch=branch, base_branch="main")]) + runner = EngineeringRunner(self.root, self.store, FakeRepository(branch=branch), github, agent, lambda _: None) + state = TransactionState("repair-rereview", "pcvantol/djconnect", str(self.prompt), "EXECUTE_AGENT", + branch=branch, pull_request=71, owner_authorized=True) + advanced = runner._repair(state, "quality review findings failed. Repair these bounded findings: finding-1") + self.assertEqual(advanced.phase, "WAIT_FOR_OPERATOR_MERGE") + self.assertEqual(advanced.repair_iterations, 1) + self.assertEqual(len(advanced.repair_audit), 1) + self.assertEqual(advanced.repair_audit[0]["repair_id"], "repair:repair-rereview:1") + self.assertEqual([item["status"] for item in advanced.assurance_reviews], ["PASS", "PASS"]) + self.assertIn("Local repository validation gate", agent.prompts[1]) + + def test_fourth_shared_repair_dispatch_is_refused_before_provider_invocation(self) -> None: + agent = FakeAgent(AgentResult("COMPLETE")) + runner = EngineeringRunner(self.root, self.store, FakeRepository(), FakeGitHub([]), agent, lambda _: None) + exhausted = TransactionState("repair-exhausted", "pcvantol/djconnect", str(self.prompt), "REPAIR_AGENT", + branch="codex/repair", repair_iterations=3) + blocked = runner._repair(exhausted, "hosted check failed. Repair bounded finding.") + self.assertTrue(blocked.terminal) + self.assertEqual(blocked.next_action, "repair_budget_exhausted") + self.assertEqual(agent.prompts, []) def test_local_repository_validation_iterates_before_creating_the_implementation_pr(self) -> None: agent = SequencedFakeAgent([ @@ -2475,13 +2687,11 @@ def test_local_repository_validation_iterates_before_creating_the_implementation state, AgentResult("COMPLETE", "codex/implementation") ) self.assertEqual(validated.phase, "LOCAL_REPOSITORY_VALIDATION") - self.assertEqual(validated.local_validation_iterations, 2) - self.assertEqual([item["outcome"] for item in validated.local_validation_audit], ["validation_failed", "validated"]) - self.assertEqual(validated.local_validation_audit[0]["proposed_action"], "FULL: full required repository suite") - self.assertEqual(validated.validation_evidence, ({"command": "python -m unittest tests.engineering", "result": "passed"},)) - self.assertEqual(result.pull_request, 701) - self.assertIn("iteration 1 of 3", agent.prompts[0]) - self.assertIn("Create one draft implementation pull request only after", agent.prompts[0]) + self.assertEqual(validated.local_validation_iterations, 1) + self.assertEqual([item["outcome"] for item in validated.local_validation_audit], ["validation_failed"]) + self.assertIsNone(result.pull_request) + self.assertIn("read-only", agent.prompts[0]) + self.assertNotIn("draft implementation pull request", agent.prompts[0]) def test_verified_implementation_validation_failure_enters_local_repair_route(self) -> None: sha = "b" * 40 @@ -2511,9 +2721,9 @@ def test_verified_implementation_validation_failure_enters_local_repair_route(se validated, result = runner._run_local_repository_validation(verified, initial) self.assertFalse(validated.terminal) - self.assertEqual(validated.local_validation_iterations, 2) - self.assertEqual([item["outcome"] for item in validated.local_validation_audit], ["validation_failed", "validated"]) - self.assertEqual(result.pull_request, 701) + self.assertEqual(validated.local_validation_iterations, 1) + self.assertEqual([item["outcome"] for item in validated.local_validation_audit], ["validation_failed"]) + self.assertIsNone(result.pull_request) def test_runner_routes_verified_failed_implementation_to_local_validation_before_pr(self) -> None: sha = "d" * 40 @@ -2549,12 +2759,13 @@ def invoke(self, root: Path, prompt: str) -> AgentResult: ) self.assertTrue(state.commit_evidence, state.diagnostic) - self.assertEqual(state.phase, "WAIT_FOR_OPERATOR_MERGE", state.diagnostic) - self.assertFalse(state.terminal) + self.assertEqual(state.phase, "BLOCKED", state.diagnostic) + self.assertTrue(state.terminal) + self.assertEqual(state.next_action, "implementation_pr_before_assurance") self.assertEqual(state.local_validation_iterations, 1) self.assertEqual(state.local_validation_audit[0]["outcome"], "validated") - self.assertEqual(len(agent.prompts), 3) - self.assertIn("Local repository validation gate — iteration 1 of 3", agent.prompts[1]) + self.assertEqual(len(agent.prompts), 2) + self.assertIn("Local repository validation gate", agent.prompts[1]) def test_unverified_or_external_implementation_failure_never_starts_local_repair(self) -> None: sha = "c" * 40 @@ -2571,7 +2782,7 @@ def test_unverified_or_external_implementation_failure_never_starts_local_repair self.assertFalse(runner._is_recoverable_implementation_validation_failure(state, result)) - def test_failed_local_validation_uses_all_three_bounded_attempts(self) -> None: + def test_failed_local_validation_is_one_non_mutating_turn_before_shared_repair(self) -> None: failures = [ AgentResult( "FAILED", "codex/implementation", diagnostic="Required local suite failed.", @@ -2591,10 +2802,9 @@ def test_failed_local_validation_uses_all_three_bounded_attempts(self) -> None: state, AgentResult("COMPLETE", "codex/implementation") ) - self.assertTrue(blocked.terminal) - self.assertEqual(blocked.next_action, "local_validation_attempt_limit_reached") - self.assertEqual(blocked.local_validation_iterations, 3) - self.assertEqual(len(blocked.local_validation_audit), 3) + self.assertFalse(blocked.terminal) + self.assertEqual(blocked.local_validation_iterations, 1) + self.assertEqual(len(blocked.local_validation_audit), 1) self.assertEqual({item["outcome"] for item in blocked.local_validation_audit}, {"validation_failed"}) def test_local_repository_validation_separates_proven_environment_instability(self) -> None: @@ -3048,6 +3258,8 @@ def test_genesis_mode_reconciles_a_clean_local_commit_without_remote_or_pr(self) self.assertEqual(state.genesis_repository_path, str(target)) self.assertEqual(state.genesis_commit_sha, commit) self.assertIsNone(state.pull_request) + self.assertEqual([review["reviewer"] for review in state.assurance_reviews], ["quality", "security"]) + self.assertEqual([review["status"] for review in state.assurance_reviews], ["PASS", "PASS"]) def test_genesis_selects_its_target_before_managed_cleanliness_checks(self) -> None: target = self.root.parent / f"genesis-clean-{self.root.name}" @@ -3603,7 +3815,7 @@ def test_merged_operator_handoff_resumes_on_a_later_poll(self) -> None: ]) runner = EngineeringRunner( self.root, self.store, FakeRepository(), github, - SequencedFakeAgent([AgentResult("WAITING", "codex/final", 22), AgentResult("COMPLETE", terminal_condition="repository_reconciled", commit_sha="a" * 40)]), lambda _: None, + SequencedFakeAgent([AgentResult("WAITING", "codex/finalize-later-merge", 22), AgentResult("COMPLETE", terminal_condition="repository_reconciled", commit_sha="a" * 40)]), lambda _: None, ) result = runner._poll(state) @@ -3616,7 +3828,7 @@ def test_merged_implementation_starts_and_reconciles_finalization(self) -> None: implementation = PullRequestEvidence(21, "MERGED", True, True, "b" * 40) final_open = PullRequestEvidence(22, "OPEN", True, True) final_merged = PullRequestEvidence(22, "MERGED", True, True, "c" * 40) - agent = SequencedFakeAgent([AgentResult("WAITING", "codex/final", 22), AgentResult("COMPLETE", terminal_condition="repository_reconciled", commit_sha="a" * 40)]) + agent = SequencedFakeAgent([AgentResult("WAITING", "codex/finalize-full-run", 22), AgentResult("COMPLETE", terminal_condition="repository_reconciled", commit_sha="a" * 40)]) state = TransactionState("full-run", "pcvantol/djconnect", str(self.prompt), "WAIT_FOR_TERMINAL_EVIDENCE", branch="codex/implementation", pull_request=21, owner_authorized=True) runner = EngineeringRunner(self.root, self.store, FakeRepository(), FakeGitHub([implementation, final_open, final_merged]), agent, lambda _: None) result = runner._poll(state) @@ -3642,20 +3854,20 @@ def test_owner_authorized_merged_lifecycle_reconciles_and_cleans_up(self) -> Non ) result = EngineeringRunner( self.root, self.store, repository, github, - SequencedFakeAgent([AgentResult("WAITING", "codex/final", 22), AgentResult("COMPLETE", terminal_condition="repository_reconciled", commit_sha="a" * 40)]), lambda _: None, + SequencedFakeAgent([AgentResult("WAITING", "codex/finalize-autonomous-happy-path", 22), AgentResult("COMPLETE", terminal_condition="repository_reconciled", commit_sha="a" * 40)]), lambda _: None, )._poll(state) self.assertEqual(result.phase, "COMPLETE") self.assertTrue(result.terminal) self.assertEqual(result.implementation_merge_commit, "b" * 40) self.assertEqual(result.finalization_merge_commit, "c" * 40) - self.assertEqual(repository.cleanup_calls, [("codex/implementation", "codex/final")]) + self.assertEqual(repository.cleanup_calls, [("codex/implementation", "codex/finalize-autonomous-happy-path")]) self.assertEqual(github.merge_calls, []) def test_merged_finalization_returned_by_agent_is_reconciled_without_ready(self) -> None: implementation = PullRequestEvidence(21, "MERGED", True, True, "b" * 40) final_merged = PullRequestEvidence(22, "MERGED", True, True, "c" * 40) - agent = SequencedFakeAgent([AgentResult("WAITING", "codex/final", 22), AgentResult("COMPLETE", terminal_condition="repository_reconciled", commit_sha="a" * 40)]) + agent = SequencedFakeAgent([AgentResult("WAITING", "codex/finalize-already-finalized", 22), AgentResult("COMPLETE", terminal_condition="repository_reconciled", commit_sha="a" * 40)]) state = TransactionState( "already-finalized", "pcvantol/djconnect", str(self.prompt), "WAIT_FOR_TERMINAL_EVIDENCE", branch="codex/implementation", @@ -3678,6 +3890,27 @@ def test_finalization_checkpoint_prevents_duplicate_generation(self) -> None: self.assertEqual(result.pull_request, 23) self.assertEqual(runner.agent.prompts, []) + def test_finalization_result_on_an_uncheckpointed_branch_is_rejected(self) -> None: + state = TransactionState( + "finalization-branch-guard", "pcvantol/djconnect", str(self.prompt), + "FINALIZE_AGENT", owner_authorized=True, + transaction_kind="FINALIZATION", branch="codex/finalize-finalization-branch-guard", + finalization_branch="codex/finalize-finalization-branch-guard", + ) + runner = EngineeringRunner( + self.root, self.store, FakeRepository(), FakeGitHub([]), + FakeAgent(AgentResult("WAITING")), lambda _: None, + ) + + blocked = runner._advance_after_finalization_agent_result( + state, + AgentResult("COMPLETE", branch="qualification/incorrect-finalization", pull_request=22), + ) + + self.assertEqual(blocked.phase, "BLOCKED") + self.assertEqual(blocked.next_action, "finalization_branch_mismatch") + self.assertIsNone(blocked.finalization_pull_request) + def test_finalization_recovery_persists_existing_pr_without_invoking_agent(self) -> None: state = TransactionState( "recover-finalization", "pcvantol/djconnect", str(self.prompt), "FINALIZE_AGENT", diff --git a/tests/engineering/test_execution_lifecycle.py b/tests/engineering/test_execution_lifecycle.py index ae552ef..b94181a 100644 --- a/tests/engineering/test_execution_lifecycle.py +++ b/tests/engineering/test_execution_lifecycle.py @@ -68,6 +68,22 @@ def test_local_validation_is_a_managed_step_with_its_own_iteration_evidence(self self.assertEqual(by_id["LOCAL_REPOSITORY_VALIDATION"]["iteration_count"], 2) self.assertEqual(by_id["LOCAL_REPOSITORY_VALIDATION"]["repair_audit"], list(audit)) + def test_assurance_projects_two_pinned_reviews_and_one_run_wide_counter(self) -> None: + profile = {"version": "validation-profile@1", "digest": "sha256:" + "a" * 64, "candidate_sha": "b" * 40} + finding = {"id": "security-1", "fingerprint": "f" * 32, "category": "SECURITY", "criterion": "post_implementation_assurance", "observation": "Missing project isolation test.", "severity": "HIGH", "confidence": "MEDIUM", "blocking": True, "disposition": "OPEN"} + reviews = ( + {"reviewer": "quality", "status": "PASS", "candidate_sha": "b" * 40, "profile_digest": profile["digest"], "invocation_id": "quality-1", "findings": []}, + {"reviewer": "security", "status": "FAIL", "candidate_sha": "b" * 40, "profile_digest": profile["digest"], "invocation_id": "security-1", "findings": [finding]}, + ) + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + self._state(root, "QUALITY_CONTROL_AGENT", repair_iterations=2, assurance_profile=profile, assurance_reviews=reviews) + value = projection(root, "inbox-flow") + quality = next(step for step in value["steps"] if step["id"] == "QUALITY_CONTROL_AGENT") + self.assertEqual(quality["repair_rounds"], {"used": 2, "maximum": 3}) + self.assertEqual(quality["assurance_reviews"], list(reviews)) + self.assertEqual(value["repair_rounds"], {"used": 2, "maximum": 3}) + def test_genesis_has_its_own_canonical_path(self) -> None: self.assertNotIn("WAIT_FOR_OPERATOR_MERGE", intended_path("GENESIS")) self.assertIn("WAIT_FOR_OPERATOR_MERGE", intended_path("MANAGED")) diff --git a/tests/engineering/test_parity_lifecycle_dispatcher.py b/tests/engineering/test_parity_lifecycle_dispatcher.py index 5bbfab5..55d5e82 100644 --- a/tests/engineering/test_parity_lifecycle_dispatcher.py +++ b/tests/engineering/test_parity_lifecycle_dispatcher.py @@ -151,6 +151,23 @@ def test_claims_one_submission_once_and_preserves_central_run_linkage(self) -> N self.assertFalse((self.roots["alpha"] / ".engineering" / "engineering.db").exists()) self.assertFalse((self.roots["alpha"] / ".engineering" / "engineering-runs").exists()) + def test_initialize_only_dispatch_materializes_input_on_normal_resume(self) -> None: + """A visible pre-run dispatch remains resumable after qualification pauses it.""" + submission = self._submission("alpha") + dispatcher = ParityLifecycleDispatcher(self.data, runner_factory=lambda root: _Runner()) + with patch.dict(os.environ, {"EP_QUALIFICATION_INITIALIZE_ONLY": "1"}, clear=False): + initialized = dispatcher.dispatch(submission) + prompt = self.data / "artifacts" / "projects" / "alpha" / "runs" / initialized.run_id / "submission.md" + self.assertEqual(initialized.state, "RUNNING") + self.assertFalse(prompt.exists()) + with patch("engineering_platform.parity_lifecycle_dispatcher.execute_host_preflight", return_value=_PassingPreflight()), \ + patch("engineering_platform.parity_lifecycle_dispatcher.execute_workspace_preflight", return_value=_PassingPreflight()), \ + patch("engineering_platform.parity_lifecycle_dispatcher.execute_capability_preflight", return_value=_PassingPreflight()): + resumed = dispatcher.dispatch(submission) + self.assertTrue(resumed.duplicate_claim) + self.assertEqual(resumed.state, "COMPLETE") + self.assertTrue(prompt.is_file()) + def test_context_never_crosses_project_binding(self) -> None: alpha, beta = self._submission("alpha"), self._submission("beta") dispatcher = ParityLifecycleDispatcher(self.data, runner_factory=lambda root: _Runner()) diff --git a/tests/engineering/test_platform_productization.py b/tests/engineering/test_platform_productization.py index 066826a..bfe9840 100644 --- a/tests/engineering/test_platform_productization.py +++ b/tests/engineering/test_platform_productization.py @@ -4,6 +4,7 @@ from pathlib import Path import json import sqlite3 +import shutil import tempfile import tomllib import unittest @@ -33,6 +34,7 @@ _link_workspace, _merge_databases, _merge_workspace, + _provision_workspace_paths, _worktree_roots, _merge_legacy_workspace, _validate_legacy_merge, @@ -60,6 +62,28 @@ def _version_projection_files() -> tuple[str, ...]: class PlatformProductizationTest(unittest.TestCase): + def test_runtime_workspace_symlink_is_excluded_from_git_untracked_evidence(self) -> None: + """The platform-owned workspace link must not fail a clean-candidate gate.""" + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) / "repository" + root.mkdir() + (root / ".git" / "info").mkdir(parents=True) + config = root / "src" / "engineering_platform" + config.mkdir(parents=True) + (config / "ENGINEERING_PLATFORM_CONFIG.json").write_text( + (ROOT / "src" / "engineering_platform" / "ENGINEERING_PLATFORM_CONFIG.json").read_text(encoding="utf-8"), + encoding="utf-8", + ) + workspace = root / ".git" / "engineering-platform" + workspace.mkdir() + (root / ".engineering").symlink_to(workspace, target_is_directory=True) + + _provision_workspace_paths(root, workspace) + + self.assertEqual( + (root / ".git" / "info" / "exclude").read_text(encoding="utf-8").splitlines(), + [".engineering"], + ) def test_release_version_is_consistent_across_every_canonical_component(self) -> None: """A release cannot publish a mixed Server, Console, Runner, or package version.""" manifest = json.loads((ROOT / "src" / "engineering_platform" / "ENGINEERING_PLATFORM_VERSION.json").read_text(encoding="utf-8")) @@ -133,12 +157,44 @@ def test_canonical_wheel_build_advances_one_patch_across_all_projections(self) - for relative in _version_projection_files(): target = root / relative target.parent.mkdir(parents=True, exist_ok=True) - target.write_text('version = "2.1.0"\n', encoding="utf-8") + shutil.copy2(ROOT / relative, target) + module.set_version(root, "2.1.0") self.assertEqual(module.advance(root), "2.1.1") self.assertEqual(module.advance(root), "2.1.2") - for path in root.rglob("*"): - if path.is_file(): - self.assertIn("2.1.2", path.read_text(encoding="utf-8")) + self.assertEqual(module._current_version(root), "2.1.2") + + def test_canonical_versioning_can_advance_a_minor_and_resets_patch(self) -> None: + import importlib.util + script = ROOT / "tools" / "qualification" / "advance_platform_build.py" + spec = importlib.util.spec_from_file_location("advance_platform_build", script) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + for relative in _version_projection_files(): + target = root / relative + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(ROOT / relative, target) + module.set_version(root, "2.1.6") + self.assertEqual(module.advance(root, component="minor"), "2.2.0") + self.assertEqual(module._current_version(root), "2.2.0") + + def test_bootstrap_docs_only_classification_does_not_allocate_a_version(self) -> None: + import importlib.util + script = ROOT / "tools" / "qualification" / "advance_platform_build.py" + spec = importlib.util.spec_from_file_location("advance_platform_build", script) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + for relative in _version_projection_files(): + target = root / relative + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(ROOT / relative, target) + module.set_version(root, "2.3.0") + self.assertEqual(module.advance(root, component="none"), "2.3.0") def test_release_build_can_set_an_exact_branch_version_across_all_projections(self) -> None: import importlib.util @@ -152,14 +208,35 @@ def test_release_build_can_set_an_exact_branch_version_across_all_projections(se for relative in _version_projection_files(): target = root / relative target.parent.mkdir(parents=True, exist_ok=True) - target.write_text('version = "2.1.6"\n', encoding="utf-8") + shutil.copy2(ROOT / relative, target) + module.set_version(root, "2.1.6") self.assertEqual(module.set_version(root, "2.2.0"), "2.2.0") - for path in root.rglob("*"): - if path.is_file(): - self.assertIn("2.2.0", path.read_text(encoding="utf-8")) + self.assertEqual(module._current_version(root), "2.2.0") + package_lock = json.loads((root / "package-lock.json").read_text(encoding="utf-8")) + self.assertEqual(package_lock["version"], "2.2.0") + self.assertEqual(package_lock["packages"][""]["version"], "2.2.0") with self.assertRaisesRegex(RuntimeError, "stable X.Y.Z"): module.set_version(root, "2.2") + def test_version_writer_rejects_invalid_last_projection_without_partial_update(self) -> None: + import importlib.util + script = ROOT / "tools" / "qualification" / "advance_platform_build.py" + spec = importlib.util.spec_from_file_location("advance_platform_build", script) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + for relative in _version_projection_files(): + target = root / relative + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(ROOT / relative, target) + pyproject_before = (root / "pyproject.toml").read_bytes() + (root / "src" / "engineering_platform" / "platform_version.py").write_text("# missing canonical constant\n", encoding="utf-8") + with self.assertRaisesRegex(RuntimeError, "runtime version projection drift"): + module.set_version(root, "2.2.1") + self.assertEqual((root / "pyproject.toml").read_bytes(), pyproject_before) + def test_public_api_has_all_productization_capabilities(self) -> None: registered = set(capabilities()) self.assertTrue({"runner", "runtime_provider", "repository_provider", "service_manager_provider", "remote_submission_provider", "private_remote_access_provider"} <= registered) diff --git a/tests/engineering/test_provider_recovery_controller.py b/tests/engineering/test_provider_recovery_controller.py index 3aed071..07bdc5a 100644 --- a/tests/engineering/test_provider_recovery_controller.py +++ b/tests/engineering/test_provider_recovery_controller.py @@ -3,10 +3,12 @@ from pathlib import Path from tempfile import TemporaryDirectory import os +import sqlite3 import unittest from engineering_platform.agent_state import StateStore, TransactionState from engineering_platform.provider_recovery import ( + _connection, ControlledInterruptionControlError, arm_controlled_interruption, controlled_interruption_status, @@ -22,6 +24,7 @@ consume_controlled_interruption_hook, watcher_resume_action, ) +from engineering_platform.storage import EngineeringStorageError class ProviderRecoveryControllerTests(unittest.TestCase): @@ -41,6 +44,19 @@ def setUp(self) -> None: def tearDown(self) -> None: self.temp.cleanup() + def test_explicit_central_binding_requires_an_existing_database(self) -> None: + missing = self.root / "missing-central.sqlite" + with self.assertRaisesRegex(EngineeringStorageError, "CENTRAL recovery database"): + _connection(self.root, missing) + + central = self.root / "central.sqlite" + sqlite3.connect(central).close() + connection = _connection(self.root, central) + try: + self.assertEqual(connection.execute("PRAGMA foreign_keys").fetchone(), (1,)) + finally: + connection.close() + def test_claim_start_terminal_lineage_is_exactly_once(self) -> None: self.assertTrue(transition_recovery_state( self.root, run_id=self.run_id, expected="RECOVERY_AVAILABLE", target="RECOVERY_STARTING", @@ -177,34 +193,34 @@ def test_controlled_hook_is_consumed_before_the_synthetic_interruption(self) -> def test_operator_arm_status_disarm_and_durable_consumption(self) -> None: run_id = "operator-control-run" self.store.save(TransactionState(run_id, "pcvantol/djconnect", "prompt.md", "LOCAL_REPOSITORY_VALIDATION")) - self.assertEqual(controlled_interruption_status(self.root, run_id=run_id, phase="QUALITY_CONTROL_AGENT"), "NOT_ARMED") - armed = arm_controlled_interruption(self.root, run_id=run_id, phase="QUALITY_CONTROL_AGENT", armed_by="operator") + self.assertEqual(controlled_interruption_status(self.root, run_id=run_id, phase="EXECUTE_AGENT"), "NOT_ARMED") + armed = arm_controlled_interruption(self.root, run_id=run_id, phase="EXECUTE_AGENT", armed_by="operator") self.assertEqual(armed["state"], "ARMED") - self.assertEqual(controlled_interruption_status(self.root, run_id=run_id, phase="QUALITY_CONTROL_AGENT"), "ARMED") - self.assertTrue(consume_controlled_interruption_hook(self.root, run_id=run_id, phase="QUALITY_CONTROL_AGENT")) - self.assertEqual(controlled_interruption_status(self.root, run_id=run_id, phase="QUALITY_CONTROL_AGENT"), "CONSUMED") - self.assertFalse(consume_controlled_interruption_hook(self.root, run_id=run_id, phase="QUALITY_CONTROL_AGENT")) + self.assertEqual(controlled_interruption_status(self.root, run_id=run_id, phase="EXECUTE_AGENT"), "ARMED") + self.assertTrue(consume_controlled_interruption_hook(self.root, run_id=run_id, phase="EXECUTE_AGENT")) + self.assertEqual(controlled_interruption_status(self.root, run_id=run_id, phase="EXECUTE_AGENT"), "CONSUMED") + self.assertFalse(consume_controlled_interruption_hook(self.root, run_id=run_id, phase="EXECUTE_AGENT")) with self.assertRaises(ControlledInterruptionControlError): - disarm_controlled_interruption(self.root, run_id=run_id, phase="QUALITY_CONTROL_AGENT") + disarm_controlled_interruption(self.root, run_id=run_id, phase="EXECUTE_AGENT") def test_operator_control_rejects_unsafe_targets(self) -> None: with self.assertRaises(ControlledInterruptionControlError): - arm_controlled_interruption(self.root, run_id="unknown-run", phase="QUALITY_CONTROL_AGENT") + arm_controlled_interruption(self.root, run_id="unknown-run", phase="EXECUTE_AGENT") with self.assertRaises(ControlledInterruptionControlError): arm_controlled_interruption(self.root, run_id=self.run_id, phase="EXECUTE_AGENT") self.store.save(TransactionState(self.run_id, "pcvantol/djconnect", "prompt.md", "QUALITY_CONTROL_AGENT")) with self.assertRaises(ControlledInterruptionControlError): - arm_controlled_interruption(self.root, run_id=self.run_id, phase="QUALITY_CONTROL_AGENT") + arm_controlled_interruption(self.root, run_id=self.run_id, phase="EXECUTE_AGENT") terminal_run = "terminal-operator-control-run" self.store.save(TransactionState(terminal_run, "pcvantol/djconnect", "prompt.md", "COMPLETE", terminal=True)) with self.assertRaises(ControlledInterruptionControlError): - arm_controlled_interruption(self.root, run_id=terminal_run, phase="QUALITY_CONTROL_AGENT") + arm_controlled_interruption(self.root, run_id=terminal_run, phase="EXECUTE_AGENT") def test_operator_disarm_preserves_unconsumed_and_wrong_run_cannot_consume(self) -> None: run_id = "operator-disarm-run" self.store.save(TransactionState(run_id, "pcvantol/djconnect", "prompt.md", "LOCAL_REPOSITORY_VALIDATION")) - arm_controlled_interruption(self.root, run_id=run_id, phase="QUALITY_CONTROL_AGENT") - self.assertFalse(consume_controlled_interruption_hook(self.root, run_id="unrelated-run", phase="QUALITY_CONTROL_AGENT")) + arm_controlled_interruption(self.root, run_id=run_id, phase="EXECUTE_AGENT") + self.assertFalse(consume_controlled_interruption_hook(self.root, run_id="unrelated-run", phase="EXECUTE_AGENT")) self.assertFalse(consume_controlled_interruption_hook(self.root, run_id=run_id, phase="REPAIR_AGENT")) - self.assertEqual(disarm_controlled_interruption(self.root, run_id=run_id, phase="QUALITY_CONTROL_AGENT"), "DISARMED") - self.assertEqual(controlled_interruption_status(self.root, run_id=run_id, phase="QUALITY_CONTROL_AGENT"), "NOT_ARMED") + self.assertEqual(disarm_controlled_interruption(self.root, run_id=run_id, phase="EXECUTE_AGENT"), "DISARMED") + self.assertEqual(controlled_interruption_status(self.root, run_id=run_id, phase="EXECUTE_AGENT"), "NOT_ARMED") diff --git a/tests/engineering/test_qualification_runtime.py b/tests/engineering/test_qualification_runtime.py new file mode 100644 index 0000000..f72fce8 --- /dev/null +++ b/tests/engineering/test_qualification_runtime.py @@ -0,0 +1,127 @@ +"""Unit coverage for the explicit, local-only qualification provider seam.""" + +from __future__ import annotations + +import os +from pathlib import Path +import subprocess +import tempfile +import unittest +from types import SimpleNamespace +from unittest.mock import patch + +from engineering_platform.qualification_runtime import ( + DeterministicQualificationAgent, + LocalQualificationGitHub, +) + + +class DeterministicQualificationRuntimeTests(unittest.TestCase): + def setUp(self) -> None: + self.temporary = tempfile.TemporaryDirectory() + self.root = Path(self.temporary.name) + subprocess.run(("git", "init", "-b", "main", str(self.root)), check=True, capture_output=True) + subprocess.run(("git", "-C", str(self.root), "config", "user.email", "qualification@example.invalid"), check=True) + subprocess.run(("git", "-C", str(self.root), "config", "user.name", "Qualification"), check=True) + (self.root / "README.md").write_text("fixture\n", encoding="utf-8") + subprocess.run(("git", "-C", str(self.root), "add", "README.md"), check=True) + subprocess.run(("git", "-C", str(self.root), "commit", "-m", "fixture"), check=True, capture_output=True) + + def tearDown(self) -> None: + self.temporary.cleanup() + + def test_local_genesis_managed_and_reconciliation_results_are_deterministic(self) -> None: + agent = DeterministicQualificationAgent() + process = [] + agent.set_process_callback(process.append) + + managed = agent.invoke(self.root, "implement the approved work") + publication = agent.invoke(self.root, "First implementation pull-request publication gate") + reconciled = agent.invoke(self.root, "the sole automatic post-finalization reconciliation") + genesis = agent.invoke(self.root, f"Execution mode: Genesis\nTarget repository: {self.root}") + + self.assertEqual(process[0]["pid"], os.getpid()) + self.assertEqual(managed.branch, "qualification-managed") + self.assertIsNone(managed.pull_request) + self.assertEqual(publication.pull_request, 1) + self.assertEqual(reconciled.terminal_condition, "repository_reconciled") + self.assertEqual(genesis.terminal_condition, "local_commit_reconciled") + self.assertEqual(genesis.repository_path, str(self.root.resolve())) + self.assertTrue(agent.available()) + self.assertEqual(agent.version(), "0.153.4") + + def test_finalization_branch_contract_and_external_target_gate(self) -> None: + agent = DeterministicQualificationAgent() + prompt = "Create Finalization PR on exactly `qualification-finalize`." + self.assertEqual(agent._finalization_branch(prompt), "qualification-finalize") + with self.assertRaisesRegex(RuntimeError, "QUALIFICATION_FINALIZATION_BRANCH_UNAVAILABLE"): + agent._finalization_branch("no branch") + with patch.dict(os.environ, {}, clear=True): + self.assertFalse(agent._github_write_target(self.root)) + with patch.dict(os.environ, { + "EP_QUALIFICATION_GITHUB_WRITE_FLOW": "1", + "EP_QUALIFICATION_GITHUB_REPOSITORY": "owner/repository", + }, clear=True), patch("engineering_platform.qualification_runtime.subprocess.run") as run: + run.return_value.returncode = 0 + run.return_value.stdout = "https://github.com/owner/repository.git\n" + self.assertTrue(agent._github_write_target(self.root)) + run.return_value.stdout = "https://github.com/other/repository.git\n" + self.assertFalse(agent._github_write_target(self.root)) + run.return_value.returncode = 1 + self.assertFalse(agent._github_write_target(self.root)) + + def test_controlled_interruption_window_is_explicit_and_bounded(self) -> None: + agent = DeterministicQualificationAgent() + ready = self.root / "arm-ready.json" + enable = ready.with_suffix(".json.enable") + enable.write_text("enabled\n", encoding="utf-8") + ready.with_suffix(".json.continue").write_text("continue\n", encoding="utf-8") + with patch.dict(os.environ, {"EP_QUALIFICATION_CONTROL_ARM_READY_FILE": str(ready)}, clear=True): + self.assertIsNone(agent.wait_for_controlled_interruption_arm(self.root, SimpleNamespace(run_id="run-a"))) + self.assertEqual(ready.read_text(encoding="utf-8"), '{"run_id": "run-a", "phase": "EXECUTE_AGENT"}') + with patch.dict(os.environ, {}, clear=True): + self.assertIsNone(agent.wait_for_controlled_interruption_arm(self.root, SimpleNamespace(run_id="run-a"))) + + def test_external_handoff_writes_only_the_explicit_fixture_contract(self) -> None: + agent = DeterministicQualificationAgent() + (self.root / ".engineering-platform").mkdir() + completed = SimpleNamespace(stdout="17\n", returncode=0) + environment = { + "EP_QUALIFICATION_GITHUB_REPOSITORY": "owner/fixture", + "EP_QUALIFICATION_GITHUB_BRANCH": "qualification-managed", + } + with patch.dict(os.environ, environment, clear=True), patch( + "engineering_platform.qualification_runtime.subprocess.run", return_value=completed + ) as run: + handoff = agent._create_github_managed_handoff(self.root) + recovery = agent._create_github_managed_handoff(self.root, prompt="controlled recovery qualification") + finalization = agent._create_github_managed_handoff( + self.root, finalization=True, prompt="Finalization PR on exactly `qualification-finalize`." + ) + self.assertEqual((handoff.branch, handoff.pull_request), ("qualification-managed", 17)) + self.assertEqual(recovery.branch, "qualification-managed-recovery") + self.assertEqual(finalization.branch, "qualification-finalize") + self.assertTrue((self.root / ".engineering-platform" / "managed-github-e2e-proof.json").is_file()) + self.assertTrue((self.root / ".engineering-platform" / "managed-github-e2e-finalization-proof.json").is_file()) + self.assertGreaterEqual(run.call_count, 21) + with patch.dict(os.environ, {}, clear=True): + with self.assertRaisesRegex(RuntimeError, "QUALIFICATION_GITHUB_WRITE_CONFIGURATION_INVALID"): + agent._create_github_managed_handoff(self.root) + self.assertEqual(agent.review(self.root, SimpleNamespace(reviewer="quality"), "objective").reviewer, "quality") + + def test_local_github_adapter_models_open_then_merged_evidence(self) -> None: + adapter = LocalQualificationGitHub(self.root) + opened = adapter.pull_request(7) + merged = adapter.pull_request(7) + + self.assertEqual((opened.state, opened.head_branch), ("OPEN", "qualification-managed")) + self.assertEqual((merged.state, merged.head_branch), ("MERGED", "qualification-managed")) + self.assertTrue(merged.merge_commit) + self.assertIsNone(adapter.pull_request_for_head_branch("ignored")) + self.assertFalse(adapter.normalize_markdown_body(7)) + self.assertIsNone(adapter.ready(7)) + self.assertIsNone(adapter.merge(7)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/engineering/test_receipt_run_provenance.py b/tests/engineering/test_receipt_run_provenance.py index b70b4b3..5cbcd1b 100644 --- a/tests/engineering/test_receipt_run_provenance.py +++ b/tests/engineering/test_receipt_run_provenance.py @@ -68,7 +68,11 @@ def _downgrade_to_schema_51(self) -> None: db.execute("CREATE TABLE ep_installations (instance_id TEXT PRIMARY KEY, created_at TEXT NOT NULL, schema_version INTEGER NOT NULL CHECK(schema_version IN (41,42,43,44,45,46,47,48,49,50,51)))") db.execute("INSERT INTO ep_installations(instance_id,created_at,schema_version) SELECT instance_id,created_at,51 FROM ep_installations_schema52") db.execute("DROP TABLE ep_installations_schema52") - db.execute("DELETE FROM engineering_schema_migrations WHERE version IN (52,53)") + # Keep the canary a genuine schema-51 installation. Newer Server + # migrations may already have been applied by setUp; retaining one + # would make `_schema_version` report the current schema and skip + # the provenance upgrade this fixture is meant to exercise. + db.execute("DELETE FROM engineering_schema_migrations WHERE version >= 52") db.execute("UPDATE engineering_metadata SET value='51' WHERE key='installation.schema_version'") db.execute("PRAGMA legacy_alter_table=OFF") @@ -78,7 +82,10 @@ def test_schema_51_upgrade_backfills_every_verified_canonical_binding(self) -> N server.initialize(self.root) with sqlite3.connect(self.database) as db: self.assertEqual(db.execute("SELECT submission_id,run_id,project_id,repository_id,installation_id FROM ep_receipt_run_provenance ORDER BY submission_id").fetchall(), [("sub-a", "run-a", "project-a", "repo-a", self.installation), ("sub-b", "run-b", "project-b", "repo-b", self.installation)]) - self.assertEqual(db.execute("SELECT MAX(version) FROM engineering_schema_migrations").fetchone()[0], 53) + self.assertEqual( + db.execute("SELECT MAX(version) FROM engineering_schema_migrations").fetchone()[0], + server.SERVER_STORE_SCHEMA_VERSION, + ) def test_schema_51_upgrade_rejects_an_incomplete_or_conflicting_import(self) -> None: self._downgrade_to_schema_51() diff --git a/tests/engineering/test_server_foundation.py b/tests/engineering/test_server_foundation.py index b485ea1..0dd6337 100644 --- a/tests/engineering/test_server_foundation.py +++ b/tests/engineering/test_server_foundation.py @@ -47,6 +47,141 @@ def test_empty_installation_bootstraps_a_clean_valid_store(self) -> None: self.assertFalse(report["running"]) self.assertFalse((self.root / ".engineering").exists()) + def test_queue_disposition_http_helpers_reject_ambiguous_json_and_foreign_origins(self) -> None: + """The queue mutation boundary treats origin and JSON ambiguity safely.""" + self.assertTrue(server._same_origin({"Host": "localhost:8765"})) + self.assertTrue(server._same_origin({"Host": "localhost:8765", "Origin": "https://localhost:8765"})) + self.assertFalse(server._same_origin({"Host": "localhost:8765", "Origin": "https://other.example"})) + self.assertEqual(server._strict_json_object(b'{"operation_id":"once"}'), {"operation_id": "once"}) + with self.assertRaises(ValueError): + server._strict_json_object(b'{"operation_id":"first","operation_id":"second"}') + with self.assertRaises(ValueError): + server._strict_json_object(b'[]') + + def test_disconnected_read_only_probe_does_not_emit_server_traceback(self) -> None: + """A client closing a health response is not an operational server failure.""" + class DisconnectedWriter: + def write(self, _: bytes) -> None: + raise BrokenPipeError("probe closed") + + class DetachedHandler: + wfile = DisconnectedWriter() + + def send_response(self, _: int) -> None: pass + def send_header(self, _: str, __: str) -> None: pass + def end_headers(self) -> None: pass + + handler = DetachedHandler() + server._HealthHandler._send(handler, 200, {"ready": True}) + server._HealthHandler._send_ndjson(handler, [{"event": "health"}]) + + def test_server_presentation_boundaries_reject_unsafe_headers_and_normalize_quota_data(self) -> None: + """Console-only helpers remain fail-closed for unsafe or malformed inputs.""" + self.assertEqual( + server._attachment_content_disposition("qualification-report.md"), + 'attachment; filename="qualification-report.md"', + ) + self.assertEqual( + server._report_content_disposition("run-123"), + 'attachment; filename="engineering-report-run-123.md"', + ) + for unsafe in (None, "report\nInjected: value", "../../report.md"): + with self.assertRaises(ValueError): + server._attachment_content_disposition(unsafe) + for unsafe_id in (None, "../run", "run\r\n"): + with self.assertRaises(ValueError): + server._report_content_disposition(unsafe_id) + + self.assertIsNone(server._remaining_rate_limit_capacity({})) + self.assertIsNone(server._remaining_rate_limit_capacity({"windows": ["malformed", {"used_percent": True}]})) + self.assertEqual(server._remaining_rate_limit_capacity({"windows": [{"used_percent": -4}, {"used_percent": 125}]}), 0.0) + normalized = server._normalize_rate_limits({ + "rateLimits": { + "primary": {"usedPercent": 19.7, "windowDurationMins": 300, "resetsAt": 12}, + "secondary": {"usedPercent": True, "windowDurationMins": 60, "resetsAt": 13}, + }, + "rateLimitResetCredits": {"availableCount": 2}, + }) + self.assertEqual(normalized, { + "windows": [{"label": "5-uursvenster", "used_percent": 20, "window_minutes": 300, "resets_at": 12}], + "reset_credits": 2, + }) + self.assertEqual(server._rate_limit_window_label(10_080), "Weekvenster") + self.assertEqual(server._rate_limit_window_label(2_880), "2-daags venster") + self.assertEqual(server._rate_limit_window_label(120), "2-uursvenster") + self.assertEqual(server._rate_limit_window_label(15), "15-minutenvenster") + + def test_github_rate_limit_projection_handles_unavailable_and_exhausted_resources(self) -> None: + with patch("engineering_platform.server.GitHubProvider") as provider: + provider.return_value.github.side_effect = RuntimeError("rate limit unavailable") + self.assertEqual(server._github_rate_limit_status(), {"limited": True}) + provider.return_value.github.side_effect = None + provider.return_value.github.return_value = json.dumps({"resources": {"core": {"remaining": 3}}}) + self.assertEqual(server._github_rate_limit_status(), {"limited": False}) + provider.return_value.github.return_value = json.dumps({ + "resources": { + "core": {"remaining": 0, "reset": 50}, + "graphql": {"remaining": -1, "reset": 20}, + "search": "malformed", + }, + }) + self.assertEqual(server._github_rate_limit_status(), {"limited": True, "reset_at": 20}) + + def test_component_log_query_contract_rejects_invalid_filters_before_central_sql(self) -> None: + query = server._parse_central_log_query({ + "page": ["2"], "page_size": ["25"], "start": ["2026-09-01"], "end": ["2026-09-02"], + "inclusive_end": ["1"], "search": ["%_term"], "level": ["warning"], + "event": [" run.finished ", "run.started", "run.started"], "sort": ["event"], "direction": ["asc"], + }) + self.assertEqual((query.page, query.page_size, query.level, query.events, query.sort_key, query.direction), + (2, 25, "WARNING", ("run.finished", "run.started"), "event", "asc")) + self.assertTrue(query.inclusive_end) + self.assertEqual(server._central_log_components("all")[0], PLATFORM_COMPONENT_IDS) # type: ignore[index] + self.assertEqual(server._central_log_components("ep_server")[1], ("ep_server",)) # type: ignore[index] + self.assertIsNone(server._central_log_components("not-a-component")) + for invalid in ( + {"page": ["zero"]}, {"page": ["0"]}, {"page_size": ["501"]}, + {"level": ["verbose"]}, {"search": ["x" * 161]}, {"event": ["x" * 161]}, + {"sort": ["unknown"]}, {"direction": ["sideways"]}, + ): + with self.assertRaises(ValueError): + server._parse_central_log_query(invalid) + + def test_local_only_provider_controls_fail_closed_before_starting_external_processes(self) -> None: + with patch("engineering_platform.server.sys.platform", "linux"): + with self.assertRaisesRegex(ValueError, "LOCAL_DIRECTORY_PICKER_UNAVAILABLE"): + server._choose_local_directory(self.root) + with self.assertRaisesRegex(ValueError, "local macOS Server"): + server._start_provider_login(self.root, "GITHUB") + with self.assertRaisesRegex(ValueError, "Unsupported provider login"): + server._start_provider_login(self.root, "OTHER") + with self.assertRaisesRegex(ValueError, "Unsupported provider logout"): + server._logout_provider(self.root, "OTHER") + + def test_queue_operator_capability_commands_are_explicit(self) -> None: + parsed = server.build_parser().parse_args(( + "grant-operator-capability", "--project-id", "project", "--consumer-id", "operator", + "--capability", "QUEUE_DECLINE", + )) + self.assertEqual((parsed.command, parsed.project_id, parsed.consumer_id, parsed.capability), + ("grant-operator-capability", "project", "operator", "QUEUE_DECLINE")) + + def test_queue_operator_capability_grant_and_revoke_are_durable(self) -> None: + server.initialize(self.root) + with sqlite3.connect(self.root / server.SERVER_DATABASE_FILENAME) as connection: + server.project_topology.register_server_local_topology(connection, declaration={ + "schema_version": "1.0", "project": {"id": "queue-project", "authority_repository_id": "queue-repository"}, + "repository": {"id": "queue-repository", "role": "authority"}, "validation": {"kind": "none"}, + }) + from engineering_platform.submission_service import issue_consumer_credential + issue_consumer_credential(connection, consumer_id="queue-operator", project_id="queue-project") + arguments = ("--data-root", str(self.root), "--project-id", "queue-project", "--consumer-id", "queue-operator", "--capability", "QUEUE_DECLINE") + with redirect_stdout(io.StringIO()): + self.assertEqual(server.main(("grant-operator-capability", *arguments)), 0) + self.assertEqual(server.main(("revoke-operator-capability", *arguments)), 0) + with sqlite3.connect(self.root / server.SERVER_DATABASE_FILENAME) as connection: + self.assertIsNotNone(connection.execute("SELECT revoked_at FROM ep_operator_capabilities WHERE consumer_id='queue-operator' AND project_id='queue-project' AND capability='QUEUE_DECLINE'").fetchone()[0]) + def test_execution_runtime_status_preserves_the_virtual_environment_launcher(self) -> None: launcher = Path(self.temporary.name) / "venv" / "bin" / "python" launcher.parent.mkdir(parents=True) diff --git a/tests/engineering/test_submission_service.py b/tests/engineering/test_submission_service.py index 11d369c..d76677f 100644 --- a/tests/engineering/test_submission_service.py +++ b/tests/engineering/test_submission_service.py @@ -26,6 +26,8 @@ def setUp(self) -> None: connection.execute("INSERT INTO ep_project_registrations VALUES(?,?,?,?,?)", ("djconnect", "{}", "ACTIVE", now, now)) connection.execute("INSERT INTO ep_repository_registrations VALUES(?,?,?,?,?,?,?)", ("djconnect", "djconnect", "djconnect", "authority", "{}", now, now)) self.credential = submission_service.issue_consumer_credential(connection, consumer_id="cli", project_id="djconnect")["credential"] + connection.execute("INSERT INTO ep_operator_capabilities VALUES(?,?,?,?,?)", ("cli", "djconnect", "QUEUE_HOLD_RESUME", now, None)) + connection.execute("INSERT INTO ep_operator_capabilities VALUES(?,?,?,?,?)", ("cli", "djconnect", "QUEUE_DECLINE", now, None)) def tearDown(self) -> None: server.stop(self.root) @@ -47,6 +49,94 @@ def test_service_preserves_cross_transport_idempotency_and_history(self) -> None self.assertEqual(http.submission_id, cli.submission_id) self.assertEqual(connection.execute("SELECT count(*) FROM ep_submission_prompt_history").fetchone()[0], 1) + def test_operator_dispositions_are_audited_and_only_resumable_from_hold_states(self) -> None: + with sqlite3.connect(self.root / server.SERVER_DATABASE_FILENAME) as connection: + submitted = submission_service.submit(connection, submission_service.request_from_mapping("djconnect", self.payload("operator"), transport="HTTP")) + held = submission_service.operator_queue_disposition( + connection, project_id="djconnect", submission_id=submitted.submission_id, + disposition="QUARANTINED", reason="Needs operator review", + ) + self.assertEqual(held["state"], "QUARANTINED") + readback = submission_service.producer_readback(connection, project_id="djconnect", submission_id=submitted.submission_id) + self.assertEqual(readback["disposition"], {"state": "QUARANTINED", "terminal": False, "execution_eligible": False, "revision": 1, "operation_id": None, "event_reference": None, "reason": "NOT_RECORDED", "actor_reference": "NOT_RECORDED", "recorded_at": None}) + self.assertEqual( + submission_service.producer_readback(connection, project_id="djconnect", submission_id=submitted.submission_id)["submission"]["state"], + "QUARANTINED", + ) + self.assertEqual(submission_service.operator_queue_disposition( + connection, project_id="djconnect", submission_id=submitted.submission_id, + disposition="DECLINED", reason="The operator rejected the quarantined submission", + )["state"], "DECLINED") + declined_readback = submission_service.producer_readback(connection, project_id="djconnect", submission_id=submitted.submission_id) + self.assertIsNone(declined_readback["run"]) + self.assertEqual(declined_readback["result"]["outcome"], "NOT_STARTED") + self.assertTrue(declined_readback["disposition"]["terminal"]) + events = [row[0] for row in connection.execute("SELECT event_kind FROM ep_submission_events WHERE submission_id=? ORDER BY event_id", (submitted.submission_id,))] + self.assertEqual(events[-2:], ["OPERATOR_QUEUE_QUARANTINED", "OPERATOR_QUEUE_DECLINED"]) + self.assertNotIn("OPERATOR_QUEUE_QUEUED", events[-2:]) + submitted = submission_service.submit(connection, submission_service.request_from_mapping("djconnect", self.payload("resume"), transport="HTTP")) + held = submission_service.operator_queue_disposition( + connection, project_id="djconnect", submission_id=submitted.submission_id, + disposition="QUARANTINED", reason="Needs operator review", + ) + resumed = submission_service.operator_queue_disposition( + connection, project_id="djconnect", submission_id=submitted.submission_id, + disposition="QUEUED", reason="Review completed", + ) + self.assertEqual(resumed["state"], "QUEUED") + events = [row[0] for row in connection.execute("SELECT event_kind FROM ep_submission_events WHERE submission_id=? ORDER BY event_id", (submitted.submission_id,))] + self.assertEqual(events[-2:], ["OPERATOR_QUEUE_QUARANTINED", "OPERATOR_QUEUE_QUEUED"]) + + deferred = submission_service.operator_queue_disposition( + connection, project_id="djconnect", submission_id=submitted.submission_id, + disposition="DEFERRED", reason="Schedule later", + ) + self.assertEqual(deferred["state"], "DEFERRED") + self.assertEqual(submission_service.operator_queue_disposition( + connection, project_id="djconnect", submission_id=submitted.submission_id, + disposition="QUEUED", reason="Schedule resumed", + )["state"], "QUEUED") + self.assertEqual(submission_service.operator_queue_disposition( + connection, project_id="djconnect", submission_id=submitted.submission_id, + disposition="DECLINED", reason="No longer wanted", + )["state"], "DECLINED") + with self.assertRaisesRegex(submission_service.SubmissionError, "QUEUE_DISPOSITION_CONFLICT"): + submission_service.operator_queue_disposition( + connection, project_id="djconnect", submission_id=submitted.submission_id, + disposition="QUEUED", reason="Must not revive a decline", + ) + + def test_claimed_submission_rejects_queue_mutation_without_audit_event(self) -> None: + with sqlite3.connect(self.root / server.SERVER_DATABASE_FILENAME) as connection: + submitted = submission_service.submit(connection, submission_service.request_from_mapping("djconnect", self.payload("claimed"), transport="HTTP")) + connection.execute("INSERT INTO ep_execution_runs(run_id,project_id,state,created_at,updated_at,execution_mode) VALUES(?,?,?,?,?,?)", ("run-claimed", "djconnect", "CLAIMED", "now", "now", "MANAGED")) + connection.execute("INSERT INTO ep_parity_lifecycle_dispatches(submission_id,project_id,repository_id,run_id,state,prompt_path,claimed_at,updated_at,operator_resolution) VALUES(?,?,?,?,?,?,?,?,?)", (submitted.submission_id, "djconnect", "djconnect", "run-claimed", "CLAIMED", "prompt", "now", "now", "NONE")) + before = connection.execute("SELECT state,disposition_revision FROM ep_submissions WHERE submission_id=?", (submitted.submission_id,)).fetchone() + with self.assertRaisesRegex(submission_service.SubmissionError, "QUEUE_DISPOSITION_CONFLICT"): + submission_service.operator_queue_disposition(connection, project_id="djconnect", submission_id=submitted.submission_id, disposition="QUARANTINED", reason="Too late") + self.assertEqual(connection.execute("SELECT state,disposition_revision FROM ep_submissions WHERE submission_id=?", (submitted.submission_id,)).fetchone(), before) + self.assertEqual(connection.execute("SELECT COUNT(*) FROM ep_submission_events WHERE submission_id=? AND event_kind LIKE 'OPERATOR_QUEUE_%'", (submitted.submission_id,)).fetchone()[0], 0) + + def test_queue_operation_id_replays_once_and_rejects_payload_collision(self) -> None: + with sqlite3.connect(self.root / server.SERVER_DATABASE_FILENAME) as connection: + submitted = submission_service.submit(connection, submission_service.request_from_mapping("djconnect", self.payload("operation"), transport="HTTP")) + first = submission_service.operator_queue_disposition(connection, project_id="djconnect", submission_id=submitted.submission_id, disposition="QUARANTINED", reason="Investigate source", expected_state="QUEUED", expected_revision=0, operation_id="queue-operation-1", actor_reference="operator-a") + replay = submission_service.operator_queue_disposition(connection, project_id="djconnect", submission_id=submitted.submission_id, disposition="QUARANTINED", reason="Investigate source", expected_state="QUEUED", expected_revision=0, operation_id="queue-operation-1", actor_reference="operator-a") + self.assertEqual((first["state"], replay["state"]), ("QUARANTINED", "QUARANTINED")) + self.assertTrue(replay["replayed"]) + self.assertEqual(connection.execute("SELECT COUNT(*) FROM ep_submission_events WHERE submission_id=? AND event_kind='OPERATOR_QUEUE_QUARANTINED'", (submitted.submission_id,)).fetchone()[0], 1) + with self.assertRaisesRegex(submission_service.SubmissionError, "OPERATION_ID_CONFLICT"): + submission_service.operator_queue_disposition(connection, project_id="djconnect", submission_id=submitted.submission_id, disposition="DECLINED", reason="Different command", expected_state="QUARANTINED", expected_revision=1, operation_id="queue-operation-1", actor_reference="operator-a") + + def test_queue_disposition_rejects_non_string_or_control_reason_without_mutation(self) -> None: + with sqlite3.connect(self.root / server.SERVER_DATABASE_FILENAME) as connection: + submitted = submission_service.submit(connection, submission_service.request_from_mapping("djconnect", self.payload("invalid-reason"), transport="HTTP")) + for reason in (None, {}, [], True, "\x00bad", "\n"): + with self.assertRaisesRegex(submission_service.SubmissionError, "INVALID_QUEUE_DISPOSITION"): + submission_service.operator_queue_disposition(connection, project_id="djconnect", submission_id=submitted.submission_id, disposition="QUARANTINED", reason=reason) # type: ignore[arg-type] + self.assertEqual(connection.execute("SELECT state,disposition_revision FROM ep_submissions WHERE submission_id=?", (submitted.submission_id,)).fetchone(), ("QUEUED", 0)) + self.assertEqual(connection.execute("SELECT COUNT(*) FROM ep_submission_events WHERE submission_id=? AND event_kind LIKE 'OPERATOR_QUEUE_%'", (submitted.submission_id,)).fetchone()[0], 0) + def test_http_auth_scope_and_acceptance(self) -> None: server.start(self.root) request = Request(f"http://127.0.0.1:{self.port}/v1/projects/djconnect/submissions", data=json.dumps(self.payload("http")).encode(), headers={"Authorization": f"Bearer {self.credential}", "Content-Type": "application/json"}, method="POST") @@ -58,6 +148,66 @@ def test_http_auth_scope_and_acceptance(self) -> None: urlopen(wrong) # nosec B310 self.assertEqual(rejected.exception.code, 401) + def test_console_queue_actions_change_only_the_selected_submission_and_are_audited(self) -> None: + """Exercise the browser-facing queue action endpoint against CENTRAL.""" + with sqlite3.connect(self.root / server.SERVER_DATABASE_FILENAME) as connection: + submitted = submission_service.submit( + connection, submission_service.request_from_mapping("djconnect", self.payload("console-actions"), transport="HTTP"), + ) + server.start(self.root) + endpoint = f"http://127.0.0.1:{self.port}/api/queue-disposition?project=djconnect" + + current_state, current_revision = "QUEUED", 0 + def action(disposition: str, reason: str) -> dict[str, object]: + nonlocal current_state, current_revision + revision, expected_state = current_revision, current_state + body = json.dumps({"contract_version": "1.0", "operation_id": f"operation-{disposition}-{revision}", "submission_id": submitted.submission_id, "expected_state": expected_state, "expected_revision": revision, "disposition": disposition, "reason": reason}).encode() + request = Request(endpoint, data=body, method="POST", headers={ + "Content-Type": "application/json", "Origin": f"http://127.0.0.1:{self.port}", "Authorization": f"Bearer {self.credential}", + }) + with urlopen(request) as response: # nosec B310 + result = json.loads(response.read()) + current_state, current_revision = result["state"], result["resulting_revision"] + return result + + self.assertEqual(action("DEFERRED", "Wait for the maintenance window")["state"], "DEFERRED") + self.assertEqual(action("QUEUED", "Maintenance window is open")["state"], "QUEUED") + # Quarantine is a separate operator hold and must be independently resumable. + self.assertEqual(action("QUARANTINED", "Investigate the source envelope")["state"], "QUARANTINED") + self.assertEqual(action("QUEUED", "Investigation completed")["state"], "QUEUED") + self.assertEqual(action("DECLINED", "The request is no longer needed")["state"], "DECLINED") + body = json.dumps({"contract_version": "1.0", "operation_id": "must-not-revive", "submission_id": submitted.submission_id, "expected_state": "DECLINED", "expected_revision": current_revision, "disposition": "QUEUED", "reason": "Must not revive a declined request"}).encode() + with self.assertRaises(HTTPError) as rejected: + urlopen(Request(endpoint, data=body, method="POST", headers={"Content-Type": "application/json", "Origin": f"http://127.0.0.1:{self.port}", "Authorization": f"Bearer {self.credential}"})) # nosec B310 + self.assertEqual(rejected.exception.code, 409) + with sqlite3.connect(self.root / server.SERVER_DATABASE_FILENAME) as connection: + events = [row[0] for row in connection.execute( + "SELECT event_kind FROM ep_submission_events WHERE submission_id=? ORDER BY event_id", (submitted.submission_id,) + )] + self.assertEqual(events[-5:], [ + "OPERATOR_QUEUE_DEFERRED", "OPERATOR_QUEUE_QUEUED", + "OPERATOR_QUEUE_QUARANTINED", "OPERATOR_QUEUE_QUEUED", "OPERATOR_QUEUE_DECLINED", + ]) + + def test_console_queue_actions_reject_cross_origin_and_unknown_project(self) -> None: + with sqlite3.connect(self.root / server.SERVER_DATABASE_FILENAME) as connection: + submitted = submission_service.submit( + connection, submission_service.request_from_mapping("djconnect", self.payload("console-denial"), transport="HTTP"), + ) + server.start(self.root) + body = json.dumps({"submission_id": submitted.submission_id, "disposition": "DEFERRED", "reason": "Later"}).encode() + cases = ( + (f"http://127.0.0.1:{self.port}/api/queue-disposition?project=djconnect", "https://untrusted.example", 403), + (f"http://127.0.0.1:{self.port}/api/queue-disposition?project=other", f"http://127.0.0.1:{self.port}", 409), + ) + for endpoint, origin, expected in cases: + request = Request(endpoint, data=body, method="POST", headers={"Content-Type": "application/json", "Origin": origin}) + with self.assertRaises(HTTPError) as rejected: + urlopen(request) # nosec B310 + self.assertEqual(rejected.exception.code, expected) + with sqlite3.connect(self.root / server.SERVER_DATABASE_FILENAME) as connection: + self.assertEqual(connection.execute("SELECT state FROM ep_submissions WHERE submission_id=?", (submitted.submission_id,)).fetchone()[0], "QUEUED") + def test_authenticated_producer_readback_is_exactly_correlated_and_terminal_evidence_backed(self) -> None: server.start(self.root) payload = self.payload("readback") @@ -82,7 +232,7 @@ def test_authenticated_producer_readback_is_exactly_correlated_and_terminal_evid endpoint = f"http://127.0.0.1:{self.port}/v1/projects/djconnect/submissions/{submission_id}" with urlopen(Request(endpoint, headers={"Authorization": f"Bearer {self.credential}"})) as response: # nosec B310 initial = json.loads(response.read()) - self.assertEqual(initial["contract_version"], "1.1") + self.assertEqual(initial["contract_version"], "1.2") self.assertEqual(initial["correlation"], { "correlation_id": "forge-correlation-1", "mission_id": "mission-1", "engineering_action_id": "action-1", }) @@ -94,7 +244,13 @@ def test_authenticated_producer_readback_is_exactly_correlated_and_terminal_evid connection.execute("INSERT INTO ep_execution_runs(run_id,project_id,state,created_at,updated_at,execution_mode) VALUES(?,?,?,?,?,?)", ("run-readback", "djconnect", "COMPLETE", "now", "now", "MANAGED")) connection.execute("INSERT INTO execution_runs(run_id,execution_date,arrived_at,execution_started_at,execution_finished_at,queue_wait_seconds,execution_seconds,terminal_state,input_tokens,output_tokens,total_tokens,execution_mode,workspace,repository,execution_host_version) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", ("run-readback", "2026-01-01", "now", "now", "now", 0, 0, "COMPLETE", None, None, None, "MANAGED", "djconnect", "djconnect", "test")) connection.execute("INSERT INTO ep_parity_lifecycle_dispatches(submission_id,project_id,repository_id,run_id,state,prompt_path,claimed_at,updated_at,operator_resolution) VALUES(?,?,?,?,?,?,?,?,?)", (submission_id, "djconnect", "djconnect", "run-readback", "COMPLETE", "/private/prompt", "now", "now", "NONE")) - checkpoint = TransactionState(run_id="run-readback", repository="djconnect", prompt_path="prompt", phase="COMPLETE", terminal=True, action_intent="VALIDATION_ONLY") + profile = {"version": "validation-profile@1", "digest": "sha256:" + "b" * 64, "candidate_sha": "c" * 40} + finding = {"id": "security-1", "fingerprint": "d" * 32, "category": "SECURITY", "criterion": "post_implementation_assurance", "observation": "Project isolation lacks a negative test.", "severity": "HIGH", "confidence": "MEDIUM", "blocking": True, "disposition": "OPEN"} + reviews = ( + {"reviewer": "quality", "status": "PASS", "candidate_sha": "c" * 40, "profile_digest": profile["digest"], "invocation_id": "quality-1", "findings": []}, + {"reviewer": "security", "status": "FAIL", "candidate_sha": "c" * 40, "profile_digest": profile["digest"], "invocation_id": "security-1", "findings": [finding]}, + ) + checkpoint = TransactionState(run_id="run-readback", repository="djconnect", prompt_path="prompt", phase="COMPLETE", terminal=True, action_intent="VALIDATION_ONLY", assurance_profile=profile, assurance_reviews=reviews, repair_iterations=2) connection.execute("INSERT INTO engineering_transactions(run_id,payload,phase,updated_at) VALUES(?,?,?,?)", ("run-readback", json.dumps(checkpoint.to_dict()), "COMPLETE", "now")) connection.execute("INSERT INTO prompt_execution_history(run_id,terminal_state,prompt_title,executed_at,git_commit,report_path,updated_at) VALUES(?,?,?,?,?,?,?)", ("run-readback", "COMPLETE", "safe", "now", None, "/private/report", "now")) artifact_id = submission_service.write_terminal_evidence(self.root, repository_root=self.root, run_id="run-readback") @@ -104,6 +260,8 @@ def test_authenticated_producer_readback_is_exactly_correlated_and_terminal_evid stored_artifact = submission_service.producer_evidence_artifact(connection, project_id="djconnect", artifact_id=artifact_id) self.assertIsNotNone(stored_artifact) self.assertEqual(json.loads(stored_artifact or b"{}")['run']['id'], "run-readback") + findings_artifact = submission_service.producer_evidence_artifact(connection, project_id="djconnect", artifact_id="assurance-findings:run-readback") + self.assertEqual(json.loads(findings_artifact or b"{}")["reviews"], list(reviews)) with urlopen(Request(endpoint, headers={"Authorization": f"Bearer {self.credential}"})) as response: # nosec B310 terminal = json.loads(response.read()) self.assertEqual(terminal["run"]["id"], "run-readback") @@ -116,6 +274,8 @@ def test_authenticated_producer_readback_is_exactly_correlated_and_terminal_evid artifact = json.loads(response.read()) self.assertEqual(artifact["submission"]["id"], submission_id) self.assertEqual(artifact["run"]["id"], "run-readback") + self.assertEqual(artifact["assurance"]["repair_rounds"], {"used": 2, "maximum": 3}) + self.assertEqual(artifact["assurance"]["findings"]["artifact"]["id"], "assurance-findings:run-readback") # The projection is CENTRAL state, not a process-local cache: a Server # restart preserves the exact submission/run/evidence correlation. @@ -153,6 +313,35 @@ def test_authenticated_producer_readback_is_exactly_correlated_and_terminal_evid self.assertFalse(corrupt["result"]["delivery_qualified"]) # type: ignore[index] self.assertIsNone(submission_service.producer_evidence_artifact(connection, project_id="djconnect", artifact_id=artifact_id)) + def test_terminal_assurance_requires_a_complete_current_pair_and_explicit_historical_resolution(self) -> None: + """A missing/stale review cannot become PASS through empty aggregation.""" + candidate, digest = "c" * 40, "sha256:" + "b" * 64 + profile = {"version": "validation-profile@1", "digest": digest, "candidate_sha": candidate} + quality = {"reviewer": "quality", "status": "PASS", "candidate_sha": candidate, + "profile_digest": digest, "invocation_id": "quality-current", "findings": []} + security = {"reviewer": "security", "status": "PASS", "candidate_sha": candidate, + "profile_digest": digest, "invocation_id": "security-current", "findings": []} + base = dict(run_id="assurance-current", repository="djconnect", prompt_path="prompt", + phase="COMPLETE", terminal=True, assurance_profile=profile) + self.assertEqual(submission_service._current_assurance(TransactionState(**base))[0], "UNRESOLVED") + self.assertEqual(submission_service._current_assurance(TransactionState(**base, assurance_reviews=(quality,)))[0], "UNRESOLVED") + stale = {**security, "candidate_sha": "d" * 40} + self.assertEqual(submission_service._current_assurance(TransactionState(**base, assurance_reviews=(quality, stale)))[0], "UNRESOLVED") + + old_finding = {"id": "old-security", "fingerprint": "e" * 32, "category": "SECURITY", + "criterion": "isolation", "observation": "Missing denial test", "severity": "HIGH", + "confidence": "HIGH", "blocking": True, "disposition": "OPEN"} + old_security = {**security, "status": "FAIL", "invocation_id": "security-old", "findings": [old_finding]} + unresolved = TransactionState(**base, assurance_reviews=(old_security, quality, security)) + self.assertEqual(submission_service._current_assurance(unresolved)[0], "FAIL") + resolved = TransactionState( + **base, assurance_reviews=(old_security, quality, security), + assurance_resolutions=({"finding_id": "old-security", "disposition": "RESOLVED", + "resolution_ref": "repair:assurance-current:1|quality-current,security-current", + "candidate_sha": candidate},), + ) + self.assertEqual(submission_service._current_assurance(resolved)[0], "PASS") + def test_forge_provenance_is_required_and_part_of_idempotency_identity(self) -> None: payload = self.payload("forge-replay") payload.update({"producer": {"id": "forge", "type": "FORGE", "version": "1.0"}, diff --git a/tests/engineering/test_transport_authority_guard.py b/tests/engineering/test_transport_authority_guard.py index 371b84f..fa9ec5f 100644 --- a/tests/engineering/test_transport_authority_guard.py +++ b/tests/engineering/test_transport_authority_guard.py @@ -1,8 +1,11 @@ from __future__ import annotations import ast +import importlib.util from pathlib import Path +import subprocess import unittest +from unittest.mock import patch from engineering_platform.platform_components import ( PLATFORM_COMPONENT_IDS, @@ -122,6 +125,28 @@ def test_dependabot_canary_uses_one_bounded_named_wait(self) -> None: self.assertIn("DEPENDABOT_DISPATCH_TIMEOUT_SECONDS = 60", qualification) self.assertIn("timeout=DEPENDABOT_DISPATCH_TIMEOUT_SECONDS", qualification) + def test_installed_bootstrap_retries_only_the_transient_post_crash_store_window(self) -> None: + """Crash recovery may retry idempotent topology bootstrap, never credential issuance.""" + root = Path(__file__).resolve().parents[2] + specification = importlib.util.spec_from_file_location( + "p_transport_installed_ingress_matrix", + root / "tools" / "qualification" / "p_transport_installed_ingress_matrix.py", + ) + self.assertIsNotNone(specification) + module = importlib.util.module_from_spec(specification) # type: ignore[arg-type] + specification.loader.exec_module(module) # type: ignore[union-attr] + unavailable = subprocess.CompletedProcess(("server",), 2, "", '{"error":"EP Server store is unavailable.","ready":false}') + success = subprocess.CompletedProcess(("server",), 0, '{"result":"REGISTERED"}', "") + with patch.object(module.subprocess, "run", side_effect=(unavailable, success)) as invoked, \ + patch.object(module.time, "sleep") as slept: + self.assertEqual(module.command(Path("server"), "bootstrap-topology"), {"result": "REGISTERED"}) + self.assertEqual(invoked.call_count, 2) + slept.assert_called_once_with(module.BOOTSTRAP_TOPOLOGY_RETRY_DELAY_SECONDS) + with patch.object(module.subprocess, "run", return_value=unavailable) as invoked: + with self.assertRaisesRegex(RuntimeError, "issue-consumer-credential"): + module.command(Path("server"), "issue-consumer-credential") + invoked.assert_called_once() + def test_browser_fixture_uses_the_server_boundary_and_no_local_finder_route(self) -> None: """Dashboard browser evidence must not revive the retired direct listener.""" root = Path(__file__).resolve().parents[2] diff --git a/tools/qualification/advance_platform_build.py b/tools/qualification/advance_platform_build.py index 5df5ce5..11d43ea 100644 --- a/tools/qualification/advance_platform_build.py +++ b/tools/qualification/advance_platform_build.py @@ -1,19 +1,25 @@ #!/usr/bin/env python3 -"""Update the canonical Engineering Platform version for a wheel build. +"""Plan or apply an explicit, field-aware EP version operation. -The version is deliberately a checked-in, cross-surface release fact. A build -therefore advances it once before packaging rather than letting individual -Server, Console, Runner, or wheel metadata drift independently. +Normal builds only read the version already committed in ``pyproject.toml``. +This helper is the separate source mutation boundary; it has no publication or +qualification authority and its per-file atomic replacements are not claimed to +be a multi-file transaction. """ from __future__ import annotations import argparse +import json +import os from pathlib import Path import re +import tempfile +import tomllib _VERSION = re.compile(r"^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$") +POLICY_REVISION = "engineering-platform-bootstrap-release-cadence-v2" VERSION_PROJECTION_PATHS = ( "pyproject.toml", "package.json", @@ -25,46 +31,153 @@ ) -def _replace(path: Path, before: str, after: str) -> None: - text = path.read_text(encoding="utf-8") - if before not in text: - raise RuntimeError(f"canonical version {before} is absent from {path}") - path.write_text(text.replace(before, after), encoding="utf-8") +def _atomic_write(path: Path, text: str) -> None: + mode = path.stat().st_mode + fd, temporary = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent) + try: + with os.fdopen(fd, "w", encoding="utf-8") as handle: + handle.write(text) + handle.flush() + os.fsync(handle.fileno()) + os.chmod(temporary, mode) + os.replace(temporary, path) + except BaseException: + try: + os.unlink(temporary) + except FileNotFoundError: + pass + raise def _current_version(root: Path) -> str: root = root.resolve() - pyproject = root / "pyproject.toml" - match = re.search(r'^version = "([^"]+)"$', pyproject.read_text(encoding="utf-8"), re.MULTILINE) - if match is None or _VERSION.fullmatch(match.group(1)) is None: + try: + document = tomllib.loads((root / "pyproject.toml").read_text(encoding="utf-8")) + project = document["project"] + if not isinstance(document, dict) or not isinstance(project, dict): + raise TypeError("[project] must be an object") + value = project["version"] + except (OSError, KeyError, TypeError, tomllib.TOMLDecodeError) as error: + raise RuntimeError("the canonical package version is unreadable") from error + if not isinstance(value, str) or _VERSION.fullmatch(value) is None: raise RuntimeError("the canonical package version must be a stable X.Y.Z release") - return match.group(1) + return value + + +def _json_projection(path: Path, current: str, target: str, *fields: tuple[str, ...]) -> str: + try: + payload = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(payload, dict): + raise RuntimeError(f"invalid root object projection {path}") + for field in fields: + item: object = payload + for key in field[:-1]: + if not isinstance(item, dict) or key not in item: + raise RuntimeError(f"invalid object projection {path}") + item = item[key] + if not isinstance(item, dict) or not isinstance(item.get(field[-1]), str) or item[field[-1]] != current: + raise RuntimeError(f"canonical version projection drift in {path}") + item[field[-1]] = target + return json.dumps(payload, indent=2) + "\n" + except (OSError, KeyError, TypeError, json.JSONDecodeError) as error: + raise RuntimeError(f"invalid canonical version projection {path}") from error + + +def _python_projection(path: Path, current: str, target: str) -> str: + text = path.read_text(encoding="utf-8") + old = f'CURRENT_PLATFORM_VERSION = "{current}"' + if text.count(old) != 1: + raise RuntimeError(f"canonical runtime version projection drift in {path}") + return text.replace(old, f'CURRENT_PLATFORM_VERSION = "{target}"', 1) -def set_version(root: Path, version: str) -> str: +def _toml_projection(path: Path, current: str, target: str) -> str: + text = path.read_text(encoding="utf-8") + # TOML parsing establishes that this is the product's [project].version. + # The subsequent narrow replacement preserves the surrounding document + # rather than treating an arbitrary dependency/version line as canonical. + try: + document = tomllib.loads(text) + project = document["project"] + except (KeyError, TypeError, tomllib.TOMLDecodeError) as error: + raise RuntimeError(f"invalid canonical package projection {path}") from error + if not isinstance(project, dict) or project.get("version") != current: + raise RuntimeError(f"canonical package version projection drift in {path}") + section = re.compile(r"(?ms)^\[project\]\s*$\n(?P.*?)(?=^\[|\Z)") + match = section.search(text) + if match is None: + raise RuntimeError(f"canonical package version projection drift in {path}") + version = re.compile(rf'(?m)^(?P\s*version\s*=\s*)"{re.escape(current)}"\s*$') + matches = list(version.finditer(match.group("body"))) + if len(matches) != 1: + raise RuntimeError(f"canonical package version projection drift in {path}") + start = match.start("body") + matches[0].start() + end = match.start("body") + matches[0].end() + return text[:start] + f'{matches[0].group("prefix")}"{target}"' + text[end:] + + +def _prepared_writes(root: Path, current: str, target: str) -> dict[Path, str]: + paths = {relative: root / relative for relative in VERSION_PROJECTION_PATHS} + # Read and validate every projection before touching any source file. + return { + paths["pyproject.toml"]: _toml_projection(paths["pyproject.toml"], current, target), + paths["package.json"]: _json_projection(paths["package.json"], current, target, ("version",)), + paths["package-lock.json"]: _json_projection( + paths["package-lock.json"], current, target, ("version",), ("packages", "", "version"), + ), + paths["src/engineering_platform/ENGINEERING_PLATFORM_VERSION.json"]: _json_projection(paths["src/engineering_platform/ENGINEERING_PLATFORM_VERSION.json"], current, target, ("platform_version",)), + paths["src/engineering_platform/ENGINEERING_PLATFORM_CONFIG.json"]: _json_projection(paths["src/engineering_platform/ENGINEERING_PLATFORM_CONFIG.json"], current, target, ("platform", "version")), + paths["src/engineering_platform/templates/workspace-config.json"]: _json_projection(paths["src/engineering_platform/templates/workspace-config.json"], current, target, ("platform", "version")), + paths["src/engineering_platform/platform_version.py"]: _python_projection(paths["src/engineering_platform/platform_version.py"], current, target), + } + + +def set_version(root: Path, version: str, *, expected_version: str | None = None) -> str: """Set every public EP version projection to one stable release version.""" root = root.resolve() if _VERSION.fullmatch(version) is None: raise RuntimeError("the requested release version must be a stable X.Y.Z release") current = _current_version(root) - for relative_path in VERSION_PROJECTION_PATHS: - _replace(root / relative_path, current, version) + if expected_version is not None and expected_version != current: + raise RuntimeError(f"stale version operation: expected {expected_version}, found {current}") + writes = _prepared_writes(root, current, version) + if version == current: + return version + for path, text in writes.items(): + _atomic_write(path, text) return version -def advance(root: Path) -> str: - """Advance one patch number while retaining the current major/minor line.""" +def advance(root: Path, *, component: str = "patch") -> str: + """Advance one stable semantic-version component across all projections.""" current = _current_version(root) major, minor, patch = (int(part) for part in current.split(".")) - return set_version(root, f"{major}.{minor}.{patch + 1}") + if component == "none": + return current + if component == "patch": + target = f"{major}.{minor}.{patch + 1}" + elif component == "minor": + target = f"{major}.{minor + 1}.0" + else: + raise RuntimeError("version component must be none, patch or minor") + return set_version(root, target) def main(argv: list[str] | None = None) -> int: - parser = argparse.ArgumentParser(description="Advance one canonical EP wheel build number") + parser = argparse.ArgumentParser(description="Apply an explicit canonical EP version operation") parser.add_argument("--source-root", type=Path, default=Path.cwd()) parser.add_argument("--set-version", help="set all canonical projections to this exact stable X.Y.Z version") + parser.add_argument("--bump", choices=("none", "patch", "minor"), help="bootstrap release classification") + parser.add_argument("--expected-version") + parser.add_argument("--check", action="store_true") args = parser.parse_args(argv) - version = set_version(args.source_root, args.set_version) if args.set_version else advance(args.source_root) + if args.check: + if args.bump or args.set_version: parser.error("--check cannot change a version") + version = _current_version(args.source_root) + elif (args.bump is None) == (args.set_version is None): + parser.error("provide exactly one of --bump or --set-version") + else: + version = set_version(args.source_root, args.set_version, expected_version=args.expected_version) if args.set_version else advance(args.source_root, component=args.bump) print(f"EP_BUILD_VERSION={version}") return 0 diff --git a/tools/qualification/build_platform_wheel.py b/tools/qualification/build_platform_wheel.py index e3b0a09..f8ef7b2 100644 --- a/tools/qualification/build_platform_wheel.py +++ b/tools/qualification/build_platform_wheel.py @@ -8,19 +8,21 @@ import subprocess import sys -from advance_platform_build import advance, set_version +from advance_platform_build import _current_version def main(argv: list[str] | None = None) -> int: - parser = argparse.ArgumentParser(description="Advance, build, and optionally install an EP wheel") + parser = argparse.ArgumentParser(description="Build an already-versioned EP wheel without source mutation") parser.add_argument("--source-root", type=Path, default=Path.cwd()) parser.add_argument("--wheel-directory", type=Path) parser.add_argument("--install-python", type=Path) - parser.add_argument("--version", help="build this exact stable X.Y.Z release instead of advancing a patch") + parser.add_argument("--version", help="require this already-committed stable X.Y.Z release") parser.add_argument("--sdist", action="store_true", help="also create the matching source distribution") args = parser.parse_args(argv) root = args.source_root.resolve() - version = set_version(root, args.version) if args.version else advance(root) + version = _current_version(root) + if args.version is not None and args.version != version: + raise RuntimeError(f"requested build version {args.version} does not match committed source {version}; prepare it first") wheel_directory = (args.wheel_directory or root / "dist").resolve() wheel_directory.mkdir(parents=True, exist_ok=True) if args.sdist: diff --git a/tools/qualification/ep_server_postman_surface.py b/tools/qualification/ep_server_postman_surface.py new file mode 100644 index 0000000..521b4a1 --- /dev/null +++ b/tools/qualification/ep_server_postman_surface.py @@ -0,0 +1,98 @@ +"""Complete, fail-closed Postman surface manifest for EP Server. + +This is deliberately distinct from the public OpenAPI collection: it covers +the installed Operations Console, administration and retired compatibility +routes as well as producer transport. Every item is a callable HTTP request +with its intended status code, so a route cannot silently disappear or start +accepting an unsafe request without changing the qualification manifest. +""" +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class Case: + name: str + method: str + path: str + status: int + body: str | None = None + project: bool = False + origin: bool = False + + +# One concrete case for every route family in console_route_ownership, plus +# transport aliases that are intentionally outside that Console matrix. +CASES: tuple[Case, ...] = ( + Case("Console shell", "GET", "/", 200), + Case("Console JavaScript asset", "GET", "/assets/dashboard.js", 200), + Case("Console favicon", "GET", "/favicon.ico", 200), + Case("Platform health", "GET", "/health", 503), + Case("Platform projection", "GET", "/api/platform-status", 200), + Case("Dashboard snapshot", "GET", "/api/dashboard-snapshot", 200), + Case("Status alias", "GET", "/api/status", 200), + Case("Platform component details", "GET", "/api/components/ep_server/details", 200), + Case("Invalid component details", "GET", "/api/components/not_a_component/details", 409), + Case("Component restart rejects malformed body", "POST", "/api/components/ep_server/restart", 409, "{}"), + Case("Platform logs", "GET", "/api/logs/all", 200), + Case("Platform logs reject malformed clear", "POST", "/api/logs/all", 400, "{}"), + Case("Provider readiness", "GET", "/api/provider-login-status", 200), + Case("Provider repair rejects malformed body", "POST", "/api/provider-login/repair", 409, "{}"), + Case("Provider logout rejects malformed body", "POST", "/api/provider-login/logout", 409, "{}"), + Case("Execution runtime", "GET", "/api/execution-runtime-status", 200), + Case("Runtime repair rechecks the installed runtime", "POST", "/api/execution-runtime/repair", 200, "{}"), + Case("Provider capacity", "GET", "/api/provider-capacity", 200), + Case("Provider capacity configuration", "GET", "/api/provider-capacity/configuration", 200), + Case("Provider capacity rejects malformed update", "POST", "/api/provider-capacity/configuration", 409, "{}"), + Case("GitHub rate-limit diagnostics", "GET", "/api/github-rate-limit", 200), + Case("Console configuration", "GET", "/api/configuration", 200), + Case("Console configuration rejects malformed update", "POST", "/api/configuration", 409, "{}"), + Case("Process metrics", "GET", "/api/process-metrics", 200), + Case("Usage diagnostics", "GET", "/api/usage", 200), + Case("Host-admin diagnostics", "GET", "/api/host-admin/diagnostics", 200), + Case("Central data export", "GET", "/api/central-data/export", 200), + Case("Central relocation rejects bad request", "POST", "/api/central-data/relocate", 409, "{}"), + Case("Central import requires confirmation", "POST", "/api/central-data/import", 409, "{}"), + Case("Central database configuration", "GET", "/api/central-database/configuration", 200), + Case("Central database configuration rejects bad request", "POST", "/api/central-database/configuration", 400, "{}"), + Case("Project history", "GET", "/api/prompt-history", 200, project=True), + Case("Missing project report", "GET", "/api/prompt-history/missing-run/report", 404, project=True), + Case("Missing project chat", "GET", "/api/prompt-history/missing-run/chat", 404, project=True), + Case("Missing project detail", "GET", "/api/prompt-history/missing-run/details", 404, project=True), + Case("Missing telemetry day", "GET", "/api/telemetry/2026-01-01", 404, project=True), + Case("Execution action rejects malformed request", "POST", "/api/execution-dismiss", 400, "{}", True, True), + Case("Queue action rejects malformed request", "POST", "/api/queue-disposition", 400, "{}", True, True), + Case("Translation route is fail-closed pending project mutation support", "POST", "/api/dashboard-translate", 405, "{}", True, True), + Case("Retired runtime directory", "POST", "/api/runtime-directory/open", 410, "{}"), + Case("Retired Inbox configuration", "POST", "/api/configuration/inbox-location", 410, "{}"), + Case("Retired component logs", "GET", "/api/logs/inbox", 410), + Case("Operations projects", "GET", "/v1/operations/projects", 200), + Case("Topology diagnostics", "GET", "/diagnostics/topology", 200), + Case("Health probe", "GET", "/healthz", 200), + Case("Readiness probe", "GET", "/readyz", 200), + Case("Submission requires credential", "POST", "/v1/projects/postman-project/submissions", 401, "{}"), + Case("Submission readback requires credential", "GET", "/v1/projects/postman-project/submissions/missing", 401), + Case("Evidence artifact requires credential", "GET", "/v1/projects/postman-project/artifacts/terminal-evidence:missing", 401), + Case("Agent pairing rejects malformed request", "POST", "/v1/agent/pair", 400, "{}"), + Case("Agent registration requires credential", "POST", "/v1/agent/register", 401, "{}"), + Case("Unknown route", "GET", "/not-an-ep-route", 404), +) + + +def postman_collection() -> dict[str, object]: + def item(case: Case) -> dict[str, object]: + headers = ([{"key": "Content-Type", "value": "application/json"}] + if case.body is not None else []) + if case.origin: + headers.append({"key": "Origin", "value": "{{baseUrl}}"}) + suffix = "?project=postman-project" if case.project else "" + request: dict[str, object] = {"method": case.method, "header": headers, + "url": "{{baseUrl}}" + case.path + suffix} + if case.body is not None: + request["body"] = {"mode": "raw", "raw": case.body} + return {"name": case.name, "request": request, "event": [{"listen": "test", "script": {"exec": [ + f"pm.test('expected status', () => pm.response.to.have.status({case.status}));" + ]}}]} + return {"info": {"name": "EP Server complete HTTP surface", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"}, + "item": [item(case) for case in CASES]} diff --git a/tools/qualification/http_json_postman_contract.py b/tools/qualification/http_json_postman_contract.py index 07d0de7..1a46d6d 100644 --- a/tools/qualification/http_json_postman_contract.py +++ b/tools/qualification/http_json_postman_contract.py @@ -8,18 +8,29 @@ from pathlib import Path import re import socket +import sqlite3 import tempfile -from threading import Thread +from threading import Lock, Thread from urllib.error import HTTPError from urllib.request import Request, urlopen -from engineering_platform import server +from engineering_platform import server, submission_service +from ep_server_postman_surface import postman_collection COLLECTION = Path("tests/engineering/postman/http-json-api.postman_collection.json") +SURFACE_COLLECTION = Path("tests/engineering/postman/ep-server-http-surface.postman_collection.json") _STATUS = re.compile(r"pm\.response\.to\.have\.status\((\d+)\)") +class _NoopService: + """Minimal lifecycle double for isolated HTTP surface qualification.""" + def start(self) -> None: pass + def stop(self) -> None: pass + def diagnostics(self) -> "_NoopService": return self + def to_dict(self) -> dict[str, object]: return {"state": "QUALIFICATION_NOOP"} + + def _operations_from_openapi(document: dict[str, object]) -> set[tuple[str, str]]: paths = document.get("paths") if not isinstance(paths, dict): @@ -56,7 +67,9 @@ def _collection_operation(item: dict[str, object]) -> tuple[str, str]: if not isinstance(method, str) or not isinstance(raw, str): raise RuntimeError("POSTMAN_REQUEST_INVALID") path = raw.removeprefix("{{baseUrl}}") - path = re.sub(r":([A-Za-z_][A-Za-z0-9_]*)", r"{\1}", path) + # Postman's ``:name`` parameters occur at a path-segment boundary. Do + # not rewrite a literal colon in an evidence-artifact identifier. + path = re.sub(r"/:([A-Za-z_][A-Za-z0-9_]*)", r"/{\1}", path) if not path.startswith("/"): raise RuntimeError("POSTMAN_URL_INVALID") return method.upper(), path @@ -83,7 +96,7 @@ def _request(base_url: str, item: dict[str, object]) -> int: assert isinstance(request, dict) method, path = _collection_operation(item) url = base_url + path.replace("{project_id}", "postman-project") - headers = {str(header["key"]): str(header["value"]) + headers = {str(header["key"]): str(header["value"]).replace("{{baseUrl}}", base_url) for header in request.get("header", []) if isinstance(header, dict) and "key" in header and "value" in header} body = request.get("body", {}) data = str(body.get("raw", "")).encode("utf-8") if isinstance(body, dict) and method != "GET" else None @@ -94,13 +107,102 @@ def _request(base_url: str, item: dict[str, object]) -> int: return error.code +def _queue_action_contract(data_root: Path, base_url: str) -> None: + """Exercise Operations Console actions through their real HTTP boundary. + + These are intentionally separate from the public OpenAPI collection: the + queue controls are authenticated local-console operations, not producer + transport endpoints. Keeping the distinction prevents a console write + from being silently advertised as a public API operation. + """ + project, repository = "postman-queue", "postman-queue-repository" + declaration = { + "schema_version": "1.0", + "project": {"id": project, "authority_repository_id": repository}, + "repository": {"id": repository, "role": "authority"}, + "validation": {"kind": "none"}, + } + with sqlite3.connect(data_root / server.SERVER_DATABASE_FILENAME) as connection: + server.project_topology.register_server_local_topology(connection, declaration=declaration) + credential = str(submission_service.issue_consumer_credential( + connection, consumer_id="postman-queue", project_id=project, + )["credential"]) + producer_credential = str(submission_service.issue_consumer_credential( + connection, consumer_id="postman-producer", project_id=project, + )["credential"]) + payload = { + "repository_id": repository, + "producer": {"id": "postman-queue", "type": "HUMAN", "version": "1"}, + "prompt": "Postman Operations Console queue qualification", + "idempotency_key": "postman-queue-actions", + } + submit = Request( + base_url + f"/v1/projects/{project}/submissions", data=json.dumps(payload).encode(), method="POST", + headers={"Content-Type": "application/json", "Authorization": f"Bearer {credential}"}, + ) + with urlopen(submit, timeout=3) as response: # nosec B310 + submission_id = str(json.loads(response.read())["submission_id"]) + + expected_state, expected_revision = "QUEUED", 0 + + def action(disposition: str, reason: str, *, origin: str | None = None, + bearer: str | None = credential) -> tuple[int, str]: + nonlocal expected_state, expected_revision + request = Request( + base_url + f"/api/queue-disposition?project={project}", + data=json.dumps({"contract_version": "1.0", "operation_id": f"postman-{expected_revision + 1}", + "submission_id": submission_id, "expected_state": expected_state, + "expected_revision": expected_revision, "disposition": disposition, + "reason": reason}).encode(), + method="POST", headers={"Content-Type": "application/json", "Origin": origin or base_url, + **({"Authorization": f"Bearer {bearer}"} if bearer else {})}, + ) + try: + with urlopen(request, timeout=3) as response: # nosec B310 + response_payload = json.loads(response.read()) + expected_state = str(response_payload["state"]) + expected_revision = int(response_payload["resulting_revision"]) + return response.status, "" + except HTTPError as error: + return error.code, error.read().decode("utf-8", "replace") + + for bearer, expected_status in ((None, 401), (producer_credential, 403)): + status, detail = action("DEFERRED", "Capability negative contract", bearer=bearer) + if status != expected_status: + raise RuntimeError(f"POSTMAN_QUEUE_AUTHORIZATION_FAILED:{expected_status}:{status}:{detail}") + with sqlite3.connect(data_root / server.SERVER_DATABASE_FILENAME) as connection: + for capability in ("QUEUE_HOLD_RESUME", "QUEUE_DECLINE"): + connection.execute( + "INSERT INTO ep_operator_capabilities(consumer_id,project_id,capability,granted_at) VALUES(?,?,?,?)", + ("postman-queue", project, capability, "qualification"), + ) + + for disposition, reason in ( + ("DEFERRED", "Postman defer contract"), ("QUEUED", "Postman resume contract"), + ("QUARANTINED", "Postman quarantine contract"), + ("DECLINED", "Postman decline contract"), + ): + status, detail = action(disposition, reason) + if status != 200: + raise RuntimeError(f"POSTMAN_QUEUE_ACTION_FAILED:{disposition}:{status}:{detail}") + status, detail = action("QUEUED", "Must not revive a declined submission") + if status != 409: + raise RuntimeError(f"POSTMAN_QUEUE_TERMINAL_TRANSITION_NOT_REJECTED:{status}:{detail}") + status, detail = action("DEFERRED", "Untrusted origin", origin="https://untrusted.example") + if status != 403: + raise RuntimeError("POSTMAN_QUEUE_ORIGIN_ISOLATION_FAILED") + + def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument("--source-root", type=Path, required=True) root = parser.parse_args(argv).source_root.resolve() collection = json.loads((root / COLLECTION).read_text(encoding="utf-8")) + surface_collection = json.loads((root / SURFACE_COLLECTION).read_text(encoding="utf-8")) if collection.get("info", {}).get("schema") != "https://schema.getpostman.com/json/collection/v2.1.0/collection.json": raise RuntimeError("POSTMAN_SCHEMA_INVALID") + if surface_collection != postman_collection(): + raise RuntimeError("POSTMAN_SERVER_SURFACE_MANIFEST_DRIFT") expected_openapi = server._http_json_openapi_document() items = _collection_items(collection.get("item")) if _operations_from_openapi(expected_openapi) != {_collection_operation(item) for item in items}: @@ -109,8 +211,18 @@ def main(argv: list[str] | None = None) -> int: data_root = Path(temporary) / "data" port = _port() server.initialize(data_root, bind_port=port) + with sqlite3.connect(data_root / server.SERVER_DATABASE_FILENAME) as connection: + server.project_topology.register_server_local_topology(connection, declaration={ + "schema_version": "1.0", "project": {"id": "postman-project", "authority_repository_id": "postman-repository"}, + "repository": {"id": "postman-repository", "role": "authority"}, "validation": {"kind": "none"}, + }) http_server = http.server.ThreadingHTTPServer(("127.0.0.1", port), server._HealthHandler) http_server.data_root = data_root # type: ignore[attr-defined] + http_server.central_data_transfer_lock = Lock() # type: ignore[attr-defined] + http_server.central_data_transfer_active = False # type: ignore[attr-defined] + http_server.dependabot_service = _NoopService() # type: ignore[attr-defined] + http_server.inbox_service = _NoopService() # type: ignore[attr-defined] + http_server.lifecycle_worker = _NoopService() # type: ignore[attr-defined] worker = Thread(target=http_server.serve_forever, daemon=True) worker.start() try: @@ -123,6 +235,11 @@ def main(argv: list[str] | None = None) -> int: observed, expected = _request(base_url, item), _expected_status(item) if observed != expected: raise RuntimeError(f"API_POSTMAN_DRIFT {item.get('name')}: expected {expected}, got {observed}") + for item in _collection_items(surface_collection.get("item")): + observed, expected = _request(base_url, item), _expected_status(item) + if observed != expected: + raise RuntimeError(f"SERVER_SURFACE_POSTMAN_DRIFT {item.get('name')}: expected {expected}, got {observed}") + _queue_action_contract(data_root, base_url) finally: http_server.shutdown() worker.join(timeout=3) diff --git a/tools/qualification/p_deterministic_execution_e2e.py b/tools/qualification/p_deterministic_execution_e2e.py new file mode 100644 index 0000000..b16e6b6 --- /dev/null +++ b/tools/qualification/p_deterministic_execution_e2e.py @@ -0,0 +1,408 @@ +#!/usr/bin/env python3 +"""Installed CENTRAL execution qualification for Genesis, Managed and recovery. + +The default Managed fixture uses a local bare Git remote. An external GitHub +qualification is deliberately separate: it requires a clean checkout, its +exact approved ``owner/repository`` identity, and an explicit write flag. It +creates one branch and pull request, then stops at the normal human merge +boundary; CI never selects that profile. +""" +from __future__ import annotations + +import argparse +from contextlib import nullcontext +import json +import os +from pathlib import Path +import socket +import sqlite3 +import subprocess +import sys +import tempfile +import time +import uuid +from urllib.request import Request, urlopen + + +CENTRAL_DATABASE_FILENAME = "epdata.sqlite" +SERVER_CONFIGURATION_FILENAME = "server.json" +QUALIFICATION_RUNTIME_VERSION = "0.153.4" + + +def command(binary: Path, *args: str) -> dict[str, object]: + result = subprocess.run((str(binary), "-m", "engineering_platform.server", *args), check=True, text=True, capture_output=True) # nosec B603 + return json.loads(result.stdout) + + +def git(path: Path, *args: str) -> None: + subprocess.run(("git", "-C", str(path), *args), check=True, capture_output=True) # nosec B603 + + +def git_output(path: Path, *args: str) -> str: + return subprocess.run(("git", "-C", str(path), *args), check=True, text=True, capture_output=True).stdout.strip() # nosec B603 + + +def approved_github_checkout(path: Path, repository: str) -> Path: + """Fail closed unless the operator named this exact clean GitHub checkout.""" + checkout = path.resolve() + if not (checkout / ".git").exists() or git_output(checkout, "status", "--porcelain"): + raise RuntimeError("MANAGED_GITHUB_FIXTURE_MUST_BE_A_CLEAN_GIT_CHECKOUT") + remote = git_output(checkout, "remote", "get-url", "origin").removesuffix(".git") + normalized = remote.removeprefix("https://github.com/").removeprefix("git@github.com:") + if normalized != repository: + raise RuntimeError("MANAGED_GITHUB_FIXTURE_REPOSITORY_MISMATCH") + subprocess.run(("gh", "api", f"repos/{repository}"), check=True, capture_output=True) # nosec B603 + return checkout + + +def declared_fixture_identity(checkout: Path) -> tuple[str, str]: + """Use the fixture's committed binding; never overwrite its authority.""" + try: + declaration = json.loads((checkout / ".engineering-platform" / "repository.json").read_text(encoding="utf-8")) + project = declaration["project"]["id"] + repository = declaration["repository"]["id"] + except (OSError, KeyError, TypeError, json.JSONDecodeError) as error: + raise RuntimeError("MANAGED_GITHUB_FIXTURE_DECLARATION_REQUIRED") from error + if not isinstance(project, str) or not isinstance(repository, str) or not project or not repository: + raise RuntimeError("MANAGED_GITHUB_FIXTURE_DECLARATION_INVALID") + return project, repository + + +def port() -> int: + with socket.socket() as listener: + listener.bind(("127.0.0.1", 0)) + return int(listener.getsockname()[1]) + + +def create_repository(path: Path, *, origin: Path | None = None) -> None: + path.mkdir(parents=True) + subprocess.run(("git", "init", "-q", "-b", "main", str(path)), check=True) # nosec B603 + git(path, "config", "user.email", "qualification@example.invalid") + git(path, "config", "user.name", "Installed qualification") + (path / "BOOTSTRAP.md").write_text("# Installed qualification\n", encoding="utf-8") + git(path, "add", "BOOTSTRAP.md") + git(path, "commit", "-qm", "initial qualification repository") + if origin is not None: + subprocess.run(("git", "init", "-q", "--bare", str(origin)), check=True) # nosec B603 + git(path, "remote", "add", "origin", str(origin)) + git(path, "push", "-qu", "origin", "main") + subprocess.run(("git", "--git-dir", str(origin), "symbolic-ref", "HEAD", "refs/heads/main"), check=True) # nosec B603 + + +def configure_deterministic_runtime(data_root: Path, root: Path) -> Path: + """Install the minimal managed-runtime contract inside this qualification only. + + Host preflight must remain real in the installed E2E. CI deliberately has + no account-wide EP-managed Codex installation, so the isolated data root + owns a tiny version-reporting launcher. The deterministic provider never + invokes it for agent work; it exists solely to exercise the same resolved + launcher and invocation checks that production performs. + """ + prefix = root / "managed-codex-cli" + executable = prefix / "bin" / "codex" + executable.parent.mkdir(parents=True) + executable.write_text(f"#!/bin/sh\nprintf 'codex {QUALIFICATION_RUNTIME_VERSION}\\n'\n", encoding="utf-8") + executable.chmod(0o755) + configuration_path = data_root / SERVER_CONFIGURATION_FILENAME + configuration = json.loads(configuration_path.read_text(encoding="utf-8")) + if not isinstance(configuration, dict) or set(configuration) != { + "version", "bind_host", "bind_port", "managed_codex_cli_prefix", + }: + raise RuntimeError("QUALIFICATION_SERVER_CONFIGURATION_INVALID") + configuration["managed_codex_cli_prefix"] = str(prefix.resolve()) + configuration_path.write_text(json.dumps(configuration, sort_keys=True) + "\n", encoding="utf-8") + return executable + + +def wait_terminal(server: Path, data_root: Path, submission_id: str) -> tuple[str, str]: + # Managed deliberately yields between the implementation merge and the + # finalization/reconciliation polls; leave room for those bounded resumes. + deadline = time.monotonic() + 120 + while time.monotonic() < deadline: + diagnosis = command(server, "submission-diagnose", "--data-root", str(data_root), "--submission-id", submission_id) + state, run_id = diagnosis.get("dispatch_state"), diagnosis.get("run_id") + if state in {"COMPLETE", "BLOCKED", "FAILED"}: + if state != "COMPLETE" or not isinstance(run_id, str): + raise RuntimeError(f"E2E_EXECUTION_NOT_COMPLETE: {diagnosis}") + return submission_id, run_id + time.sleep(.2) + raise RuntimeError(f"E2E_EXECUTION_TIMED_OUT: {submission_id}") + + +def wait_github_handoff(checkout: Path, repository: str, branch: str) -> dict[str, object]: + """Prove the external write boundary without bypassing human merge authority.""" + deadline = time.monotonic() + 120 + while time.monotonic() < deadline: + result = subprocess.run( + ("gh", "pr", "list", "--repo", repository, "--head", branch, "--state", "open", "--json", "number,url,headRefName,baseRefName"), + check=True, text=True, capture_output=True, + ) # nosec B603 + pull_requests = json.loads(result.stdout) + if isinstance(pull_requests, list) and len(pull_requests) == 1: + pull_request = pull_requests[0] + if pull_request.get("headRefName") != branch or pull_request.get("baseRefName") != "main": + raise RuntimeError("MANAGED_GITHUB_PULL_REQUEST_SCOPE_INVALID") + remote_branch = git_output(checkout, "ls-remote", "--heads", "origin", f"refs/heads/{branch}") + if not remote_branch: + raise RuntimeError("MANAGED_GITHUB_REMOTE_BRANCH_UNAVAILABLE") + return {"repository": repository, "branch": branch, "pull_request": pull_request} + time.sleep(.2) + raise RuntimeError("MANAGED_GITHUB_PULL_REQUEST_TIMED_OUT") + + +def verify_receipt(data_root: Path, project: str, run_id: str) -> dict[str, object]: + findings = data_root / "artifacts" / "projects" / project / "runs" / run_id / "assurance-findings-v1.json" + receipt = json.loads(findings.read_text(encoding="utf-8")) + reviews = receipt.get("reviews") + observed = [(item.get("reviewer"), item.get("status")) for item in reviews] if isinstance(reviews, list) else [] + if observed != [("quality", "PASS"), ("security", "PASS")]: + raise RuntimeError(f"ASSURANCE_RECEIPT_INVALID: {observed}") + # The standalone Server owns epdata.sqlite. The former engineering.db + # name is deliberately retired and must never become an accidental second + # lifecycle authority during qualification. + with sqlite3.connect(data_root / CENTRAL_DATABASE_FILENAME) as connection: + row = connection.execute("SELECT phase,payload FROM engineering_transactions WHERE run_id=?", (run_id,)).fetchone() + if row is None or row[0] != "COMPLETE": + raise RuntimeError("TERMINAL_CHECKPOINT_UNAVAILABLE") + return {"run_id": run_id, "assurance_reviews": observed, "terminal_phase": row[0]} + + +def verify_controlled_recovery(data_root: Path, checkout: Path, run_id: str, base: str, project: str = "recovery") -> dict[str, object]: + control = checkout / ".engineering" / "artifacts" / "provider-recovery-fault-injection" / f"{run_id}-EXECUTE_AGENT.json" + consumed = json.loads(control.read_text(encoding="utf-8")) + if consumed.get("kind") != "CONTROLLED_PROVIDER_INTERRUPTION" or consumed.get("phase") != "EXECUTE_AGENT": + raise RuntimeError(f"CONTROLLED_INTERRUPTION_EVIDENCE_INVALID: {consumed}") + with sqlite3.connect(data_root / CENTRAL_DATABASE_FILENAME) as connection: + recovery = connection.execute( + "SELECT lifecycle_phase,state,result FROM provider_recovery_attempts WHERE run_id=?", (run_id,) + ).fetchone() + if recovery != ("EXECUTE_AGENT", "RECOVERED", "SUCCESS"): + raise RuntimeError(f"CONTROLLED_RECOVERY_LINEAGE_INVALID: {recovery}") + with urlopen(base + f"/api/prompt-history?project={project}", timeout=5) as response: # nosec B310 + history = json.loads(response.read()) + if not isinstance(history, list) or not any(item.get("run_id") == run_id for item in history if isinstance(item, dict)): + raise RuntimeError("CONTROLLED_RECOVERY_DASHBOARD_HISTORY_UNAVAILABLE") + return {"run_id": run_id, "control": "CONSUMED", "recovery": "RECOVERED", "dashboard_history": "VISIBLE"} + + +def merge_github_pull_request(repository: str, pull_request: int) -> None: + """Cross the explicit human merge boundary of the disposable fixture.""" + subprocess.run( + ("gh", "pr", "merge", str(pull_request), "--repo", repository, "--merge", "--delete-branch"), + check=True, capture_output=True, text=True, + ) # nosec B603 + + +def run_id_for_submission(server: Path, data_root: Path, submission_id: str) -> str: + deadline = time.monotonic() + 120 + while time.monotonic() < deadline: + diagnosis = command(server, "submission-diagnose", "--data-root", str(data_root), "--submission-id", submission_id) + run_id = diagnosis.get("run_id") + if isinstance(run_id, str) and run_id: + return run_id + time.sleep(.2) + raise RuntimeError("MANAGED_GITHUB_RUN_ID_UNAVAILABLE") + + +def complete_github_managed_run( + server: Path, data_root: Path, submission_id: str, checkout: Path, + repository: str, implementation_branch: str, project: str, +) -> dict[str, object]: + """Verify the real remote implementation and Finalization handoffs to COMPLETE. + + The script is the designated human operator for this disposable fixture. + It never changes production merge authority: each merge is an explicit + GitHub CLI mutation on the exact repository named by the caller. + """ + implementation = wait_github_handoff(checkout, repository, implementation_branch) + pull_request = implementation["pull_request"] + if not isinstance(pull_request, dict) or not isinstance(pull_request.get("number"), int): + raise RuntimeError("MANAGED_GITHUB_IMPLEMENTATION_PR_INVALID") + merge_github_pull_request(repository, pull_request["number"]) + run_id = run_id_for_submission(server, data_root, submission_id) + finalization_branch = f"codex/finalize-{run_id}" + finalization = wait_github_handoff(checkout, repository, finalization_branch) + finalization_pr = finalization["pull_request"] + if not isinstance(finalization_pr, dict) or not isinstance(finalization_pr.get("number"), int): + raise RuntimeError("MANAGED_GITHUB_FINALIZATION_PR_INVALID") + merge_github_pull_request(repository, finalization_pr["number"]) + _, terminal_run_id = wait_terminal(server, data_root, submission_id) + if terminal_run_id != run_id: + raise RuntimeError("MANAGED_GITHUB_RUN_ID_CHANGED") + receipt = verify_receipt(data_root, project, run_id) + return { + **receipt, + "implementation": implementation, + "finalization": finalization, + } + + +def submit(base: str, credential: str, project: str, repository: str, prompt: str, key: str) -> str: + body = {"repository_id": repository, "producer": {"id": "installed-e2e", "type": "HUMAN", "version": "1"}, "prompt": prompt, "idempotency_key": key, "constraints": {"mode": "GENESIS" if "Genesis" in prompt else "MANAGED"}} + request = Request(base + f"/v1/projects/{project}/submissions", data=json.dumps(body).encode(), method="POST", headers={"Content-Type": "application/json", "Authorization": f"Bearer {credential}"}) + with urlopen(request, timeout=5) as response: # nosec B310 + return str(json.loads(response.read())["submission_id"]) + + +def bind_project(server: Path, data_root: Path, *, project: str, repository: str, + checkout: Path, push_declaration: bool = False, existing_declaration: bool = False) -> None: + """Bind a clean fixture through the same installed Server commands as production.""" + command(server, "bootstrap-topology", "--data-root", str(data_root), "--project-id", project, "--repository-id", repository) + if not existing_declaration: + command(server, "provision-declaration", "--data-root", str(data_root), "--project-id", project, "--repository-id", repository, "--path", str(checkout)) + git(checkout, "add", ".engineering-platform") + git(checkout, "commit", "-qm", "bind installed e2e project") + if push_declaration: + git(checkout, "push", "-q", "origin", "main") + command(server, "bind-repository", "--data-root", str(data_root), "--project-id", project, "--repository-id", repository, "--path", str(checkout)) + + +def arm_controlled_recovery(venv: Path, data_root: Path, checkout: Path, ready: Path) -> str: + """Wait for the deterministic adapter's bounded arm window, then use its public CLI.""" + deadline = time.monotonic() + 30 + while not ready.is_file() and time.monotonic() < deadline: + time.sleep(.05) + if not ready.is_file(): + raise RuntimeError("CONTROLLED_RECOVERY_ARM_WINDOW_UNAVAILABLE") + control = json.loads(ready.read_text(encoding="utf-8")) + run_id = control.get("run_id") + if not isinstance(run_id, str) or control.get("phase") != "EXECUTE_AGENT": + raise RuntimeError(f"CONTROLLED_RECOVERY_ARM_WINDOW_INVALID: {control}") + subprocess.run( + (str(venv / "bin" / "python"), "-m", "engineering_platform.provider_recovery", + "arm-controlled-interruption", "--repo", str(checkout), "--run-id", run_id, + "--phase", "EXECUTE_AGENT", "--central-database", str(data_root / CENTRAL_DATABASE_FILENAME)), + check=True, capture_output=True, text=True, + ) # nosec B603 + ready.with_suffix(ready.suffix + ".continue").write_text("armed\n", encoding="utf-8") + return run_id + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--source-root", type=Path, default=Path.cwd()) + parser.add_argument("--managed-repository", type=Path, help="Clean checkout of the approved dummy GitHub repository.") + parser.add_argument("--managed-github-repository", help="Exact approved dummy GitHub owner/repository identity.") + parser.add_argument("--allow-managed-github-writes", action="store_true", help="Explicitly authorize one dummy-repository branch and pull request.") + parser.add_argument("--persistent-root", type=Path, help="New isolated qualification root retained for inspection; it never shares canonical production data.") + parser.add_argument("--bind-port", type=int, help="Fixed localhost port for a retained qualification Server.") + args = parser.parse_args(argv) + github_write = args.allow_managed_github_writes or args.managed_github_repository is not None + if github_write and (not args.allow_managed_github_writes or not args.managed_repository or not args.managed_github_repository): + raise RuntimeError("MANAGED_GITHUB_WRITE_AUTHORIZATION_REQUIRED") + if args.persistent_root and args.persistent_root.exists() and any(args.persistent_root.iterdir()): + raise RuntimeError("PERSISTENT_QUALIFICATION_ROOT_MUST_BE_EMPTY") + context = nullcontext(str(args.persistent_root.resolve())) if args.persistent_root else tempfile.TemporaryDirectory(prefix="ep-deterministic-e2e-") + with context as temporary: + root, wheelhouse, venv, data = Path(temporary), Path(temporary) / "wheelhouse", Path(temporary) / "venv", Path(temporary) / "central" + root.mkdir(mode=0o700, parents=True, exist_ok=True) + wheelhouse.mkdir() + subprocess.run((sys.executable, "-m", "pip", "wheel", "--no-deps", "--wheel-dir", str(wheelhouse), str(args.source_root)), check=True, capture_output=True, text=True) # nosec B603 + subprocess.run((sys.executable, "-m", "venv", str(venv)), check=True) # nosec B603 + wheels = tuple(wheelhouse.glob("engineering_platform-*.whl")) + if len(wheels) != 1: + raise RuntimeError("QUALIFICATION_WHEEL_UNAVAILABLE") + # Install the exact wheel path. A requirement-name install from the + # source checkout can falsely treat its adjacent metadata as already + # installed, leaving a non-restartable qualification venv. + subprocess.run((str(venv / "bin" / "pip"), "install", "--no-index", "--force-reinstall", str(wheels[0])), check=True, capture_output=True, text=True) # nosec B603 + # Invoke the installed module directly so qualification verifies the + # wheel contents rather than relying on a platform-specific console + # script wrapper being present in the virtual environment. + server = venv / "bin" / "python" + bind_port = args.bind_port or port() + command(server, "init", "--data-root", str(data), "--bind-port", str(bind_port)) + configure_deterministic_runtime(data, root) + genesis_host, genesis_target = root / "genesis-host", root / "genesis-target" + create_repository(genesis_host) + create_repository(genesis_target) + local = genesis_host / ".engineering" + local.mkdir() + (local / "engineering-platform.local.json").write_text(json.dumps({"workspace": {"workspace_authorization": {"allowed_roots": [], "allowed_repositories": [str(genesis_target.resolve())], "denied_repositories": [], "symlink_policy": "reject", "case_sensitivity": "host"}}}), encoding="utf-8") + if args.managed_repository: + if not github_write: + raise RuntimeError("MANAGED_GITHUB_WRITE_AUTHORIZATION_REQUIRED") + managed = approved_github_checkout(args.managed_repository, str(args.managed_github_repository)) + managed_project, managed_identity = declared_fixture_identity(managed) + else: + managed = root / "managed" + create_repository(managed, origin=root / "managed-origin.git") + managed_project, managed_identity = "managed", "managed-repo" + evidence: dict[str, object] = {} + layouts = (("genesis", "genesis", "genesis-repo", genesis_host), ("managed", managed_project, managed_identity, managed)) + for mode, project, repository, checkout in layouts: + bind_project(server, data, project=project, repository=repository, checkout=checkout, push_declaration=mode == "managed", existing_declaration=github_write and mode == "managed") + control_ready = root / "controlled-recovery-ready.json" + env = { + **os.environ, + "EP_QUALIFICATION_DETERMINISTIC_FLOW": "1", + "EP_CENTRAL_OPERATIONAL_DATABASE": str(data / CENTRAL_DATABASE_FILENAME), + "EP_QUALIFICATION_CONTROL_ARM_READY_FILE": str(control_ready), + } + github_branch = None + if github_write: + github_branch = f"qualification/managed-e2e-{uuid.uuid4().hex[:12]}" + env.update({"EP_QUALIFICATION_GITHUB_WRITE_FLOW": "1", "EP_QUALIFICATION_GITHUB_REPOSITORY": str(args.managed_github_repository), "EP_QUALIFICATION_GITHUB_BRANCH": github_branch}) + process = subprocess.Popen((str(server), "-m", "engineering_platform.server", "serve", "--data-root", str(data)), env=env) # nosec B603 + try: + base = f"http://127.0.0.1:{bind_port}" + for _ in range(100): + try: + urlopen(base + "/readyz", timeout=.2).read() # nosec B310 + break + except OSError: + time.sleep(.1) + for mode, project, repository, prompt in (("genesis", "genesis", "genesis-repo", f"Execution Mode: Genesis\nTarget repository: {genesis_target}\n\nInstalled deterministic qualification."), ("managed", managed_project, managed_identity, "Execution Mode: Managed\n\nInstalled deterministic qualification.")): + credential = str(command(server, "issue-consumer-credential", "--data-root", str(data), "--project-id", project, "--consumer-id", f"{mode}-e2e")["credential"]) + submission = submit(base, credential, project, repository, prompt, f"{mode}-e2e") + if mode == "managed" and github_write: + evidence[mode] = complete_github_managed_run( + server, data, submission, managed, + str(args.managed_github_repository), str(github_branch), project, + ) + continue + _, run_id = wait_terminal(server, data, submission) + evidence[mode] = verify_receipt(data, project, run_id) + recovery_project, recovery_repository = "recovery", "recovery-repo" + if github_write: + # Reuse the installed, already bound dummy fixture after the + # first run reached COMPLETE. This proves interruption/retry + # with the same real GitHub provider adapter and remote write + # boundary, without inventing another authority declaration. + recovery, recovery_project, recovery_repository = managed, managed_project, managed_identity + recovery_branch = f"{github_branch}-recovery" + else: + recovery = root / "controlled-recovery" + create_repository(recovery, origin=root / "controlled-recovery-origin.git") + bind_project(server, data, project=recovery_project, repository=recovery_repository, checkout=recovery, push_declaration=True) + control_ready.with_suffix(control_ready.suffix + ".enable").write_text("enabled\n", encoding="utf-8") + credential = str(command(server, "issue-consumer-credential", "--data-root", str(data), "--project-id", recovery_project, "--consumer-id", "recovery-e2e")["credential"]) + submission = submit(base, credential, recovery_project, recovery_repository, "Execution Mode: Managed\n\nInstalled controlled recovery qualification.", "controlled-recovery-e2e") + run_id = arm_controlled_recovery(venv, data, recovery, control_ready) + if github_write: + recovery_evidence = complete_github_managed_run( + server, data, submission, recovery, str(args.managed_github_repository), + recovery_branch, recovery_project, + ) + if recovery_evidence["run_id"] != run_id: + raise RuntimeError("CONTROLLED_RECOVERY_RUN_ID_CHANGED") + else: + _, terminal_run_id = wait_terminal(server, data, submission) + if terminal_run_id != run_id: + raise RuntimeError("CONTROLLED_RECOVERY_RUN_ID_CHANGED") + recovery_evidence = verify_receipt(data, recovery_project, run_id) + evidence["controlled_recovery"] = { + **recovery_evidence, + **verify_controlled_recovery(data, recovery, run_id, base, recovery_project), + } + finally: + if not args.persistent_root: + process.terminate(); process.wait(timeout=10) + else: + (root / "qualification-runtime.json").write_text(json.dumps({"data_root": str(data), "port": bind_port, "pid": process.pid, "managed_project": managed_project}, sort_keys=True) + "\n", encoding="utf-8") + print(json.dumps({"result": "PASS", "managed_fixture": "github-write" if github_write else "local-origin", "evidence": evidence}, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/qualification/p_transport_installed_ingress_matrix.py b/tools/qualification/p_transport_installed_ingress_matrix.py index b0649bf..67e790d 100644 --- a/tools/qualification/p_transport_installed_ingress_matrix.py +++ b/tools/qualification/p_transport_installed_ingress_matrix.py @@ -44,10 +44,27 @@ # lifecycle semantics depend on runner timing. DEPENDABOT_DISPATCH_TIMEOUT_SECONDS = 60 +# The crash-recovery canaries deliberately terminate the installed Server. +# A hosted SQLite runner can briefly retain the just-released store while the +# next, idempotent topology bootstrap starts. Retry only that bootstrap: an +# issuance or other mutating administrative command must never be replayed +# merely because its response was unavailable. +BOOTSTRAP_TOPOLOGY_RETRY_ATTEMPTS = 4 +BOOTSTRAP_TOPOLOGY_RETRY_DELAY_SECONDS = 0.25 + def command(binary: Path, *args: str, environment: dict[str, str] | None = None) -> dict[str, object]: - completed = subprocess.run([str(binary), *args], check=True, text=True, capture_output=True, env=environment) # nosec B603 - return json.loads(completed.stdout) + for attempt in range(1, BOOTSTRAP_TOPOLOGY_RETRY_ATTEMPTS + 1): + completed = subprocess.run([str(binary), *args], check=False, text=True, capture_output=True, env=environment) # nosec B603 + if not completed.returncode: + return json.loads(completed.stdout) + detail = (completed.stderr or completed.stdout).strip().replace("\n", " ")[:1024] + retryable = args[:1] == ("bootstrap-topology",) and "EP Server store is unavailable." in detail + if retryable and attempt < BOOTSTRAP_TOPOLOGY_RETRY_ATTEMPTS: + time.sleep(BOOTSTRAP_TOPOLOGY_RETRY_DELAY_SECONDS * attempt) + continue + raise RuntimeError(f"installed Server command failed ({' '.join(args)}): {detail}") + raise AssertionError("bounded installed Server command retry loop exhausted") def wait_for_dispatch( diff --git a/tools/qualification/platform_version_consistency.py b/tools/qualification/platform_version_consistency.py index 755ee05..3bf562b 100644 --- a/tools/qualification/platform_version_consistency.py +++ b/tools/qualification/platform_version_consistency.py @@ -1,57 +1,113 @@ #!/usr/bin/env python3 -"""Fail closed when a public Engineering Platform version projection drifts.""" +"""Fail closed on EP source or installed-version projection drift. + +Source validation is deliberately static. Installed validation is delegated to +an isolated interpreter so a checkout's ``src`` tree cannot masquerade as the +wheel that was just qualified. +""" from __future__ import annotations import argparse -from importlib.metadata import version as installed_version +import ast import json +import os from pathlib import Path -import sys +import subprocess import tomllib +def _object(path: Path) -> dict[str, object]: + try: + value = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as error: + raise RuntimeError(f"EP_VERSION_COMPONENT_DRIFT invalid JSON {path}") from error + if not isinstance(value, dict): + raise RuntimeError(f"EP_VERSION_COMPONENT_DRIFT invalid JSON root {path}") + return value + + +def _field(document: dict[str, object], path: Path, *keys: str) -> str: + value: object = document + for key in keys: + if not isinstance(value, dict) or key not in value: + raise RuntimeError(f"EP_VERSION_COMPONENT_DRIFT missing {path}:{'.'.join(keys)}") + value = value[key] + if not isinstance(value, str): + raise RuntimeError(f"EP_VERSION_COMPONENT_DRIFT invalid type {path}:{'.'.join(keys)}") + return value + + +def _runtime_constant(path: Path) -> str: + try: + tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) + except (OSError, SyntaxError) as error: + raise RuntimeError(f"EP_VERSION_COMPONENT_DRIFT invalid runtime projection {path}") from error + values = [node.value.value for node in tree.body if isinstance(node, ast.Assign) for target in node.targets if isinstance(target, ast.Name) and target.id == "CURRENT_PLATFORM_VERSION" and isinstance(node.value, ast.Constant) and isinstance(node.value.value, str)] + if len(values) != 1: + raise RuntimeError(f"EP_VERSION_COMPONENT_DRIFT invalid runtime constant {path}") + return values[0] + + +def _source_projections(root: Path) -> tuple[str, dict[str, str]]: + try: + package = tomllib.loads((root / "pyproject.toml").read_text(encoding="utf-8")) + project = package["project"] + except (OSError, KeyError, TypeError, tomllib.TOMLDecodeError) as error: + raise RuntimeError("EP_VERSION_COMPONENT_DRIFT invalid pyproject.toml") from error + if not isinstance(project, dict) or not isinstance(project.get("version"), str): + raise RuntimeError("EP_VERSION_COMPONENT_DRIFT invalid [project].version") + source = root / "src" / "engineering_platform" + manifest = _object(source / "ENGINEERING_PLATFORM_VERSION.json") + expected = _field(manifest, source / "ENGINEERING_PLATFORM_VERSION.json", "platform_version") + package_lock = _object(root / "package-lock.json") + return expected, { + "package": project["version"], "package_json_root": _field(_object(root / "package.json"), root / "package.json", "version"), + "package_lock_root": _field(package_lock, root / "package-lock.json", "version"), "package_lock_workspace_root": _field(package_lock, root / "package-lock.json", "packages", "", "version"), + "manifest_platform": expected, "manifest_runner": _field(manifest, source / "ENGINEERING_PLATFORM_VERSION.json", "runner_version"), + "manifest_dashboard": _field(manifest, source / "ENGINEERING_PLATFORM_VERSION.json", "dashboard_version"), "manifest_watcher": _field(manifest, source / "ENGINEERING_PLATFORM_VERSION.json", "watcher_version"), + "platform_configuration": _field(_object(source / "ENGINEERING_PLATFORM_CONFIG.json"), source / "ENGINEERING_PLATFORM_CONFIG.json", "platform", "version"), + "workspace_template": _field(_object(source / "templates" / "workspace-config.json"), source / "templates" / "workspace-config.json", "platform", "version"), + "runtime_constant": _runtime_constant(source / "platform_version.py"), + } + + +def _installed_projections(root: Path, python: Path) -> dict[str, str]: + program = '''import importlib.metadata, importlib.resources, json, os, pathlib +import engineering_platform +from engineering_platform import server +from engineering_platform.platform_version import CURRENT_PLATFORM_VERSION, RunnerCompatibility +from engineering_platform.server_console_services import DASHBOARD_VERSION +base = importlib.resources.files("engineering_platform") +manifest = json.loads(base.joinpath("ENGINEERING_PLATFORM_VERSION.json").read_text()) +config = json.loads(base.joinpath("ENGINEERING_PLATFORM_CONFIG.json").read_text()) +template = json.loads(base.joinpath("templates/workspace-config.json").read_text()) +if pathlib.Path(engineering_platform.__file__).resolve().is_relative_to(pathlib.Path(os.environ["EP_SOURCE_ROOT"]).resolve()): raise RuntimeError("installed module was loaded from source checkout") +print(json.dumps({"installed_package": importlib.metadata.version("engineering-platform"), "manifest_platform": manifest["platform_version"], "manifest_runner": manifest["runner_version"], "manifest_dashboard": manifest["dashboard_version"], "manifest_watcher": manifest["watcher_version"], "platform_configuration": config["platform"]["version"], "workspace_template": template["platform"]["version"], "runtime_constant": CURRENT_PLATFORM_VERSION, "runner": RunnerCompatibility().runner_version, "console": DASHBOARD_VERSION, "server": server._console_platform_version()}))''' + environment = os.environ.copy() + environment["EP_SOURCE_ROOT"] = str(root.resolve()) + completed = subprocess.run((str(python), "-I", "-c", program), check=False, capture_output=True, text=True, env=environment) + if completed.returncode: + raise RuntimeError(f"EP_VERSION_COMPONENT_DRIFT installed verification failed: {completed.stderr.strip()}") + try: + payload = json.loads(completed.stdout) + except json.JSONDecodeError as error: + raise RuntimeError("EP_VERSION_COMPONENT_DRIFT invalid installed verification result") from error + if not isinstance(payload, dict) or not all(isinstance(value, str) for value in payload.values()): + raise RuntimeError("EP_VERSION_COMPONENT_DRIFT invalid installed projection types") + return payload + + def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument("--source-root", type=Path, default=Path.cwd()) + parser.add_argument("--installed-python", type=Path, help="verify the wheel installed in this isolated interpreter") args = parser.parse_args(argv) - root = args.source_root.resolve() - source = root / "src" - if str(source) not in sys.path: - sys.path.insert(0, str(source)) - - from engineering_platform import server - from engineering_platform.platform_api import PlatformConfiguration - from engineering_platform.platform_version import ( - CURRENT_PLATFORM_VERSION, - EngineeringPlatformManifest, - RunnerCompatibility, - ) - from engineering_platform.server_console_services import DASHBOARD_VERSION - - manifest_path = source / "engineering_platform" / "ENGINEERING_PLATFORM_VERSION.json" - manifest = json.loads(manifest_path.read_text(encoding="utf-8")) - expected = manifest["platform_version"] - package = tomllib.loads((root / "pyproject.toml").read_text(encoding="utf-8")) - configuration = json.loads((source / "engineering_platform" / "ENGINEERING_PLATFORM_CONFIG.json").read_text(encoding="utf-8")) - template = json.loads((source / "engineering_platform" / "templates" / "workspace-config.json").read_text(encoding="utf-8")) - - projections = { - "package": package["project"]["version"], - "installed_package": installed_version("engineering-platform"), - "manifest_platform": manifest["platform_version"], - "manifest_runner": manifest["runner_version"], - "manifest_dashboard": manifest["dashboard_version"], - "manifest_watcher": manifest["watcher_version"], - "platform_configuration": configuration["platform"]["version"], - "workspace_template": template["platform"]["version"], - "runtime_constant": CURRENT_PLATFORM_VERSION, - "runner": RunnerCompatibility().runner_version, - "console": DASHBOARD_VERSION, - "server": server._console_platform_version(), - "platform_api": PlatformConfiguration.load(root).platform.version, - "installed_manifest": EngineeringPlatformManifest.load(manifest_path).platform_version, - } + expected, projections = _source_projections(args.source_root.resolve()) + if args.installed_python is not None: + # Keep a virtualenv launcher path intact: resolving it follows its + # symlink to the base interpreter and silently defeats isolation. + projections.update(_installed_projections(args.source_root.resolve(), args.installed_python)) drift = {name: value for name, value in projections.items() if value != expected} if drift: raise RuntimeError(f"EP_VERSION_COMPONENT_DRIFT expected={expected} observed={drift}")