diff --git a/.github/product-version-operations/forge-r57-genesis-ep-apply-20261016.json b/.github/product-version-operations/forge-r57-genesis-ep-apply-20261016.json new file mode 100644 index 0000000..3d04deb --- /dev/null +++ b/.github/product-version-operations/forge-r57-genesis-ep-apply-20261016.json @@ -0,0 +1,16 @@ +{ + "baseline_version": "2.22.0", + "classification_rationale": "L3-GENESIS-EP-APPLY-COORDINATOR-V1-20261016", + "component": "product", + "event_lineage": "L3-GENESIS-EP-APPLY-COORDINATOR-V1-20261016", + "expected_head": "523a13f3196c096da217421e74130df474fadcc6", + "operation_id": "forge-r57-genesis-ep-apply-20261016", + "policy_revision": "forge-bootstrap-release-cadence-v2", + "product": "forge", + "projection_paths": "product-version.json", + "release_class": "MINOR", + "requested_bump": "minor", + "requested_version": null, + "schema_version": "1", + "target_version": "2.23.0" +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 89aa17d..66c7dc6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,10 +10,36 @@ permissions: contents: read jobs: + genesis-native: + name: Qualified macOS Genesis native execution + runs-on: macos-14 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.14" + - run: python -m pip install '.[validation]' + - name: Genesis qualified producer apply and separate OS recovery + timeout-minutes: 25 + run: bash scripts/qualification/qualify_genesis_apply_ci.sh + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + if: always() + with: + name: forge-genesis-apply-${{ github.sha }} + path: ${{ runner.temp }}/forge-genesis-apply-ci/proof-*/*.public.json + if-no-files-found: error test: name: Test and static validation + needs: genesis-native + if: ${{ always() }} runs-on: ubuntu-24.04 steps: + - name: Require qualified native Genesis execution + env: + GENESIS_NATIVE_RESULT: ${{ needs.genesis-native.result }} + run: test "$GENESIS_NATIVE_RESULT" = success - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 diff --git a/docs/architecture/GENESIS_EP_APPLY_COORDINATOR_V1.md b/docs/architecture/GENESIS_EP_APPLY_COORDINATOR_V1.md new file mode 100644 index 0000000..04ee7b7 --- /dev/null +++ b/docs/architecture/GENESIS_EP_APPLY_COORDINATOR_V1.md @@ -0,0 +1,148 @@ +# Genesis EP apply coordinator V1 — selected original local effect + +R57/plan1. ASSIGNMENT_ID=L3-GENESIS-EP-APPLY-COORDINATOR-V1-20261016. +JOIN_ID=L2-L3-GENESIS-APPLY-V1-20261016. Forge2.23.0/schema46 allocation. +Implementation and qualification are OPEN. R56 and predecessors remain CLOSED. + +## Selected boundary + +One original R52 portable GENESIS authority proposal, its applicable original +R54 BUSINESS AND ARCHITECTURE content decisions, an exact frozen R56 plan and +separate currently applicable human effect decision can be explicitly offered +to one separately owner-authorized EP CREATE_GENESIS_LOCAL operation. + +Forge controls exact binding, durable original dispatch intent, authenticated +transport, readback and evidence interpretation. EP owns physical admission, +absence, placement, identity reservation, document/Git/validation/B8R effects +and their recovery. Forge never creates or modifies a target and never issues, +renews or borrows an EP grant. COMPLETE is an original technical operation +result, not project/Mission READY or human product acceptance. + +Only GENESIS/LOCAL_POSIX/authority/portable-docs1.0.0/DOCUMENT_ONLY/NO_PRODUCT_CODE, +the exact original37 document files and a new local target are selected. No +adoption/existing/unborn/child/remote/Managed/promotion, new renderer, scan, +model/provider call, Action, Mission, publication or native GUI is selected. + +## Exact producer mapping + +| Forge owning snapshot | EP request/binding | +| --- | --- | +| Durable explicit apply operation_id | Original operation_id, not a generated retry ID | +| R56 target.target_ref | Opaque target_ref; EP owner alone maps physical placement | +| R56 plan_id / plan_revision / plan_digest | Identical effect_plan_binding fields | +| Original separate positive R56 decision_id / decision_digest | Identical decision binding, never content approval substituted | +| Original R52 plan_id | proposal_id | +| Original R52 artifacts-array/result_digest | proposal_digest; distinct from EP-envelope digest | +| Original Forge instance / dossier | source_instance_id / context_id | +| Actual original human effect decision principal | source_actor_id as provenance, never EP authentication | +| Original37 UTF8 document payloads and SHA256 | Lossless pb-render/1.0 RENDERED_PROPOSAL_EXAMPLE files-map | + +The fixed original R50 example EP-envelope digest remains +0c2c0fb41a68793be0960bb4289d7590060c8092ebb976852e1c4674b0f66b1d. +Original array digest, manifest semantic digest, file digest and request digest +retain their distinct meanings. No file mode or executable-bit invention. + +Actual qualified producer routes are POST /v1/bootstrap/genesis/operations, +GET /v1/bootstrap/genesis/operations/{operation_id}, and explicit POST to its +/resume. Exact EP authorization uses a distinct CREATE_GENESIS_LOCAL bearer +and EP-Bootstrap-Contract, EP-Instance-ID, EP-Host-ID, EP-Consumer-ID, +EP-Principal, EP-Audience headers. Source actor never fills an auth header. +GET and explicit resume carry no body; EP rejects any nonzero Content-Length +on them. These routes are enabled only after the actual independently qualified L2 +final-main/schema/wheel handoff is pinned. A merged candidate is insufficient. +Existing R55 EP inspection pins and L4 consumer pins remain unchanged. + +## Own HTTP and CLI + +Closed packaged contract: forge/api/genesis-apply-v1.json. +Same existing Forge Server, separate finite GENESIS_APPLY_ONLY/1.0 bearer and +X-Forge-Genesis-Apply-Audience (the existing unambiguous audience-header parser). +No trusted arbitrary actor, viewer or role header. + +- POST /v1/genesis-apply/operations: explicit original apply request. +- GET /v1/genesis-apply/operations/{operation_id}?dossier_id=...: + original intent plus live authenticated owning EP readback; never effects. +- POST /v1/genesis-apply/operations/{operation_id}/resume: + explicit unique attempt_id plus exact original_request_digest and scope. + +Ordinary forge-genesis-apply CLI shares these services: request/read/resume. +Owner init/issue/revoke/bind-peer/revoke-peer use private files; neither bearer +nor root/host path, shell, validator command or uploaded approved bundle is +part of user apply input. Existing R56 plan/decision routes stay non-executing. + +An apply grant is separate from all earlier grants, bound to exact retained +source plans, dossier, principal, instance, audience, human/operator binding, +finite expiry and READ/REQUEST_APPLY/RESUME. Effects require current R54/R56 +conditions before admission, before sending and protected publication. A +replacement grant cannot launder an original revoked effect grant into resume. +Private EP pairing is immutable and sealed, with qualified producer pins, +exact original operation/request/plan/decision/target/placement binding and a +separately installation-owner-issued finite EP credential. Forge cannot mint it. + +## Durable dispatch and publication + +Original request, EP request bytes/digest, original Forge grant and private +pairing digest are durable before any effect POST. Under the owning boundary +one CREATE sendclaim is persisted before transport. An original intent with +no sendclaim can recover its first explicit dispatch after a healthy original +GET404. Once a sendclaim exists, all identical submissions first recover by +original GET; even GET404 after an uncertain send never causes another create. +Changed input under the same operation ID conflicts. No unbounded retry. + +Explicit resume first verifies the owning original admitted EP operation and +current original authority. GET/refresh never resumes. Attempt CAS fences late +responses and late sends; older source_updated_at cannot overwrite newer +observations. EP remains the source of effect truth; stored receipts retain +original evidence and are not an independent execution database. + +Authority locks cover private state and current canonical checks, never network +waits. Finite direct loopback transport has no proxy, redirects or ambient auth. +A current-authority callback runs after connect immediately before sending; +absolute deadline checks and a socket watchdog bound send and response success. +Protected publication rechecks actual peer access and current own scope/epoch +without holding own authority across the peer request. Missing/corrupt initialized +stores or absent original peer evidence fail closed, never healthy empty history +or cached COMPLETE. Revocation/expiry never triggers rollback, delete, force, +credential replacement or rewriting original evidence. + +## Remaining delivery gates + +Source/unit/synthetic peer tests are not installed effect acceptance. First small +noneditable installed Forge plus separately installed qualified EP must prove +original37 bytes, separate real synthetic owner grant, HTTP POST/GET, independent +Git commit/tree/trusted validator/B8R and replay without duplicate effects before +heavy suites. Required negative/concurrency/lost-response/separate server OS +restart/partial/corruption/authority/late-publication and restored-fault RED/GREEN +proofs, strict >80.2% per changed production file, full CI, independent complete +Quality/Security, actual unchanged observe-only TDE disposition, protected exact +head implementation, own nonempty reviewed/protected finalization, NEW exact +final-main installed chain and safe cleanup remain OPEN until exact receipts. +No full PB-F3/PB-F7/HOB/readiness parent closure or automatic successor. + +## Qualified producer handoff + +L2 terminal handoff #142/6223927644 and #207/6223928601 binds EP2.3.123 +to source `4d0c0461d7a5ec7fb7a50aa42f775cc4be09faa5`, tree +`b095ba006beccd79171115a7deab55ef7b7c4ff9`, response schema SHA256 +`3ccf4d3ba775c9f4503e986fce5416b33fe1d125d0b18244beeae562868aa231` and ordinary wheel SHA256 +`bc49e7a3b15c9021b16546a5ca96e5fbf280072d900d8d3dc173dee4b79d5b9e`. +Own R57 installed integration and complete delivery remain OPEN. + + +## Owning qualified host boundary + +The L2 final-main handoff qualifies the full installed native Genesis chain on +macOS at finite short private placements; Linux's canonical wheel identity, +component fixtures and general native controls are separate evidence. It does +not qualify full native Genesis execution on Linux. The original three Ubuntu +R57 native-start failures remain preserved, including actual BrokenPipeError32 +before PREFLIGHT completed. No Linux full-chain PASS is claimed. + +The same14 real native qualification cases run in a separate ordinary macOS CI +job with the same25-minute boundary. The existing required Ubuntu Test and +static validation job runs with always() and explicitly requires native job +success before its unchanged tests, coverage and installed gates. A native +failure, cancellation or skip cannot become a successful skipped required job. +No sandbox profile, test assertion or producer contract is relaxed. Hosted +operator fixtures remain explicitly modeled, separate from actual local Mac +owner identity. This hosting correction is not a Linux capability delivery. diff --git a/docs/roadmap/PROJECT_BOOTSTRAP_V1.md b/docs/roadmap/PROJECT_BOOTSTRAP_V1.md index 47d97b7..bff10f9 100644 --- a/docs/roadmap/PROJECT_BOOTSTRAP_V1.md +++ b/docs/roadmap/PROJECT_BOOTSTRAP_V1.md @@ -119,3 +119,22 @@ handoff/terminal readback and cleanup. The owning terminal is the closure receipt; candidate artifacts do not close R56. Full PB-F2/PB-F7/HOB parents, PB-F3 apply coordinator, actual Genesis readiness, adoption effects, Managed, promotion and broad onboarding remain OPEN. No automatic successor. + +## R56 terminal reconciliation and selected R57 original effect coordinator + +[R56 terminal](https://github.com/pcvantol/forge/issues/207#issuecomment-6215404997) +closed its bounded pre-admission plan/separate effect-decision subset on +Forge2.22.0/schema46, final main523a13f3196c096da217421e74130df474fadcc6, +implementation PR296 and separate protected finalization PR297. The earlier +stage checkpoint above remains historical. Its failures, original producer pins, +702-open-finding observe-only TDE FAIL and counters remain retained. + +[R57 selected contract](../architecture/GENESIS_EP_APPLY_COORDINATOR_V1.md) +adds only explicit exact original local Genesis-effect coordination over R52, +R54 and R56. Forge2.23.0/schema46 is allocated; source/independent assurance, +qualified L2 producer handoff, installed HTTP/Git/B8R/recovery, normal protected +delivery, own finalization and NEW exact-final-main qualification remain OPEN. +Actual EP completion is not project/Mission readiness. R56 and predecessors +stay CLOSED; full PB-F3/PB-F7/HOB/readiness/Managed/promotion remain OPEN. +L4-plan108/Forge2.21.0/EP2.3.121 consumer pins and budgets are unchanged. +No automatic successor. diff --git a/docs/roadmap/project-bootstrap-v1.json b/docs/roadmap/project-bootstrap-v1.json index a8613e5..550b174 100644 --- a/docs/roadmap/project-bootstrap-v1.json +++ b/docs/roadmap/project-bootstrap-v1.json @@ -241,5 +241,18 @@ "candidate_installed_proof": "https://github.com/pcvantol/forge/actions/runs/38790025378/artifacts/12000150726", "finalization_effect": "NO_BUMP", "closure_gate": "Own finalization checks/reviews/required CI/protected merge; NEW exact-final-main installed HTTP/restart/readback/cleanup and terminal register" + }, + "r57_selected_effect_coordinator": { + "assignment_id": "L3-GENESIS-EP-APPLY-COORDINATOR-V1-20261016", + "join_id": "L2-L3-GENESIS-APPLY-V1-20261016", + "status": "IMPLEMENTING_PENDING_QUALIFICATION", + "contract": "docs/architecture/GENESIS_EP_APPLY_COORDINATOR_V1.md", + "version_allocation": "2.23.0", + "schema_version": 46, + "predecessor_terminal": "https://github.com/pcvantol/forge/issues/207#issuecomment-6215404997", + "predecessor_final_main": "523a13f3196c096da217421e74130df474fadcc6", + "parents_unchanged": true, + "readiness": "NOT_ASSESSED", + "automatic_successor": false } } diff --git a/forge/api/ep-genesis-local-bootstrap-v1.schema.json b/forge/api/ep-genesis-local-bootstrap-v1.schema.json new file mode 100644 index 0000000..4f37acd --- /dev/null +++ b/forge/api/ep-genesis-local-bootstrap-v1.schema.json @@ -0,0 +1,383 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:engineering-platform:genesis-local-bootstrap:1.0", + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "properties": { + "contract_version": { + "type": "string", + "const": "1.0" + }, + "operation_kind": { + "type": "string", + "const": "CREATE_GENESIS_LOCAL" + }, + "operation_id": { + "type": "string", + "pattern": "[A-Za-z0-9][A-Za-z0-9._:-]{0,127}" + }, + "producer_version": { + "type": "string", + "maxLength": 32 + }, + "execution_mode": { + "type": "string", + "const": "GENESIS" + }, + "baseline": { + "type": "string", + "const": "portable-docs/1.0.0" + }, + "authorization_ref": { + "type": "string", + "pattern": "[A-Za-z0-9][A-Za-z0-9._:-]{0,127}" + }, + "placement_precondition": { + "type": "string", + "const": "ABSENT_VERIFIED_AT_ADMISSION" + }, + "request_digest": { + "type": "string", + "pattern": "[0-9a-f]{64}" + }, + "target_ref": { + "type": "string", + "pattern": "[A-Za-z0-9][A-Za-z0-9._:-]{0,127}" + }, + "state": { + "type": "string", + "enum": [ + "RESERVED", + "EXECUTING", + "COMPLETE", + "PARTIAL", + "EFFECT_UNCERTAIN" + ] + }, + "effect_plan_binding": { + "type": "object", + "additionalProperties": false, + "properties": { + "context_id": { + "type": "string", + "pattern": "[A-Za-z0-9][A-Za-z0-9._:-]{0,127}" + }, + "decision_digest": { + "type": "string", + "pattern": "[0-9a-f]{64}" + }, + "decision_id": { + "type": "string", + "pattern": "[A-Za-z0-9][A-Za-z0-9._:-]{0,127}" + }, + "plan_digest": { + "type": "string", + "pattern": "[0-9a-f]{64}" + }, + "plan_id": { + "type": "string", + "pattern": "[A-Za-z0-9][A-Za-z0-9._:-]{0,127}" + }, + "plan_revision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "proposal_digest": { + "type": "string", + "pattern": "[0-9a-f]{64}" + }, + "proposal_id": { + "type": "string", + "pattern": "[A-Za-z0-9][A-Za-z0-9._:-]{0,127}" + }, + "source_actor_id": { + "type": "string", + "pattern": "[A-Za-z0-9][A-Za-z0-9._:-]{0,127}" + }, + "source_instance_id": { + "type": "string", + "pattern": "[A-Za-z0-9][A-Za-z0-9._:-]{0,127}" + } + }, + "required": [ + "context_id", + "decision_digest", + "decision_id", + "plan_digest", + "plan_id", + "plan_revision", + "proposal_digest", + "proposal_id", + "source_actor_id", + "source_instance_id" + ] + }, + "bundle_digest": { + "type": "string", + "pattern": "[0-9a-f]{64}" + }, + "manifest_digest": { + "type": "string", + "pattern": "[0-9a-f]{64}" + }, + "input_digest": { + "type": "string", + "pattern": "[0-9a-f]{64}" + }, + "instance_id": { + "type": "string", + "pattern": "[A-Za-z0-9][A-Za-z0-9._:-]{0,127}" + }, + "host_id": { + "type": "string", + "pattern": "[0-9a-f]{64}" + }, + "identity": { + "type": "object", + "additionalProperties": false, + "properties": { + "project_id": { + "type": "string", + "pattern": "[A-Za-z0-9][A-Za-z0-9._:-]{0,127}" + }, + "authority_repository_id": { + "type": "string", + "pattern": "[A-Za-z0-9][A-Za-z0-9._:-]{0,127}" + }, + "repository_id": { + "type": "string", + "pattern": "[A-Za-z0-9][A-Za-z0-9._:-]{0,127}" + }, + "state": { + "type": "string", + "enum": [ + "RESERVED", + "COMMITTED", + "REGISTERED" + ] + } + }, + "required": [ + "project_id", + "authority_repository_id", + "repository_id", + "state" + ] + }, + "completed_steps": { + "type": "array", + "maxItems": 10, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "PREFLIGHT", + "CREATE_TARGET", + "WRITE_FILES", + "VALIDATE_DOCS", + "GIT_INIT", + "GIT_PREPARE", + "GIT_OBJECT", + "GIT_REF", + "VERIFY_COMMIT", + "REGISTER_AND_ATTACH" + ] + } + }, + "step_effects": { + "type": "array", + "maxItems": 256, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "attempt_id": { + "type": "string", + "pattern": "[0-9a-f]{32}" + }, + "step": { + "type": "string", + "enum": [ + "PREFLIGHT", + "CREATE_TARGET", + "WRITE_FILES", + "VALIDATE_DOCS", + "GIT_INIT", + "GIT_PREPARE", + "GIT_OBJECT", + "GIT_REF", + "VERIFY_COMMIT", + "REGISTER_AND_ATTACH" + ] + }, + "outcome": { + "type": "string", + "enum": [ + "INTENT_RECORDED", + "STEP_PERMITTED", + "STEP_COMPLETED" + ] + }, + "exit_code": { + "type": [ + "integer", + "null" + ], + "minimum": -255, + "maximum": 255 + }, + "private_evidence_ref": { + "type": "string", + "pattern": "genesis-attempt:[0-9a-f]{32}" + } + }, + "required": [ + "attempt_id", + "step", + "outcome", + "exit_code", + "private_evidence_ref" + ] + } + }, + "commit": { + "type": [ + "string", + "null" + ], + "pattern": "[0-9a-f]{40}" + }, + "tree": { + "type": [ + "string", + "null" + ], + "pattern": "[0-9a-f]{40}" + }, + "validator_sha256": { + "type": [ + "string", + "null" + ], + "pattern": "[0-9a-f]{64}" + }, + "validation": { + "type": [ + "object", + "null" + ], + "additionalProperties": false, + "properties": { + "state": { + "type": "string", + "const": "VALIDATED_PROPOSAL" + }, + "manifest_digest": { + "type": "string", + "pattern": "[0-9a-f]{64}" + }, + "product_code_coverage": { + "type": "string", + "const": "NOT_APPLICABLE_NO_PRODUCT_CODE" + }, + "artifacts": { + "type": "integer", + "const": 37 + } + }, + "required": [ + "state", + "manifest_digest", + "product_code_coverage", + "artifacts" + ] + }, + "last_error": { + "type": [ + "string", + "null" + ], + "pattern": "BOOTSTRAP_[A-Z0-9_]{1,96}" + }, + "source_updated_at": { + "type": "string", + "format": "date-time" + }, + "observed_at": { + "type": "string", + "format": "date-time" + }, + "forge_readiness": { + "type": "string", + "const": "NOT_OWNED_BY_EP" + }, + "remote_governance": { + "type": "string", + "const": "NOT_APPLICABLE_GENESIS" + }, + "product_code_coverage": { + "type": "string", + "const": "NOT_APPLICABLE_NO_PRODUCT_CODE" + }, + "in_repository_validation_command": { + "type": "string", + "const": "NOT_QUALIFIED_AFTER_GIT_METADATA" + }, + "effect_result_semantics": { + "type": "string", + "const": "ORIGINAL_OPERATION_AS_OF_RECORDED_STEP_EVIDENCE" + } + }, + "required": [ + "contract_version", + "operation_kind", + "operation_id", + "producer_version", + "execution_mode", + "baseline", + "authorization_ref", + "placement_precondition", + "request_digest", + "target_ref", + "state", + "effect_plan_binding", + "bundle_digest", + "manifest_digest", + "input_digest", + "instance_id", + "host_id", + "identity", + "completed_steps", + "step_effects", + "commit", + "tree", + "validator_sha256", + "validation", + "last_error", + "source_updated_at", + "observed_at", + "forge_readiness", + "remote_governance", + "product_code_coverage", + "in_repository_validation_command", + "effect_result_semantics" + ] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "error": { + "type": "string", + "pattern": "BOOTSTRAP_[A-Z0-9_]{1,96}" + } + }, + "required": [ + "error" + ] + } + ] +} diff --git a/forge/api/genesis-apply-v1.json b/forge/api/genesis-apply-v1.json new file mode 100644 index 0000000..834c2f1 --- /dev/null +++ b/forge/api/genesis-apply-v1.json @@ -0,0 +1,615 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://forge.dev/api/genesis-apply-v1.json", + "title": "Explicit original Genesis apply intent and owning EP readback; not project readiness", + "$defs": { + "ApplyRequest": { + "type": "object", + "additionalProperties": false, + "properties": { + "contract_version": { + "const": "forge-genesis-apply/v1" + }, + "instance_id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$", + "maxLength": 128 + }, + "dossier_id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$", + "maxLength": 128 + }, + "operation_id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$", + "maxLength": 128 + }, + "plan_id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$", + "maxLength": 128 + }, + "decision_id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$", + "maxLength": 128 + }, + "pairing_id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$", + "maxLength": 128 + }, + "plan_digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "maxLength": 64 + }, + "decision_digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "maxLength": 64 + } + }, + "required": [ + "contract_version", + "instance_id", + "dossier_id", + "operation_id", + "plan_id", + "decision_id", + "pairing_id", + "plan_digest", + "decision_digest" + ] + }, + "ResumeRequest": { + "type": "object", + "additionalProperties": false, + "properties": { + "contract_version": { + "const": "forge-genesis-apply/v1" + }, + "instance_id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$", + "maxLength": 128 + }, + "dossier_id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$", + "maxLength": 128 + }, + "operation_id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$", + "maxLength": 128 + }, + "attempt_id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$", + "maxLength": 128 + }, + "original_request_digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "maxLength": 64 + } + }, + "required": [ + "contract_version", + "instance_id", + "dossier_id", + "operation_id", + "attempt_id", + "original_request_digest" + ] + }, + "Response": { + "type": "object", + "additionalProperties": false, + "properties": { + "contract_version": { + "const": "forge-genesis-apply/v1" + }, + "instance_id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$", + "maxLength": 128 + }, + "principal_id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$", + "maxLength": 128 + }, + "audience": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$", + "maxLength": 128 + }, + "dossier_id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$", + "maxLength": 128 + }, + "operation_id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$", + "maxLength": 128 + }, + "original_request": { + "$ref": "#/$defs/ApplyRequest" + }, + "original_request_digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "maxLength": 64 + }, + "ep_request_digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "maxLength": 64 + }, + "pairing_digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "maxLength": 64 + }, + "effect_decision_currently_applicable": { + "type": "boolean" + }, + "state": { + "enum": [ + "NOT_REQUESTED", + "EFFECT_UNCERTAIN", + "RESERVED", + "EXECUTING", + "COMPLETE", + "PARTIAL" + ] + }, + "original_ep_operation": { + "anyOf": [ + { + "type": "object", + "additionalProperties": false, + "properties": { + "contract_version": { + "type": "string", + "const": "1.0" + }, + "operation_kind": { + "type": "string", + "const": "CREATE_GENESIS_LOCAL" + }, + "operation_id": { + "type": "string", + "pattern": "[A-Za-z0-9][A-Za-z0-9._:-]{0,127}" + }, + "producer_version": { + "type": "string", + "maxLength": 32 + }, + "execution_mode": { + "type": "string", + "const": "GENESIS" + }, + "baseline": { + "type": "string", + "const": "portable-docs/1.0.0" + }, + "authorization_ref": { + "type": "string", + "pattern": "[A-Za-z0-9][A-Za-z0-9._:-]{0,127}" + }, + "placement_precondition": { + "type": "string", + "const": "ABSENT_VERIFIED_AT_ADMISSION" + }, + "request_digest": { + "type": "string", + "pattern": "[0-9a-f]{64}" + }, + "target_ref": { + "type": "string", + "pattern": "[A-Za-z0-9][A-Za-z0-9._:-]{0,127}" + }, + "state": { + "type": "string", + "enum": [ + "RESERVED", + "EXECUTING", + "COMPLETE", + "PARTIAL", + "EFFECT_UNCERTAIN" + ] + }, + "effect_plan_binding": { + "type": "object", + "additionalProperties": false, + "properties": { + "context_id": { + "type": "string", + "pattern": "[A-Za-z0-9][A-Za-z0-9._:-]{0,127}" + }, + "decision_digest": { + "type": "string", + "pattern": "[0-9a-f]{64}" + }, + "decision_id": { + "type": "string", + "pattern": "[A-Za-z0-9][A-Za-z0-9._:-]{0,127}" + }, + "plan_digest": { + "type": "string", + "pattern": "[0-9a-f]{64}" + }, + "plan_id": { + "type": "string", + "pattern": "[A-Za-z0-9][A-Za-z0-9._:-]{0,127}" + }, + "plan_revision": { + "type": "integer", + "minimum": 1, + "maximum": 1000000 + }, + "proposal_digest": { + "type": "string", + "pattern": "[0-9a-f]{64}" + }, + "proposal_id": { + "type": "string", + "pattern": "[A-Za-z0-9][A-Za-z0-9._:-]{0,127}" + }, + "source_actor_id": { + "type": "string", + "pattern": "[A-Za-z0-9][A-Za-z0-9._:-]{0,127}" + }, + "source_instance_id": { + "type": "string", + "pattern": "[A-Za-z0-9][A-Za-z0-9._:-]{0,127}" + } + }, + "required": [ + "context_id", + "decision_digest", + "decision_id", + "plan_digest", + "plan_id", + "plan_revision", + "proposal_digest", + "proposal_id", + "source_actor_id", + "source_instance_id" + ] + }, + "bundle_digest": { + "type": "string", + "pattern": "[0-9a-f]{64}" + }, + "manifest_digest": { + "type": "string", + "pattern": "[0-9a-f]{64}" + }, + "input_digest": { + "type": "string", + "pattern": "[0-9a-f]{64}" + }, + "instance_id": { + "type": "string", + "pattern": "[A-Za-z0-9][A-Za-z0-9._:-]{0,127}" + }, + "host_id": { + "type": "string", + "pattern": "[0-9a-f]{64}" + }, + "identity": { + "type": "object", + "additionalProperties": false, + "properties": { + "project_id": { + "type": "string", + "pattern": "[A-Za-z0-9][A-Za-z0-9._:-]{0,127}" + }, + "authority_repository_id": { + "type": "string", + "pattern": "[A-Za-z0-9][A-Za-z0-9._:-]{0,127}" + }, + "repository_id": { + "type": "string", + "pattern": "[A-Za-z0-9][A-Za-z0-9._:-]{0,127}" + }, + "state": { + "type": "string", + "enum": [ + "RESERVED", + "COMMITTED", + "REGISTERED" + ] + } + }, + "required": [ + "project_id", + "authority_repository_id", + "repository_id", + "state" + ] + }, + "completed_steps": { + "type": "array", + "maxItems": 10, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "PREFLIGHT", + "CREATE_TARGET", + "WRITE_FILES", + "VALIDATE_DOCS", + "GIT_INIT", + "GIT_PREPARE", + "GIT_OBJECT", + "GIT_REF", + "VERIFY_COMMIT", + "REGISTER_AND_ATTACH" + ] + } + }, + "step_effects": { + "type": "array", + "maxItems": 256, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "attempt_id": { + "type": "string", + "pattern": "[0-9a-f]{32}" + }, + "step": { + "type": "string", + "enum": [ + "PREFLIGHT", + "CREATE_TARGET", + "WRITE_FILES", + "VALIDATE_DOCS", + "GIT_INIT", + "GIT_PREPARE", + "GIT_OBJECT", + "GIT_REF", + "VERIFY_COMMIT", + "REGISTER_AND_ATTACH" + ] + }, + "outcome": { + "type": "string", + "enum": [ + "INTENT_RECORDED", + "STEP_PERMITTED", + "STEP_COMPLETED" + ] + }, + "exit_code": { + "type": [ + "integer", + "null" + ], + "minimum": -255, + "maximum": 255 + }, + "private_evidence_ref": { + "type": "string", + "pattern": "genesis-attempt:[0-9a-f]{32}" + } + }, + "required": [ + "attempt_id", + "step", + "outcome", + "exit_code", + "private_evidence_ref" + ] + } + }, + "commit": { + "type": [ + "string", + "null" + ], + "pattern": "[0-9a-f]{40}" + }, + "tree": { + "type": [ + "string", + "null" + ], + "pattern": "[0-9a-f]{40}" + }, + "validator_sha256": { + "type": [ + "string", + "null" + ], + "pattern": "[0-9a-f]{64}" + }, + "validation": { + "type": [ + "object", + "null" + ], + "additionalProperties": false, + "properties": { + "state": { + "type": "string", + "const": "VALIDATED_PROPOSAL" + }, + "manifest_digest": { + "type": "string", + "pattern": "[0-9a-f]{64}" + }, + "product_code_coverage": { + "type": "string", + "const": "NOT_APPLICABLE_NO_PRODUCT_CODE" + }, + "artifacts": { + "type": "integer", + "const": 37 + } + }, + "required": [ + "state", + "manifest_digest", + "product_code_coverage", + "artifacts" + ] + }, + "last_error": { + "type": [ + "string", + "null" + ], + "pattern": "BOOTSTRAP_[A-Z0-9_]{1,96}" + }, + "source_updated_at": { + "type": "string", + "format": "date-time" + }, + "observed_at": { + "type": "string", + "format": "date-time" + }, + "forge_readiness": { + "type": "string", + "const": "NOT_OWNED_BY_EP" + }, + "remote_governance": { + "type": "string", + "const": "NOT_APPLICABLE_GENESIS" + }, + "product_code_coverage": { + "type": "string", + "const": "NOT_APPLICABLE_NO_PRODUCT_CODE" + }, + "in_repository_validation_command": { + "type": "string", + "const": "NOT_QUALIFIED_AFTER_GIT_METADATA" + }, + "effect_result_semantics": { + "type": "string", + "const": "ORIGINAL_OPERATION_AS_OF_RECORDED_STEP_EVIDENCE" + } + }, + "required": [ + "contract_version", + "operation_kind", + "operation_id", + "producer_version", + "execution_mode", + "baseline", + "authorization_ref", + "placement_precondition", + "request_digest", + "target_ref", + "state", + "effect_plan_binding", + "bundle_digest", + "manifest_digest", + "input_digest", + "instance_id", + "host_id", + "identity", + "completed_steps", + "step_effects", + "commit", + "tree", + "validator_sha256", + "validation", + "last_error", + "source_updated_at", + "observed_at", + "forge_readiness", + "remote_governance", + "product_code_coverage", + "in_repository_validation_command", + "effect_result_semantics" + ] + }, + { + "type": "null" + } + ] + }, + "explicit_attempts": { + "type": "integer", + "minimum": 0, + "maximum": 16 + }, + "project_readiness": { + "const": "NOT_ASSESSED" + }, + "mission": { + "const": "NOT_REQUESTED" + } + }, + "required": [ + "contract_version", + "instance_id", + "principal_id", + "audience", + "dossier_id", + "operation_id", + "original_request", + "original_request_digest", + "ep_request_digest", + "pairing_digest", + "effect_decision_currently_applicable", + "state", + "original_ep_operation", + "explicit_attempts", + "project_readiness", + "mission" + ] + }, + "ErrorResponse": { + "type": "object", + "additionalProperties": false, + "properties": { + "contract_version": { + "const": "forge-genesis-apply/v1" + }, + "error": { + "type": "object", + "additionalProperties": false, + "properties": { + "code": { + "type": "string", + "pattern": "^[A-Z][A-Z0-9_]{0,127}$", + "maxLength": 128 + } + }, + "required": [ + "code" + ] + } + }, + "required": [ + "contract_version", + "error" + ] + } + }, + "oneOf": [ + { + "$ref": "#/$defs/ApplyRequest" + }, + { + "$ref": "#/$defs/ResumeRequest" + }, + { + "$ref": "#/$defs/Response" + }, + { + "$ref": "#/$defs/ErrorResponse" + } + ] +} diff --git a/forge/api/server-openapi-v1.json b/forge/api/server-openapi-v1.json index cb2bea2..9bc3187 100644 --- a/forge/api/server-openapi-v1.json +++ b/forge/api/server-openapi-v1.json @@ -5971,6 +5971,320 @@ } } } + }, + "/v1/genesis-apply/operations": { + "post": { + "summary": "POST original Genesis operation", + "security": [ + { + "GenesisApplyBearer": [] + } + ], + "parameters": [ + { + "name": "X-Forge-Genesis-Apply-Audience", + "in": "header", + "required": true, + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$", + "maxLength": 128 + } + } + ], + "description": "Explicit owner-paired CREATE_GENESIS_LOCAL over exact original R52/R54/R56 binding. GET never sends effects; lost replies recover original operation. No project readiness or Mission.", + "responses": { + "200": { + "description": "Original operation response", + "content": { + "application/json": { + "schema": { + "$ref": "genesis-apply-v1.json#/$defs/Response" + } + } + } + }, + "400": { + "description": "Closed refusal or unavailable original", + "content": { + "application/json": { + "schema": { + "$ref": "genesis-apply-v1.json#/$defs/ErrorResponse" + } + } + } + }, + "403": { + "description": "Closed refusal or unavailable original", + "content": { + "application/json": { + "schema": { + "$ref": "genesis-apply-v1.json#/$defs/ErrorResponse" + } + } + } + }, + "409": { + "description": "Closed refusal or unavailable original", + "content": { + "application/json": { + "schema": { + "$ref": "genesis-apply-v1.json#/$defs/ErrorResponse" + } + } + } + }, + "424": { + "description": "Closed refusal or unavailable original", + "content": { + "application/json": { + "schema": { + "$ref": "genesis-apply-v1.json#/$defs/ErrorResponse" + } + } + } + }, + "503": { + "description": "Closed refusal or unavailable original", + "content": { + "application/json": { + "schema": { + "$ref": "genesis-apply-v1.json#/$defs/ErrorResponse" + } + } + } + }, + "401": { + "description": "Current authentication required; shared Server authentication envelope" + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "genesis-apply-v1.json#/$defs/ApplyRequest" + } + } + } + } + } + }, + "/v1/genesis-apply/operations/{operation_id}": { + "get": { + "summary": "GET original Genesis operation", + "security": [ + { + "GenesisApplyBearer": [] + } + ], + "parameters": [ + { + "name": "X-Forge-Genesis-Apply-Audience", + "in": "header", + "required": true, + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$", + "maxLength": 128 + } + }, + { + "name": "operation_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$", + "maxLength": 128 + } + }, + { + "name": "dossier_id", + "in": "query", + "required": true, + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$", + "maxLength": 128 + } + } + ], + "description": "Explicit owner-paired CREATE_GENESIS_LOCAL over exact original R52/R54/R56 binding. GET never sends effects; lost replies recover original operation. No project readiness or Mission.", + "responses": { + "200": { + "description": "Original operation response", + "content": { + "application/json": { + "schema": { + "$ref": "genesis-apply-v1.json#/$defs/Response" + } + } + } + }, + "400": { + "description": "Closed refusal or unavailable original", + "content": { + "application/json": { + "schema": { + "$ref": "genesis-apply-v1.json#/$defs/ErrorResponse" + } + } + } + }, + "403": { + "description": "Closed refusal or unavailable original", + "content": { + "application/json": { + "schema": { + "$ref": "genesis-apply-v1.json#/$defs/ErrorResponse" + } + } + } + }, + "409": { + "description": "Closed refusal or unavailable original", + "content": { + "application/json": { + "schema": { + "$ref": "genesis-apply-v1.json#/$defs/ErrorResponse" + } + } + } + }, + "424": { + "description": "Closed refusal or unavailable original", + "content": { + "application/json": { + "schema": { + "$ref": "genesis-apply-v1.json#/$defs/ErrorResponse" + } + } + } + }, + "503": { + "description": "Closed refusal or unavailable original", + "content": { + "application/json": { + "schema": { + "$ref": "genesis-apply-v1.json#/$defs/ErrorResponse" + } + } + } + }, + "401": { + "description": "Current authentication required; shared Server authentication envelope" + } + } + } + }, + "/v1/genesis-apply/operations/{operation_id}/resume": { + "post": { + "summary": "POST original Genesis operation", + "security": [ + { + "GenesisApplyBearer": [] + } + ], + "parameters": [ + { + "name": "X-Forge-Genesis-Apply-Audience", + "in": "header", + "required": true, + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$", + "maxLength": 128 + } + }, + { + "name": "operation_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$", + "maxLength": 128 + } + } + ], + "description": "Explicit owner-paired CREATE_GENESIS_LOCAL over exact original R52/R54/R56 binding. GET never sends effects; lost replies recover original operation. No project readiness or Mission.", + "responses": { + "200": { + "description": "Original operation response", + "content": { + "application/json": { + "schema": { + "$ref": "genesis-apply-v1.json#/$defs/Response" + } + } + } + }, + "400": { + "description": "Closed refusal or unavailable original", + "content": { + "application/json": { + "schema": { + "$ref": "genesis-apply-v1.json#/$defs/ErrorResponse" + } + } + } + }, + "403": { + "description": "Closed refusal or unavailable original", + "content": { + "application/json": { + "schema": { + "$ref": "genesis-apply-v1.json#/$defs/ErrorResponse" + } + } + } + }, + "409": { + "description": "Closed refusal or unavailable original", + "content": { + "application/json": { + "schema": { + "$ref": "genesis-apply-v1.json#/$defs/ErrorResponse" + } + } + } + }, + "424": { + "description": "Closed refusal or unavailable original", + "content": { + "application/json": { + "schema": { + "$ref": "genesis-apply-v1.json#/$defs/ErrorResponse" + } + } + } + }, + "503": { + "description": "Closed refusal or unavailable original", + "content": { + "application/json": { + "schema": { + "$ref": "genesis-apply-v1.json#/$defs/ErrorResponse" + } + } + } + }, + "401": { + "description": "Current authentication required; shared Server authentication envelope" + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "genesis-apply-v1.json#/$defs/ResumeRequest" + } + } + } + } + } } }, "components": { @@ -6053,6 +6367,11 @@ "type": "http", "scheme": "bearer", "description": "Explicit finite GENESIS_BOOTSTRAP_EFFECT_ONLY/1.0 grant; no EP execution authority" + }, + "GenesisApplyBearer": { + "type": "http", + "scheme": "bearer", + "description": "Separate finite owner-issued READ/REQUEST_APPLY/RESUME authority for exact original source plans. Never the EP provisioning grant." } }, "schemas": { diff --git a/forge/api/server-postman-v1.json b/forge/api/server-postman-v1.json index da278aa..e02d5ab 100644 --- a/forge/api/server-postman-v1.json +++ b/forge/api/server-postman-v1.json @@ -3155,6 +3155,129 @@ }, "description": "Finite exact Genesis effect plan or separate human effect decision. EP admission remains required; no execution authority." } + }, + { + "name": "POST /v1/genesis-apply/operations", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{genesisApplyToken}}" + }, + { + "key": "X-Forge-Genesis-Apply-Audience", + "value": "{{genesisApplyAudience}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/v1/genesis-apply/operations", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "v1", + "genesis-apply", + "operations" + ] + }, + "description": "Explicit owner-paired CREATE_GENESIS_LOCAL over exact original R52/R54/R56 binding. GET never sends effects; lost replies recover original operation. No project readiness or Mission.", + "body": { + "mode": "raw", + "raw": "{}", + "options": { + "raw": { + "language": "json" + } + } + } + } + }, + { + "name": "GET /v1/genesis-apply/operations/{operation_id}", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{genesisApplyToken}}" + }, + { + "key": "X-Forge-Genesis-Apply-Audience", + "value": "{{genesisApplyAudience}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/v1/genesis-apply/operations/{operation_id}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "v1", + "genesis-apply", + "operations", + "{operation_id}" + ], + "query": [ + { + "key": "dossier_id", + "value": "{{dossierId}}" + } + ] + }, + "description": "Explicit owner-paired CREATE_GENESIS_LOCAL over exact original R52/R54/R56 binding. GET never sends effects; lost replies recover original operation. No project readiness or Mission." + } + }, + { + "name": "POST /v1/genesis-apply/operations/{operation_id}/resume", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{genesisApplyToken}}" + }, + { + "key": "X-Forge-Genesis-Apply-Audience", + "value": "{{genesisApplyAudience}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/v1/genesis-apply/operations/{operation_id}/resume", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "v1", + "genesis-apply", + "operations", + "{operation_id}", + "resume" + ] + }, + "description": "Explicit owner-paired CREATE_GENESIS_LOCAL over exact original R52/R54/R56 binding. GET never sends effects; lost replies recover original operation. No project readiness or Mission.", + "body": { + "mode": "raw", + "raw": "{}", + "options": { + "raw": { + "language": "json" + } + } + } + } } ] } diff --git a/forge/genesis_apply_cli.py b/forge/genesis_apply_cli.py new file mode 100644 index 0000000..31e2d84 --- /dev/null +++ b/forge/genesis_apply_cli.py @@ -0,0 +1,67 @@ +"""Owner private setup and CLI parity over the same Genesis coordinator.""" +import argparse +import json +from pathlib import Path +import sqlite3 +from .genesis_apply_contract import CONTRACT +from .genesis_apply_grant import GenesisApplyGrant +from .genesis_apply_pairing import GenesisApplyPairings +from .genesis_apply_service import GenesisApplyService, BASE +from .concept_advisory_cli import publish_cli +from .workspace_review_grant import _private_bytes +from .project_bootstrap.schema import load_json + + +def main(argv=None): + parser = argparse.ArgumentParser(prog='forge-genesis-apply') + parser.add_argument('--data-root', required=True) + sub = parser.add_subparsers(dest='action', required=True); sub.add_parser('init') + issue = sub.add_parser('issue') + for key in ('principal-id', 'proposal-principal-id', 'audience', 'expires-at', 'token-file'): + issue.add_argument('--'+key, required=True) + for key in ('dossier-id', 'source-plan-id', 'permission'): + issue.add_argument('--'+key, action='append', required=True) + issue.add_argument('--exact-plans-file') + revoke = sub.add_parser('revoke'); revoke.add_argument('--grant-id', required=True) + bind = sub.add_parser('bind-peer'); bind.add_argument('--config-file', required=True); bind.add_argument('--credential-file', required=True) + revoke_peer = sub.add_parser('revoke-peer'); revoke_peer.add_argument('--pairing-id', required=True) + for action in ('request', 'read', 'resume'): + item = sub.add_parser(action); item.add_argument('--token-file', required=True); item.add_argument('--audience', required=True) + if action != 'read': item.add_argument('--request-file', required=True) + if action != 'request': item.add_argument('--operation-id', required=True) + if action == 'read': item.add_argument('--dossier-id', required=True) + args = parser.parse_args(argv) + try: + from .server_runtime import existing_instance + instance = existing_instance(args.data_root); grant = GenesisApplyGrant(instance.data_root, instance.instance_id) + if args.action == 'init': + grant.initialize(); document = {'contract_version':CONTRACT, 'state':'PRIVATE_GENESIS_APPLY_INITIALIZED'} + elif args.action == 'issue': + plans = load_json(_private_bytes(Path(args.exact_plans_file))) if args.exact_plans_file else [] + document = grant.issue(principal_id=args.principal_id, audience=args.audience, proposal_principal_id=args.proposal_principal_id, + dossier_ids=args.dossier_id, source_plan_ids=args.source_plan_id, permissions=args.permission, exact_plans=plans, + expires_at=args.expires_at, token_path=args.token_file) + elif args.action == 'revoke': document = grant.revoke(args.grant_id) + elif args.action == 'bind-peer': + document = GenesisApplyPairings(grant).bind(load_json(_private_bytes(Path(args.config_file))), args.credential_file) + elif args.action == 'revoke-peer': document = GenesisApplyPairings(grant).revoke(args.pairing_id) + else: + token = _private_bytes(Path(args.token_file)) + if len(token) > 256: raise ValueError('finite private bearer required') + auth = 'Bearer '+token.decode('ascii').strip(); service = GenesisApplyService(grant) + if args.action == 'read': status, document = service.handle('GET', BASE+'/operations/'+args.operation_id+'?dossier_id='+args.dossier_id, auth, args.audience) + else: + path = BASE+'/operations'+('/'+args.operation_id+'/resume' if args.action == 'resume' else '') + status, document = service.handle('POST', path, auth, args.audience, load_json(_private_bytes(Path(args.request_file)))) + if status != 200: + print(json.dumps(document, sort_keys=True)); return 1 + try: publish_cli(grant, auth, args.audience, document, chunk_bytes=65536) + except (OSError, ValueError, TypeError, KeyError, RuntimeError, PermissionError, sqlite3.Error): return 1 + return 0 + print(json.dumps(document, sort_keys=True)); return 0 + except (OSError, ValueError, TypeError, KeyError, RuntimeError, PermissionError, sqlite3.Error): + print(json.dumps({'contract_version':CONTRACT, 'error':{'code':'GENESIS_APPLY_CLI_UNAVAILABLE'}})); return 1 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/forge/genesis_apply_contract.py b/forge/genesis_apply_contract.py new file mode 100644 index 0000000..284de87 --- /dev/null +++ b/forge/genesis_apply_contract.py @@ -0,0 +1,62 @@ +"""Lossless Genesis dispatch binding from owning private source snapshots. + +These pure checks confer no authority. The coordinator must obtain snapshots +from the owning services and recheck current grants before any network effect. +""" +from .approved_worklist import identifier +from .genesis_bootstrap_contract import CONTRACT as PLAN_CONTRACT, EFFECT_PURPOSE, frozen_bundle +from .project_bootstrap.schema import canonical, load_json, ContractError +from .project_bootstrap.service import digest + +CONTRACT = 'forge-genesis-apply/v1' + + +def apply_request(value): + """Closed user input contains opaque references only; never EP credentials.""" + fields = {'contract_version', 'instance_id', 'dossier_id', 'operation_id', + 'plan_id', 'plan_digest', 'decision_id', 'decision_digest', 'pairing_id'} + if not isinstance(value, dict) or set(value) != fields or value['contract_version'] != CONTRACT: + raise ContractError('closed Genesis execution request required') + from .proposal_content_contract import hash_value + for key in fields - {'contract_version'}: + (hash_value if key.endswith('_digest') else identifier)(value[key]) + return load_json(canonical(value)), digest(canonical(value)) + + +def ep_request(original, plan, decision, operation_id): + """Copy original document bytes into EP's distinct envelope, without render.""" + identifier(operation_id) + if (plan['contract_version'] != PLAN_CONTRACT + or plan['plan_digest'] != digest(canonical({k: v for k, v in plan.items() if k != 'plan_digest'})) + or decision['decision_digest'] != digest(canonical({k: v for k, v in decision.items() if k != 'decision_digest'}))): + raise ContractError('original plan or decision digest differs') + if (decision['contract_version'] != PLAN_CONTRACT or decision['purpose'] != EFFECT_PURPOSE + or decision['decision'] != 'APPROVE_EFFECT' + or (decision['instance_id'], decision['dossier_id'], decision['audience'], decision['plan_id'], decision['plan_digest']) + != (plan['instance_id'], plan['dossier_id'], plan['audience'], plan['plan_id'], plan['plan_digest'])): + raise ContractError('exact original positive effect decision required') + documents = frozen_bundle(original) + if (documents != plan['documents'] or original['plan_id'] != plan['source_subject']['plan_id'] + or digest(canonical(original['result']['artifacts'])) != plan['source_subject']['result_digest']): + raise ContractError('original frozen proposal differs') + envelope = {'contract_version': 'pb-render/1.0', 'kind': 'RENDERED_PROPOSAL_EXAMPLE', + 'files': {row['path']: {'sha256': row['sha256'], 'utf8': row['content']} for row in documents['files']}} + if digest(canonical(envelope)) != documents['ep_bundle_binding']['canonical_digest']: + raise ContractError('EP envelope differs from frozen original') + binding = {'plan_id': plan['plan_id'], 'plan_revision': plan['plan_revision'], 'plan_digest': plan['plan_digest'], + 'decision_id': decision['decision_id'], 'decision_digest': decision['decision_digest'], + 'proposal_id': original['plan_id'], 'proposal_digest': plan['source_subject']['result_digest'], + 'source_instance_id': plan['instance_id'], 'source_actor_id': decision['principal_id'], + 'context_id': plan['dossier_id']} + from .proposal_content_contract import hash_value + for key, value in binding.items(): + if key != 'plan_revision': + (hash_value if key.endswith('_digest') else identifier)(value) + if type(binding['plan_revision']) is not int or not 1 <= binding['plan_revision'] <= 8: + raise ContractError('finite original plan revision required') + identifier(plan['target']['target_ref']) + result = {'contract_version': '1.0', 'operation_id': operation_id, + 'target_ref': plan['target']['target_ref'], 'effect_plan_binding': binding, 'bundle': envelope} + if len(canonical(result)) > 524288: + raise ContractError('finite EP request required') + return result, digest(canonical(result)) diff --git a/forge/genesis_apply_grant.py b/forge/genesis_apply_grant.py new file mode 100644 index 0000000..46f433f --- /dev/null +++ b/forge/genesis_apply_grant.py @@ -0,0 +1,197 @@ +"""Separate finite apply authority; existing plan/decision grants stay non-executing.""" +from contextlib import contextmanager +from dataclasses import dataclass +from datetime import UTC, datetime, timedelta +from hashlib import sha256 +from pathlib import Path +import json +import secrets +from .approved_worklist import identifier, timestamp +from .project_bootstrap.schema import canonical +from .project_proposal_grant import ProjectProposalGrant, operator_binding +from .proposal_content_grant import ProposalContentGrant +from .candidate_decision_grant import signer +from .runtime.dynamic_mission import InstalledDynamicMissionRuntime +from .workspace_review_grant import _private_bytes, _write_private, _new_token +from .proposal_content_contract import hash_value +from .genesis_bootstrap_contract import finite_expiry +from .genesis_apply_contract import CONTRACT +from .genesis_bootstrap_grant import human, GenesisBootstrapGrant + +PURPOSE = "GENESIS_APPLY_ONLY/1.0" + + + +@dataclass(frozen=True) +class ApplyPrincipal: + grant_id: str + principal_id: str + instance_id: str + audience: str + proposal_principal_id: str + dossier_ids: tuple + source_plan_ids: tuple + permissions: tuple + exact_plans: tuple + purpose: str + human_binding: dict + operator_binding: str + + +class GenesisApplyGrant(ProjectProposalGrant): + contract = CONTRACT + + def __init__(self, root, instance_id): + super().__init__(root, instance_id) + self.path = self.root/'credentials'/'genesis-apply'/'grants.json' + self.store = self.root/'onboarding'/'genesis-apply'/'store.json' + self.witness = self.store.parent/'initialized.json' + + def initialize(self): + existed = self.witness.exists() or self.witness.is_symlink() + super().initialize() + from .genesis_apply_pairing import GenesisApplyPairings + with self.boundary(): + pairings = GenesisApplyPairings(self) + if existed: + pairings.records() + else: + pairings.save({}, create=True) + + def publication(self, authorization, audience, body, *, deadline=None): + from .genesis_apply_service import publication + return publication(self, authorization, audience, body, deadline=deadline) + + @contextmanager + def boundary(self, deadline=None): + # Global order: owning original proposal, content, Genesis, canonical DB. + with ProjectProposalGrant(self.root, self.instance_id).lock_until(deadline): + with ProposalContentGrant(self.root, self.instance_id).lock_until(deadline): + with GenesisBootstrapGrant(self.root, self.instance_id).lock_until(deadline): + with super().boundary(deadline): + yield + + @staticmethod + def bounds(dossiers, sources, permissions, plans): + for values in (dossiers, sources): + if not isinstance(values, (list, tuple)) or not 1 <= len(values) <= 8 or len(set(values)) != len(values): + raise ValueError('finite exact source scope required') + for value in values: + identifier(value) + if not isinstance(permissions, (list, tuple)) or not permissions or 'READ' not in permissions or len(set(permissions)) != len(permissions) or set(permissions)-{'READ','RESUME','REQUEST_APPLY'}: + raise ValueError('explicit Genesis permissions required') + if not isinstance(plans, (list, tuple)) or len(plans) > 8 or bool(plans) != bool(set(permissions) & {'REQUEST_APPLY','RESUME'}): + raise ValueError('decision requires finite already frozen plans') + seen = set() + for plan in plans: + if not isinstance(plan, dict) or set(plan) != {'plan_id','plan_digest'}: + raise ValueError('exact plan and digest required') + identifier(plan['plan_id']); hash_value(plan['plan_digest']) + if plan['plan_id'] in seen: + raise ValueError('duplicate exact plan') + seen.add(plan['plan_id']) + + def _records(self, missing=False): + if missing and not self.path.exists() and not self.path.is_symlink(): + return [] + value = json.loads(_private_bytes(self.path)) + if not isinstance(value, dict) or set(value) != {'contract_version','instance_id','records'} or value['contract_version'] != CONTRACT or value['instance_id'] != self.instance_id or not isinstance(value['records'], list) or len(value['records']) > 32: + raise ValueError('closed finite Genesis grant store required') + seen = set() + for row in value['records']: + if not isinstance(row, dict) or set(row) != set(ApplyPrincipal.__dataclass_fields__) | {'token_digest','expires_at','state'}: + raise ValueError('closed Genesis grant required') + for key in ('grant_id','principal_id','instance_id','audience','proposal_principal_id'): + identifier(row[key]) + self.bounds(row['dossier_ids'], row['source_plan_ids'], row['permissions'], row['exact_plans']) + finite_expiry(row['expires_at']); hash_value(row['operator_binding']); hash_value(row['token_digest']) + binding = row['human_binding'] + if not isinstance(binding, dict) or set(binding) != {'operator_id','operator_binding_version','installation_id'} or type(binding['operator_binding_version']) is not int or binding['operator_binding_version'] < 1: + raise ValueError('original human binding required') + identifier(binding['operator_id']); identifier(binding['installation_id']) + if row['instance_id'] != self.instance_id or row['purpose'] != PURPOSE or row['state'] not in ('ACTIVE','REVOKED') or row['grant_id'] in seen or (bool(set(row['permissions']) & {'REQUEST_APPLY','RESUME'}) and row['principal_id'] != binding['operator_id']): + raise ValueError('Genesis grant binding invalid') + seen.add(row['grant_id']) + return value['records'] + + def save_records(self, records): + raw = canonical({'contract_version':CONTRACT,'instance_id':self.instance_id,'records':records}) + if len(raw) > 131072: + raise ValueError('finite Genesis grant capacity exceeded') + _write_private(self.path, raw) + + def issue(self, *, principal_id, audience, proposal_principal_id, dossier_ids, source_plan_ids, permissions, exact_plans, expires_at, token_path): + for value in (principal_id, audience, proposal_principal_id): + identifier(value) + self.bounds(dossier_ids, source_plan_ids, permissions, exact_plans) + if not datetime.now(UTC) < finite_expiry(expires_at) <= datetime.now(UTC)+timedelta(days=30): + raise ValueError('finite Genesis grant expiry required') + token_path = Path(token_path) + # Credentials cannot overwrite any runtime state, other grants or evidence. + if token_path.resolve().is_relative_to(self.root.resolve()): + raise ValueError('external private token destination required') + with self.boundary(): + binding = human(self.root, self.instance_id, principal_id, bool(set(permissions) & {'REQUEST_APPLY','RESUME'})) + source = ProjectProposalGrant(self.root, self.instance_id) + from .project_proposal_service import ProjectProposalService + store = source._store() + for source_id in source_plan_ids: + record = ProjectProposalService(source)._record(store, source_id) + if record is None or record['dossier_id'] not in dossier_ids or record['principal_id'] != proposal_principal_id or record['audience'] != audience: + raise PermissionError('exact original source scope required') + self._store() + from .genesis_bootstrap_service import GenesisBootstrapService + own = GenesisBootstrapService(GenesisBootstrapGrant(self.root, self.instance_id)).store() + for plan in exact_plans: + row = own['records'].get(plan['plan_id']) + if row is None or row['kind'] != 'PLAN': + raise PermissionError('already frozen plan required') + doc = row['document'] + if doc['plan_digest'] != plan['plan_digest'] or doc['source_subject']['plan_id'] not in source_plan_ids or doc['dossier_id'] not in dossier_ids or doc['audience'] != audience or doc['source_subject']['proposal_principal_id'] != proposal_principal_id: + raise PermissionError('exact effect plan scope required') + rows = self._records(True) + if len(rows) >= 32: + raise ValueError('finite Genesis grant capacity exhausted') + token = secrets.token_urlsafe(48) + row = {'grant_id':'genesis-apply-'+secrets.token_hex(16),'principal_id':principal_id,'instance_id':self.instance_id,'audience':audience, + 'proposal_principal_id':proposal_principal_id,'dossier_ids':sorted(dossier_ids),'source_plan_ids':sorted(source_plan_ids), + 'permissions':sorted(permissions),'exact_plans':list(exact_plans),'purpose':PURPOSE,'human_binding':binding, + 'operator_binding':operator_binding(self.root,self.instance_id),'expires_at':expires_at,'state':'ACTIVE','token_digest':sha256(token.encode()).hexdigest()} + _new_token(token_path, token) + try: + self.save_records([*rows, row]) + except BaseException: + token_path.unlink(missing_ok=True) + raise + return {k:v for k,v in row.items() if k not in ('operator_binding','token_digest')} + + def current(self, row): + if row['state'] != 'ACTIVE' or finite_expiry(row['expires_at']) <= datetime.now(UTC) or row['operator_binding'] != operator_binding(self.root,self.instance_id): + raise PermissionError('Genesis grant inactive') + if human(self.root,self.instance_id,row['principal_id'],bool(set(row['permissions']) & {'REQUEST_APPLY','RESUME'})) != row['human_binding']: + raise PermissionError('current human binding changed') + + def authorize(self, authorization, *, audience=None, dossier_id=None, permission='READ', source_plan_id=None, plan=None): + if not isinstance(authorization,str) or not authorization.startswith('Bearer '): + raise PermissionError('separate Genesis apply credential required') + token = authorization[7:] + if not 1 <= len(token) <= 256 or any(ord(c) < 33 or ord(c) > 126 for c in token): + raise PermissionError('invalid Genesis apply credential') + rows = [r for r in self._records() if secrets.compare_digest(r['token_digest'],sha256(token.encode()).hexdigest())] + if len(rows) != 1: + raise PermissionError('Genesis apply credential required') + row = rows[0]; self.current(row) + if (audience is not None and audience != row['audience']) or (dossier_id is not None and dossier_id not in row['dossier_ids']) or permission not in row['permissions'] or (source_plan_id is not None and source_plan_id not in row['source_plan_ids']): + raise PermissionError('Genesis scope denied') + if plan is not None and permission in ('REQUEST_APPLY','RESUME') and {'plan_id':plan['plan_id'],'plan_digest':plan['plan_digest']} not in row['exact_plans']: + raise PermissionError('exact apply plan required') + return ApplyPrincipal(*(tuple(row[k]) if k in ('dossier_ids','source_plan_ids','permissions','exact_plans') else row[k] for k in ApplyPrincipal.__dataclass_fields__)) + + def revoke(self, grant_id): + identifier(grant_id) + with self.boundary(): + rows = self._records(); row = next((r for r in rows if r['grant_id']==grant_id),None) + if row is None: + raise ValueError('unknown Genesis grant') + row['state']='REVOKED'; self.save_records(rows) + return {'grant_id':grant_id,'state':'REVOKED'} diff --git a/forge/genesis_apply_journal.py b/forge/genesis_apply_journal.py new file mode 100644 index 0000000..38be09f --- /dev/null +++ b/forge/genesis_apply_journal.py @@ -0,0 +1,112 @@ +"""Bounded durable dispatch intents; EP remains the owner of effect truth. + +Callers hold the owning grant boundary and recheck authority. A persisted send +claim is never cleared, including after timeout or a lost response. Recovery +therefore reads the original EP operation instead of issuing another create. +""" +import secrets +from .approved_worklist import identifier +from .genesis_apply_contract import apply_request +from .genesis_bootstrap_contract import BootstrapConflict +from .project_bootstrap.schema import canonical, load_json +from .project_bootstrap.service import digest + + +class GenesisDispatchJournal: + def __init__(self, grant): + self.grant = grant + + def store(self): + try: + value = self.grant._store() + for key, row in value['records'].items(): + fields = {'request', 'request_digest', 'principal_id', 'audience', 'grant_id', + 'pairing_digest', 'ep_request', 'ep_request_digest', 'attempts', 'observation'} + if not isinstance(row, dict) or set(row) != fields: + raise ValueError('closed dispatch intent required') + request, request_digest = apply_request(row['request']) + if key != request['operation_id'] or request_digest != row['request_digest']: + raise ValueError('original dispatch identity differs') + for name in ('principal_id', 'audience', 'grant_id'): + identifier(row[name]) + from .proposal_content_contract import hash_value + hash_value(row['pairing_digest']) + if (digest(canonical(row['ep_request'])) != row['ep_request_digest'] + or row['ep_request']['operation_id'] != key): + raise ValueError('frozen EP request differs') + attempts = row['attempts'] + if not isinstance(attempts, list) or len(attempts) > 16: + raise ValueError('finite explicit dispatch attempts required') + seen = set() + for index, attempt in enumerate(attempts): + if (not isinstance(attempt, dict) or set(attempt) != {'attempt_id', 'claim', 'kind'} + or attempt['kind'] != ('CREATE' if index == 0 else 'RESUME') + or attempt['attempt_id'] in seen): + raise ValueError('append-only original dispatch attempts required') + identifier(attempt['attempt_id']); identifier(attempt['claim']) + seen.add(attempt['attempt_id']) + observation = row['observation'] + if observation is not None and (not isinstance(observation, dict) + or set(observation) != {'claim', 'response', 'response_digest'} + or observation['claim'] not in ({a['claim'] for a in attempts} if attempts else {None}) + or digest(canonical(observation['response'])) != observation['response_digest']): + raise ValueError('bound original observation required') + return value + except (OSError, ValueError, KeyError, TypeError) as error: + raise RuntimeError('initialized Genesis dispatch storage unavailable') from error + + def prepare(self, request, principal, pairing_digest, ep_request): + request, h = apply_request(request) + store = self.store(); key = request['operation_id'] + existing = store['records'].get(key) + proposed = {'request': request, 'request_digest': h, 'principal_id': principal.principal_id, + 'audience': principal.audience, 'grant_id': principal.grant_id, + 'pairing_digest': pairing_digest, 'ep_request': ep_request, + 'ep_request_digest': digest(canonical(ep_request)), 'attempts': [], 'observation': None} + if existing is not None: + if {k: v for k, v in existing.items() if k not in ('attempts', 'observation')} != { + k: v for k, v in proposed.items() if k not in ('attempts', 'observation')}: + raise BootstrapConflict('GENESIS_ORIGINAL_INTENT_CONFLICT') + return load_json(canonical(existing)), False + if any(row['request']['plan_id'] == request['plan_id'] for row in store['records'].values()): + raise BootstrapConflict('GENESIS_PLAN_ALREADY_HAS_ORIGINAL_INTENT') + if len(store['records']) >= 64: + raise BootstrapConflict('GENESIS_INTENT_CAPACITY_EXCEEDED') + store['records'][key] = proposed + self.grant.save_store(store) + return load_json(canonical(proposed)), True + + def claim(self, operation_id, *, attempt_id, resume=False): + identifier(operation_id); identifier(attempt_id) + store = self.store(); row = store['records'][operation_id] + found = next((a for a in row['attempts'] if a['attempt_id'] == attempt_id), None) + if found is not None: + if found['kind'] != ('RESUME' if resume else 'CREATE'): + raise BootstrapConflict('GENESIS_ATTEMPT_KIND_CONFLICT') + return found['claim'], False + if not resume and row['attempts']: + # Even a prior uncertain send is permanently claimed. Never retry POST. + return row['attempts'][0]['claim'], False + if resume and not row['attempts']: + raise BootstrapConflict('GENESIS_ORIGINAL_ADMISSION_REQUIRED') + if len(row['attempts']) >= 16: + raise BootstrapConflict('GENESIS_ATTEMPT_CAPACITY_EXCEEDED') + claim = 'dispatch-'+secrets.token_hex(16) + row['attempts'].append({'attempt_id': attempt_id, 'kind': 'RESUME' if resume else 'CREATE', 'claim': claim}) + self.grant.save_store(store) + return claim, True + + def observe(self, operation_id, claim, response): + store = self.store(); row = store['records'][identifier(operation_id)] + # Caller separately validates the closed producer response and current + # authority. An older in-flight response cannot replace a newer attempt. + if (row['attempts'][-1]['claim'] if row['attempts'] else None) != claim: + raise BootstrapConflict('GENESIS_LATE_RESPONSE_FENCED') + prior = row['observation'] + if prior is not None and 'source_updated_at' in response and 'source_updated_at' in prior['response']: + from .approved_worklist import timestamp + if timestamp(response['source_updated_at']) < timestamp(prior['response']['source_updated_at']): + raise BootstrapConflict('GENESIS_OLDER_OBSERVATION_FENCED') + row['observation'] = {'claim': claim, 'response': response, 'response_digest': digest(canonical(response))} + self.grant.save_store(store) + return load_json(canonical(row)) diff --git a/forge/genesis_apply_pairing.py b/forge/genesis_apply_pairing.py new file mode 100644 index 0000000..aba08dc --- /dev/null +++ b/forge/genesis_apply_pairing.py @@ -0,0 +1,96 @@ +"""Owner-only finite pairing to an independently issued EP provisioning grant.""" +from datetime import UTC, datetime, timedelta +from pathlib import Path +import secrets +from .approved_worklist import identifier +from .ep_inspection_peer import endpoint +from .genesis_bootstrap_contract import finite_expiry +from .project_bootstrap.schema import canonical, load_json, ContractError +from .project_bootstrap.service import digest +from .project_proposal_storage import read, write +from .proposal_content_contract import hash_value +from .workspace_review_grant import _private_bytes + + +def check_pairing(config): + from .genesis_apply_peer import QUALIFIED_PRODUCER + fields = {'pairing_id', 'endpoint', 'binding', 'producer_revision', 'producer_tree', 'producer_schema_sha256', + 'producer_version', 'ep_grant_id', 'expires_at', 'plan_id', 'plan_digest', 'decision_id', 'decision_digest', + 'placement_ref', 'target_ref', 'request_digest', 'input_digest'} + if not isinstance(config, dict) or set(config) != fields: + raise ContractError('closed owner Genesis pairing required') + if QUALIFIED_PRODUCER is None or any(config[k] != v for k, v in QUALIFIED_PRODUCER.items()): + raise ContractError('qualified Genesis producer required') + endpoint(config['endpoint']) + for name in ('pairing_id', 'ep_grant_id', 'plan_id', 'decision_id', 'placement_ref', 'target_ref'): + identifier(config[name]) + for name in ('plan_digest', 'decision_digest', 'request_digest', 'input_digest', 'producer_schema_sha256'): + hash_value(config[name]) + finite_expiry(config['expires_at']) + binding = config['binding'] + if not isinstance(binding, dict) or set(binding) != {'instance_id', 'host_id', 'consumer_id', 'principal', 'operation_id'}: + raise ContractError('closed exact EP authorization headers required') + for name, value in binding.items(): (hash_value if name == 'host_id' else identifier)(value) + return config + + +class GenesisApplyPairings: + def __init__(self, grant): + self.grant = grant + self.path = grant.path.parent/'pairings.json' + + def records(self): + try: + return self._records() + except (OSError, ValueError, KeyError, TypeError) as error: + raise RuntimeError('initialized Genesis pairing storage unavailable') from error + + def _records(self): + value = read(self.path, 131072) + if (not isinstance(value, dict) or set(value) != {'records', 'integrity'} + or not isinstance(value['records'], dict) or len(value['records']) > 16 + or not secrets.compare_digest(value['integrity'], self.grant.integrity(value['records']))): + raise ContractError('sealed finite Genesis pairings required') + for key, row in value['records'].items(): + if not isinstance(row, dict) or set(row) != {'config', 'credential', 'pairing_digest', 'state'}: + raise ContractError('closed private Genesis pairing required') + check_pairing(row['config']) + if (key != row['config']['pairing_id'] or row['state'] not in ('ACTIVE', 'REVOKED') + or not isinstance(row['credential'], str) or not 1 <= len(row['credential']) <= 256 + or any(ord(c) < 33 or ord(c) > 126 for c in row['credential']) + or row['pairing_digest'] != self.grant.integrity({'config':row['config'], 'credential_sha256':digest(row['credential'].encode())})): + raise ContractError('original Genesis pairing binding invalid') + return value['records'] + + def save(self, records, *, create=False): + write(self.path, {'records':records, 'integrity':self.grant.integrity(records)}, create=create) + + def bind(self, config, credential_file): + config = check_pairing(load_json(canonical(config))) + if not datetime.now(UTC) < finite_expiry(config['expires_at']) <= datetime.now(UTC)+timedelta(days=1): + raise ContractError('finite existing EP grant expiry required') + credential = _private_bytes(Path(credential_file)).decode('ascii').strip() + if not 1 <= len(credential) <= 256 or any(ord(c) < 33 or ord(c) > 126 for c in credential): + raise ContractError('separate private EP provisioning credential required') + row = {'config':config, 'credential':credential, 'state':'ACTIVE', + 'pairing_digest':self.grant.integrity({'config':config, 'credential_sha256':digest(credential.encode())})} + with self.grant.boundary(): + self.grant._store(); records = self.records() + if config['pairing_id'] in records or len(records) >= 16: + raise ContractError('Genesis pairing immutable or capacity exhausted') + records[config['pairing_id']] = row; self.save(records) + return {'pairing_id':config['pairing_id'], 'pairing_digest':row['pairing_digest'], 'state':'OWNER_CONFIGURED_NOT_EP_ADMITTED'} + + def pair(self, pairing_id, expected_digest=None): + row = self.records().get(identifier(pairing_id)) + if row is None or row['state'] != 'ACTIVE' or finite_expiry(row['config']['expires_at']) <= datetime.now(UTC): + raise PermissionError('Genesis peer grant inactive') + if expected_digest is not None and not secrets.compare_digest(row['pairing_digest'], expected_digest): + raise PermissionError('original Genesis pairing changed') + return {**row['config'], 'credential':row['credential'], 'pairing_digest':row['pairing_digest']} + + def revoke(self, pairing_id): + with self.grant.boundary(): + records = self.records(); row = records[identifier(pairing_id)] + row['state'] = 'REVOKED'; self.save(records) + return {'pairing_id':pairing_id, 'state':'REVOKED'} diff --git a/forge/genesis_apply_peer.py b/forge/genesis_apply_peer.py new file mode 100644 index 0000000..a0c417e --- /dev/null +++ b/forge/genesis_apply_peer.py @@ -0,0 +1,111 @@ +"""Finite Genesis HTTP transport; no proxy, redirects, retries or grant issuance.""" +import http.client +from importlib.resources import files +import socket +from threading import Timer +import time +from .approved_worklist import identifier, timestamp +from .ep_inspection_contract import _schema +from .ep_inspection_peer import endpoint, PeerError +from .project_bootstrap.schema import canonical, load_json, ContractError +from .project_bootstrap.service import digest + +MAX_RESPONSE = 524288 +# Filled only from the separately qualified L2 exact-final-main handoff. +# A merged candidate and its local tests do not qualify a producer. +QUALIFIED_PRODUCER = {'producer_revision': '4d0c0461d7a5ec7fb7a50aa42f775cc4be09faa5', 'producer_tree': 'b095ba006beccd79171115a7deab55ef7b7c4ff9', 'producer_schema_sha256': '3ccf4d3ba775c9f4503e986fce5416b33fe1d125d0b18244beeae562868aa231', 'producer_version': '2.3.123'} + + +def receipt(raw, pairing, request, documents): + if not isinstance(raw, bytes) or len(raw) > MAX_RESPONSE: + raise ContractError('finite original Genesis receipt required') + schema_raw = files('forge').joinpath('api/ep-genesis-local-bootstrap-v1.schema.json').read_bytes() + if digest(schema_raw) != pairing['producer_schema_sha256']: + raise ContractError('qualified Genesis schema differs') + schema = load_json(schema_raw)['oneOf'][0] + value = load_json(raw) + _schema(value, schema) + def extra_bounds(item, rule): + if type(item) is int and item > rule.get('maximum', item): + raise ContractError('Genesis integer maximum exceeded') + if type(item) is list: + if rule.get('uniqueItems') and len({canonical(v) for v in item}) != len(item): + raise ContractError('Genesis duplicate array item') + for child in item: extra_bounds(child, rule['items']) + elif type(item) is dict: + for key, child in item.items(): extra_bounds(child, rule['properties'][key]) + extra_bounds(value, schema) + binding = pairing['binding'] + expected = {'operation_id': request['operation_id'], 'request_digest': digest(canonical(request)), + 'effect_plan_binding': request['effect_plan_binding'], 'target_ref': request['target_ref'], + 'authorization_ref': pairing['ep_grant_id'], 'instance_id': binding['instance_id'], 'host_id': binding['host_id'], + 'bundle_digest': documents['ep_bundle_binding']['canonical_digest'], + 'manifest_digest': documents['manifest_digest'], 'input_digest': pairing['input_digest'], + 'producer_version': pairing['producer_version']} + if any(value[key] != item for key, item in expected.items()): + raise ContractError('original Genesis operation binding differs') + if {k: v for k, v in value['identity'].items() if k != 'state'} != documents['identity']: + raise ContractError('original Genesis identity differs') + for name in ('source_updated_at', 'observed_at'): + timestamp(value[name]) + if value['validator_sha256'] not in (None, documents['validation']['validator_sha256']): + raise ContractError('original Genesis validator differs') + if value['validation'] is not None and value['validation']['manifest_digest'] != documents['manifest_digest']: + raise ContractError('original Genesis validation manifest differs') + if value['state'] == 'COMPLETE' and (value['identity']['state'] != 'REGISTERED' or not value['commit'] or not value['tree'] + or value['validation'] is None or value['validator_sha256'] is None + or set(value['completed_steps']) != set(schema['properties']['completed_steps']['items']['enum'])): + raise ContractError('complete Genesis lacks committed registration evidence') + return value + + +def exchange(pairing, method, *, body=None, resume=False, deadline=None, before_send=None): + """One exact request. Lost responses remain uncertain; callers recover by GET.""" + if method == 'POST' and before_send is None: + raise ContractError('current Forge authority callback required before effect send') + if method not in ('GET', 'POST') or (method == 'GET' and (body is not None or resume)): + raise ContractError('finite explicit Genesis transport required') + host, port = endpoint(pairing['endpoint']); binding = pairing['binding'] + operation = identifier(binding['operation_id']) + if method == 'POST' and not resume and (body is None or body['operation_id'] != operation + or digest(canonical(body)) != pairing['request_digest']): + raise ContractError('exact owner-bound Genesis request required') + if resume and body is not None: + raise ContractError('resume cannot replace the original request') + timeout = min(30, deadline-time.monotonic()) if deadline is not None else 30 + if timeout <= 0: raise PeerError('EP_GENESIS_DEADLINE_EXCEEDED') + headers = {'Authorization': 'Bearer '+pairing['credential'], 'EP-Bootstrap-Contract': '1.0', + 'EP-Instance-ID': binding['instance_id'], 'EP-Host-ID': binding['host_id'], + 'EP-Consumer-ID': binding['consumer_id'], 'EP-Principal': binding['principal'], 'EP-Audience': 'genesis-bootstrap'} + path = '/v1/bootstrap/genesis/operations' + if method == 'GET' or resume: path += '/'+operation+('/resume' if resume else '') + # Owning EP resume explicitly forbids a request body (Content-Length0). + payload = canonical(body) if body is not None else None + if payload is not None: headers['Content-Type'] = 'application/json' + connection = http.client.HTTPConnection(host, port, timeout=timeout) + hard_deadline = time.monotonic()+timeout; watchdog = None + try: + connection.connect(); peer_socket = connection.sock + def interrupt(): + try: peer_socket.shutdown(socket.SHUT_RDWR) + except OSError: pass + peer_socket.close() + watchdog = Timer(max(0, hard_deadline-time.monotonic()), interrupt); watchdog.daemon = True; watchdog.start() + if time.monotonic() >= hard_deadline: raise PeerError('EP_GENESIS_DEADLINE_EXCEEDED') + if before_send is not None: before_send(hard_deadline) + if time.monotonic() >= hard_deadline: raise PeerError('EP_GENESIS_DEADLINE_EXCEEDED') + connection.request(method, path, body=payload, headers=headers) + response = connection.getresponse() + if response.status != 200: + raise PeerError('EP_GENESIS_HTTP_'+str(response.status), 424) + raw = response.read(MAX_RESPONSE+1) + if len(raw) > MAX_RESPONSE or response.read(1): raise PeerError('EP_GENESIS_RESPONSE_LIMIT', 424) + if time.monotonic() >= hard_deadline: raise PeerError('EP_GENESIS_DEADLINE_EXCEEDED') + return raw + except PermissionError: + raise + except (OSError, http.client.HTTPException) as error: + raise PeerError('EP_GENESIS_TRANSPORT_UNCERTAIN') from error + finally: + if watchdog is not None: watchdog.cancel(); watchdog.join(0.2) + connection.close() diff --git a/forge/genesis_apply_service.py b/forge/genesis_apply_service.py new file mode 100644 index 0000000..86addf3 --- /dev/null +++ b/forge/genesis_apply_service.py @@ -0,0 +1,204 @@ +"""Explicit effect coordination over original owning Forge and EP HTTP services.""" +from contextlib import contextmanager +import sqlite3 +from urllib.parse import urlsplit, parse_qs +from .approved_worklist import identifier +from .ep_inspection_peer import PeerError +from .genesis_apply_contract import CONTRACT, apply_request, ep_request +from .genesis_apply_journal import GenesisDispatchJournal +from .genesis_apply_pairing import GenesisApplyPairings +from .genesis_apply_peer import exchange, receipt +from .genesis_bootstrap_contract import BootstrapConflict +from .genesis_bootstrap_grant import GenesisBootstrapGrant +from .genesis_bootstrap_service import GenesisBootstrapService +from .project_bootstrap.schema import canonical, load_json, ContractError +from .project_bootstrap.service import digest + +BASE = '/v1/genesis-apply' + + +class GenesisApplyService: + def __init__(self, grant): + self.grant = grant + self.journal = GenesisDispatchJournal(grant) + self.pairings = GenesisApplyPairings(grant) + self.bootstrap = GenesisBootstrapService(GenesisBootstrapGrant(grant.root, grant.instance_id)) + + def source(self, principal, request, *, effect): + if request['instance_id'] != self.grant.instance_id or request['dossier_id'] not in principal.dossier_ids: + raise PermissionError('foreign Genesis apply instance or dossier') + view = self.bootstrap.view(principal, self.bootstrap.store(), request['plan_id']) + plan = view['plan'] + decisions = [d for d in view['effect_history'] if d['decision_id'] == request['decision_id']] + if (plan['plan_digest'] != request['plan_digest'] or len(decisions) != 1 + or decisions[0]['decision_digest'] != request['decision_digest']): + raise BootstrapConflict('GENESIS_EXACT_ORIGINAL_DECISION_REQUIRED') + if effect and (not view['effect_decision_currently_applicable'] or view['effect_history'][-1] != decisions[0]): + raise BootstrapConflict('GENESIS_CURRENT_EFFECT_DECISION_REQUIRED') + original, _, _ = self.bootstrap.source(principal, request['dossier_id'], plan['source_subject']['plan_id']) + request_ep, _ = ep_request(original, plan, decisions[0], request['operation_id']) + return view, request_ep + + def current(self, authorization, audience, request, permission, expected=None): + principal = self.grant.authorize(authorization, audience=audience, dossier_id=request['dossier_id'], permission=permission) + view, request_ep = self.source(principal, request, effect=permission != 'READ') + if permission != 'READ': + self.grant.authorize(authorization, audience=audience, dossier_id=request['dossier_id'], permission=permission, plan=view['plan']) + pairing = self.pairings.pair(request['pairing_id'], expected['pairing_digest'] if expected else None) + plan = view['plan'] + expected_pair = {'plan_id':plan['plan_id'], 'plan_digest':plan['plan_digest'], 'decision_id':request['decision_id'], + 'decision_digest':request['decision_digest'], 'placement_ref':plan['target']['placement_ref'], + 'target_ref':plan['target']['target_ref'], 'request_digest':digest(canonical(request_ep)), + 'input_digest':plan['source_subject']['input_digest']} + if (any(pairing[k] != v for k, v in expected_pair.items()) + or pairing['binding']['instance_id'] != plan['target']['ep_instance_id'] + or pairing['binding']['operation_id'] != request['operation_id']): + raise PermissionError('original EP grant/plan binding differs') + if expected is not None and permission != 'READ' and principal.grant_id != expected['grant_id']: + raise PermissionError('original Genesis effect grant required') + if expected is not None and (principal.principal_id != expected['principal_id'] or principal.audience != expected['audience'] + or request != expected['request'] or request_ep != expected['ep_request']): + raise PermissionError('original Genesis intent scope differs') + return principal, view, pairing, request_ep + + @staticmethod + def response(principal, row, view): + observed = row['observation']['response'] if row['observation'] else None + return {'contract_version':CONTRACT, 'instance_id':principal.instance_id, 'principal_id':principal.principal_id, + 'audience':principal.audience, 'dossier_id':row['request']['dossier_id'], 'operation_id':row['request']['operation_id'], + 'original_request':row['request'], 'original_request_digest':row['request_digest'], + 'ep_request_digest':row['ep_request_digest'], 'pairing_digest':row['pairing_digest'], + 'effect_decision_currently_applicable':view['effect_decision_currently_applicable'], + 'state':observed['state'] if observed else ('EFFECT_UNCERTAIN' if row['attempts'] else 'NOT_REQUESTED'), + 'original_ep_operation':observed, 'explicit_attempts':len(row['attempts']), + 'project_readiness':'NOT_ASSESSED', 'mission':'NOT_REQUESTED'} + + def _observe(self, authorization, audience, request, original, pairing, request_ep, raw, claim): + documents = self._documents(request) + observed = receipt(raw, pairing, request_ep, documents) + with self.grant.boundary(): + principal, view, _, _ = self.current(authorization, audience, request, 'READ', original) + row = self.journal.observe(request['operation_id'], claim, observed) + return self.response(principal, row, view) + + def _documents(self, request): + # Always sealed owning storage; never caller-provided approved documents. + row = self.bootstrap.store()['records'].get(request['plan_id']) + if row is None or row['kind'] != 'PLAN': raise RuntimeError('owning original plan unavailable') + return row['document']['documents'] + + def send(self, authorization, audience, request, original, pairing, method, *, permission='READ', claim=None, body=None, resume=False): + def current_before_send(deadline): + with self.grant.boundary(deadline): + self.current(authorization, audience, request, permission, original) + row = self.journal.store()['records'][request['operation_id']] + if claim is not None and (not row['attempts'] or row['attempts'][-1]['claim'] != claim): + raise BootstrapConflict('GENESIS_LATE_SEND_FENCED') + return exchange(pairing, method, body=body, resume=resume, before_send=current_before_send) + + def submit(self, authorization, audience, value): + request, _ = apply_request(value) + with self.grant.boundary(): + principal, view, pairing, request_ep = self.current(authorization, audience, request, 'REQUEST_APPLY') + original, _ = self.journal.prepare(request, principal, pairing['pairing_digest'], request_ep) + # Verify current peer authority and first recover the original operation. + # No own grant or SQLite lock spans this wait. + try: + raw = self.send(authorization, audience, request, original, pairing, 'GET') + except PeerError as error: + if error.code != 'EP_GENESIS_HTTP_404': raise + if original['attempts']: + with self.grant.boundary(): + principal, view, _, _ = self.current(authorization, audience, request, 'READ', original) + row = self.journal.store()['records'][request['operation_id']] + # Missing peer evidence must not publish an old COMPLETE. + if row['observation'] is not None: raise PeerError('EP_GENESIS_ORIGINAL_OPERATION_UNAVAILABLE', 424) + return self.response(principal, row, view) + with self.grant.boundary(): + self.current(authorization, audience, request, 'REQUEST_APPLY', original) + claim, send = self.journal.claim(request['operation_id'], attempt_id=request['operation_id']) + if not send: return self.read(authorization, audience, request['dossier_id'], request['operation_id']) + raw = self.send(authorization, audience, request, original, pairing, 'POST', permission='REQUEST_APPLY', claim=claim, body=request_ep) + else: + claim = original['attempts'][-1]['claim'] if original['attempts'] else None + return self._observe(authorization, audience, request, original, pairing, request_ep, raw, claim) + + def original(self, authorization, audience, dossier, operation): + identifier(dossier); identifier(operation) + principal = self.grant.authorize(authorization, audience=audience, dossier_id=dossier) + row = self.journal.store()['records'].get(operation) + if row is None: raise FileNotFoundError('original Genesis intent unavailable') + if row['request']['dossier_id'] != dossier or row['principal_id'] != principal.principal_id or row['audience'] != principal.audience: + raise PermissionError('foreign original Genesis intent') + return row + + def read(self, authorization, audience, dossier, operation): + with self.grant.boundary(): + original = self.original(authorization, audience, dossier, operation) + _, _, pairing, request_ep = self.current(authorization, audience, original['request'], 'READ', original) + raw = self.send(authorization, audience, original['request'], original, pairing, 'GET') + claim = original['attempts'][-1]['claim'] if original['attempts'] else None + return self._observe(authorization, audience, original['request'], original, pairing, request_ep, raw, claim) + + def resume(self, authorization, audience, operation, value): + fields = {'contract_version', 'instance_id', 'dossier_id', 'operation_id', 'attempt_id', 'original_request_digest'} + if not isinstance(value, dict) or set(value) != fields or value['contract_version'] != CONTRACT or value['operation_id'] != operation or value['instance_id'] != self.grant.instance_id: + raise ContractError('closed original explicit Genesis resume required') + identifier(value['attempt_id']) + with self.grant.boundary(): + original = self.original(authorization, audience, value['dossier_id'], operation) + if original['request_digest'] != value['original_request_digest']: + raise BootstrapConflict('GENESIS_ORIGINAL_INTENT_CONFLICT') + _, view, pairing, request_ep = self.current(authorization, audience, original['request'], 'RESUME', original) + # Explicit resume still requires a currently accessible original admission. + raw = self.send(authorization, audience, original['request'], original, pairing, 'GET') + observed = receipt(raw, pairing, request_ep, view['plan']['documents']) + if observed['state'] == 'COMPLETE': + return self.read(authorization, audience, value['dossier_id'], operation) + with self.grant.boundary(): + self.current(authorization, audience, original['request'], 'RESUME', original) + claim, send = self.journal.claim(operation, attempt_id=value['attempt_id'], resume=True) + if send: raw = self.send(authorization, audience, original['request'], original, pairing, 'POST', permission='RESUME', claim=claim, resume=True) + return self._observe(authorization, audience, original['request'], original, pairing, request_ep, raw, claim) + + def handle(self, method, target, authorization, audience, body=None): + try: + if not isinstance(audience, str): raise PermissionError('explicit consumer audience required') + parsed = urlsplit(target); path = parsed.path + if method == 'POST' and path == BASE+'/operations' and not parsed.query: + return 200, self.submit(authorization, audience, body) + pieces = path.removeprefix(BASE+'/operations/').split('/') + if path.startswith(BASE+'/operations/') and method == 'GET' and len(pieces) == 1: + query = parse_qs(parsed.query, strict_parsing=True) + if set(query) != {'dossier_id'} or len(query['dossier_id']) != 1: raise ContractError('exact dossier query required') + return 200, self.read(authorization, audience, query['dossier_id'][0], pieces[0]) + if path.startswith(BASE+'/operations/') and method == 'POST' and len(pieces) == 2 and pieces[1] == 'resume' and not parsed.query: + return 200, self.resume(authorization, audience, pieces[0], body) + raise ContractError('unsupported Genesis apply route') + except PermissionError: status, code = 403, 'GENESIS_APPLY_SCOPE_DENIED' + except BootstrapConflict as error: status, code = 409, error.code + except FileNotFoundError: status, code = 503, 'GENESIS_APPLY_ORIGINAL_UNAVAILABLE' + except PeerError as error: status, code = error.status, error.code + except (RuntimeError, OSError, sqlite3.Error): status, code = 503, 'GENESIS_APPLY_STORAGE_UNAVAILABLE' + except (ValueError, KeyError, TypeError, RecursionError): status, code = 400, 'GENESIS_APPLY_INVALID' + return status, {'contract_version':CONTRACT, 'error':{'code':code}} + + +@contextmanager +def publication(grant, authorization, audience, body, *, deadline=None): + service = GenesisApplyService(grant) + with grant.boundary(deadline): + original = service.original(authorization, audience, body['dossier_id'], body['operation_id']) + _, _, pairing, request_ep = service.current(authorization, audience, original['request'], 'READ', original) + # Recheck actual EP grant before each protected chunk, without own locks. + try: + raw = exchange(pairing, 'GET', deadline=deadline) + receipt(raw, pairing, request_ep, service._documents(original['request'])) + except (PeerError, ValueError, KeyError, TypeError) as error: + raise PermissionError('current original Genesis peer publication denied') from error + with grant.boundary(deadline): + principal, view, _, _ = service.current(authorization, audience, original['request'], 'READ', original) + now = service.journal.store()['records'][body['operation_id']] + if body != service.response(principal, now, view): + raise PermissionError('Genesis publication scope/epoch changed') + yield diff --git a/forge/server_runtime.py b/forge/server_runtime.py index c446268..bb310ce 100644 --- a/forge/server_runtime.py +++ b/forge/server_runtime.py @@ -72,6 +72,8 @@ from .concept_advisory_grant import ConceptAdvisoryGrant from .concept_advisory_service import ConceptAdvisoryService from .project_proposal_grant import ProjectProposalGrant +from .genesis_apply_grant import GenesisApplyGrant +from .genesis_apply_service import GenesisApplyService from .genesis_bootstrap_grant import GenesisBootstrapGrant from .genesis_bootstrap_service import GenesisBootstrapService from .proposal_content_grant import ProposalContentGrant @@ -109,6 +111,9 @@ ("POST", "/v1/inspection-adoptions/plans"), ("GET", "/v1/inspection-adoptions/plans/{plan_id}"), ("GET", "/v1/inspection-adoptions/intents/{operation_id}"), + ("POST", "/v1/genesis-apply/operations"), + ("GET", "/v1/genesis-apply/operations/{operation_id}"), + ("POST", "/v1/genesis-apply/operations/{operation_id}/resume"), ("GET", "/v1/genesis-bootstrap/capability"), ("POST", "/v1/genesis-bootstrap/plans"), ("POST", "/v1/genesis-bootstrap/decisions"), @@ -822,6 +827,7 @@ def __init__(self, services: ForgeServerApplicationServices, bearer_credential: self.worklist_control_grant = worklist_control_grant self.proposal_grant = ProjectProposalGrant(services.root, instance.instance_id) self.concept_grant = ConceptAdvisoryGrant(services.root, instance.instance_id) + self.apply_grant = GenesisApplyGrant(services.root, instance.instance_id) self.bootstrap_grant = GenesisBootstrapGrant(services.root, instance.instance_id) self.content_grant = ProposalContentGrant(services.root, instance.instance_id) self.inspection_grant = InspectionAdoptionGrant(services.root, instance.instance_id) @@ -852,6 +858,8 @@ def _authenticated(self, authorization: str | None) -> bool: def _authentication_kind(self, authorization: str | None) -> str | None: if self._authenticated(authorization): return "ADMIN" + if self.apply_grant.authenticate(authorization): + return "GENESIS_APPLY" if self.bootstrap_grant.authenticate(authorization): return "GENESIS_BOOTSTRAP" if self.inspection_grant.authenticate(authorization): @@ -1020,6 +1028,13 @@ def handle(self, method: str, target: str, authorization: str | None, return APIResponse(400, {"api_version": SERVER_API_VERSION, "error": { "code": "REQUEST_INVALID", "message": "Request target must be origin-form", }}, headers) + if path == '/v1/genesis-apply' or path.startswith('/v1/genesis-apply/'): + if kind != 'GENESIS_APPLY': + return APIResponse(403, {'contract_version':'forge-genesis-apply/v1','error':{'code':'GENESIS_APPLY_SCOPE_DENIED'}}, headers) + status, document = GenesisApplyService(self.apply_grant).handle(method,target,authorization,proposal_audience,body) + return APIResponse(status,document,headers) + if kind == 'GENESIS_APPLY': + return APIResponse(403, {'contract_version':'forge-genesis-apply/v1','error':{'code':'GENESIS_APPLY_SCOPE_DENIED'}}, headers) if path == '/v1/genesis-bootstrap' or path.startswith('/v1/genesis-bootstrap/'): if kind != 'GENESIS_BOOTSTRAP': return APIResponse(403, {'contract_version':'forge-genesis-bootstrap-effect-plan/v1','error':{'code':'GENESIS_SCOPE_DENIED'}}, headers) @@ -1307,7 +1322,7 @@ def _body(self) -> Mapping[str, Any] | None: length = int(length_text) except ValueError as error: raise ValueError("request Content-Length is invalid") from error - limit = 65536 if self.path.startswith('/v1/genesis-bootstrap/') else 262144 if self.path.startswith('/v1/inspection-adoptions/') else 65536 if self.path.startswith('/v1/project-proposal-decisions/') else 32768 if self.path.startswith('/v1/concept-advisory/') else 262144 if self.path.startswith('/v1/onboarding/') else _MAX_BODY + limit = 32768 if self.path.startswith('/v1/genesis-apply/') else 65536 if self.path.startswith('/v1/genesis-bootstrap/') else 262144 if self.path.startswith('/v1/inspection-adoptions/') else 65536 if self.path.startswith('/v1/project-proposal-decisions/') else 32768 if self.path.startswith('/v1/concept-advisory/') else 262144 if self.path.startswith('/v1/onboarding/') else _MAX_BODY if length < 0 or length > limit: raise ValueError("request body exceeds Forge Server limit") raw = self.rfile.read(length) @@ -1329,7 +1344,7 @@ def _dispatch(self) -> None: if len(authorizations) > 1: raise ValueError("request Authorization is ambiguous") authorization = authorizations[0] if authorizations else None - audience_values = self.headers.get_all("X-Forge-Bootstrap-Audience", []) + self.headers.get_all("X-Forge-Proposal-Audience", []) + self.headers.get_all("X-Forge-Concept-Audience", []) + self.headers.get_all("X-Forge-Content-Audience", []) + self.headers.get_all("X-Forge-Inspection-Audience", []) + audience_values = self.headers.get_all("X-Forge-Genesis-Apply-Audience", []) + self.headers.get_all("X-Forge-Bootstrap-Audience", []) + self.headers.get_all("X-Forge-Proposal-Audience", []) + self.headers.get_all("X-Forge-Concept-Audience", []) + self.headers.get_all("X-Forge-Content-Audience", []) + self.headers.get_all("X-Forge-Inspection-Audience", []) if len(audience_values) > 1: raise ValueError("ambiguous proposal audience") proposal_audience = audience_values[0] if audience_values else None kind = api._authentication_kind(authorization) @@ -1343,20 +1358,20 @@ def _dispatch(self) -> None: review_mission = api._review_mission_for_decision( self.command, path, authorization, ) if kind == "WORKSPACE_REVIEW" else None - if (path.startswith("/v1/genesis-bootstrap/") or path.startswith("/v1/inspection-adoptions/") or path.startswith("/v1/project-proposal-decisions/") or path.startswith("/v1/onboarding/") or path.startswith("/v1/concept-advisory/")) and (self.headers.get_all("X-Actor") or self.headers.get_all("X-Viewer")) or ((path.startswith("/v1/genesis-bootstrap/") or path.startswith("/v1/project-proposal-decisions/") or path.startswith("/v1/inspection-adoptions/")) and self.headers.get_all("X-Role")): + if (path.startswith("/v1/genesis-apply/") or path.startswith("/v1/genesis-bootstrap/") or path.startswith("/v1/inspection-adoptions/") or path.startswith("/v1/project-proposal-decisions/") or path.startswith("/v1/onboarding/") or path.startswith("/v1/concept-advisory/")) and (self.headers.get_all("X-Actor") or self.headers.get_all("X-Viewer")) or ((path.startswith("/v1/genesis-apply/") or path.startswith("/v1/genesis-bootstrap/") or path.startswith("/v1/project-proposal-decisions/") or path.startswith("/v1/inspection-adoptions/")) and self.headers.get_all("X-Role")): raise ValueError("untrusted actor header unsupported") - if (path.startswith("/v1/genesis-bootstrap/") or path.startswith("/v1/inspection-adoptions/") or path.startswith("/v1/project-proposal-decisions/") or path.startswith("/v1/onboarding/") or path.startswith("/v1/concept-advisory/")) and self.command == "GET" and self.headers.get("Content-Length", "0") != "0": + if (path.startswith("/v1/genesis-apply/") or path.startswith("/v1/genesis-bootstrap/") or path.startswith("/v1/inspection-adoptions/") or path.startswith("/v1/project-proposal-decisions/") or path.startswith("/v1/onboarding/") or path.startswith("/v1/concept-advisory/")) and self.command == "GET" and self.headers.get("Content-Length", "0") != "0": raise ValueError("proposal GET body forbidden") - body = self._body() if kind in {"GENESIS_BOOTSTRAP", "EP_INSPECTION_ADOPTION", "PROPOSAL_CONTENT_DECISION", "CONCEPT_ADVISORY", "PROJECT_PROPOSAL", "ADMIN", "WORKSPACE_WORKLIST_CONTROL", "ADVISORY", "ADVISORY_CANDIDATE", "CANDIDATE_DECISION", "WORKSET_RELEASE", "MISSION_FINAL_ACCEPTANCE", "WORKSET_ORDER"} or review_mission is not None else None + body = self._body() if kind in {"GENESIS_APPLY", "GENESIS_BOOTSTRAP", "EP_INSPECTION_ADOPTION", "PROPOSAL_CONTENT_DECISION", "CONCEPT_ADVISORY", "PROJECT_PROPOSAL", "ADMIN", "WORKSPACE_WORKLIST_CONTROL", "ADVISORY", "ADVISORY_CANDIDATE", "CANDIDATE_DECISION", "WORKSET_RELEASE", "MISSION_FINAL_ACCEPTANCE", "WORKSET_ORDER"} or review_mission is not None else None response = api.handle(self.command, target, authorization, body, proposal_audience=proposal_audience) except (OSError, UnicodeDecodeError, ValueError, json.JSONDecodeError) as error: response = APIResponse(400, {"api_version": SERVER_API_VERSION, "error": { "code": "REQUEST_INVALID", "message": "HTTP request was rejected", }}, ForgeServerAPI._headers()) payload = json.dumps(response.body, sort_keys=True, separators=(",", ":")).encode("utf-8") - if (kind in ('GENESIS_BOOTSTRAP','PROPOSAL_CONTENT_DECISION','PROJECT_PROPOSAL','CONCEPT_ADVISORY') and response.status == 200) or (kind == 'EP_INSPECTION_ADOPTION' and response.status in (200,202)): + if (kind in ('GENESIS_APPLY','GENESIS_BOOTSTRAP','PROPOSAL_CONTENT_DECISION','PROJECT_PROPOSAL','CONCEPT_ADVISORY') and response.status == 200) or (kind == 'EP_INSPECTION_ADOPTION' and response.status in (200,202)): from .project_proposal_publication import publish - publish(self,api.bootstrap_grant if kind == "GENESIS_BOOTSTRAP" else api.inspection_grant if kind == "EP_INSPECTION_ADOPTION" else api.content_grant if kind == "PROPOSAL_CONTENT_DECISION" else (api.concept_grant if kind == "CONCEPT_ADVISORY" else api.proposal_grant),authorization,proposal_audience,response,payload) + publish(self,api.apply_grant if kind == "GENESIS_APPLY" else api.bootstrap_grant if kind == "GENESIS_BOOTSTRAP" else api.inspection_grant if kind == "EP_INSPECTION_ADOPTION" else api.content_grant if kind == "PROPOSAL_CONTENT_DECISION" else (api.concept_grant if kind == "CONCEPT_ADVISORY" else api.proposal_grant),authorization,proposal_audience,response,payload) return self.send_response(response.status) for name, value in response.headers.items(): diff --git a/product-version.json b/product-version.json index a405f93..0e58f89 100644 --- a/product-version.json +++ b/product-version.json @@ -1,5 +1,5 @@ { "product": "forge", "schema_version": 1, - "version": "2.22.0" + "version": "2.23.0" } diff --git a/pyproject.toml b/pyproject.toml index a6bfaeb..4ef45eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ Issues = "https://github.com/pcvantol/forge/issues" [project.scripts] forge-inspection-adoption = "forge.ep_inspection_cli:main" +forge-genesis-apply = "forge.genesis_apply_cli:main" forge-genesis-bootstrap = "forge.genesis_bootstrap_cli:main" forge-project-proposal-decisions = "forge.proposal_content_cli:main" forge-concept-advisory = "forge.concept_advisory_cli:main" diff --git a/scripts/qualification/genesis_apply_matrix.py b/scripts/qualification/genesis_apply_matrix.py new file mode 100644 index 0000000..42ce97b --- /dev/null +++ b/scripts/qualification/genesis_apply_matrix.py @@ -0,0 +1,363 @@ +"""Extra qualified-peer HTTP probes; explicit synthetic owner fixtures only.""" +from concurrent.futures import ThreadPoolExecutor +from copy import deepcopy +from datetime import UTC, datetime +from hashlib import sha256 +import http.client +import json +from pathlib import Path + + +def peer(Q, c, label, method='GET', body=None, headers=None, token=None): + p=c['config'];h={'Authorization':'Bearer '+(token or c['ep_token'].read_text().strip()), + 'EP-Bootstrap-Contract':'1.0','EP-Instance-ID':p['binding']['instance_id'], + 'EP-Host-ID':p['binding']['host_id'],'EP-Consumer-ID':p['binding']['consumer_id'], + 'EP-Principal':p['binding']['principal'],'EP-Audience':'genesis-bootstrap'} + h.update(headers or {}) + path='/v1/bootstrap/genesis/operations'+('/original-apply' if method=='GET' else '') + raw=Q.canonical(body) if body is not None else None + if raw is not None:h['Content-Type']='application/json' + connection=http.client.HTTPConnection('127.0.0.1',c['ep'].port,timeout=35) + try: + connection.request(method,path,body=raw,headers=h);response=connection.getresponse() + value=response.read(524289) + if len(value)>524288:raise RuntimeError('finite negative response required') + result=json.loads(value) + Q.private(c['output']/(label+'.ep-http.private.json'),dict(status=response.status,response=result)) + return response.status,result + finally:connection.close() + + +def prechecks(Q,c): + """Actual authority/header/body refusals before the sole real admission.""" + ep=c['ep'];baseline=ep.facts();labels=[] + c['command'](['-m','forge.genesis_apply_cli',*c['data'],'issue','--principal-id',c['setup']['human_principal_id'], + *c['scope'],'--permission','READ','--token-file',str(c['forge']/'read.private')],c['forge']) + status,_=Q.retained_call(c['output'],'real-read-grant-cannot-apply',c['port'],c['forge']/'read.private', + 'POST','/v1/genesis-apply/operations',c['body']) + if status!=403:raise RuntimeError('real READ grant promoted into apply authority') + for key in ('EP-Instance-ID','EP-Host-ID','EP-Consumer-ID','EP-Principal','EP-Audience'): + status,_=peer(Q,c,'wrong-'+key,headers={key:'foreign'}) + if status not in (401,403):raise RuntimeError('wrong actual EP binding not denied') + labels.append(key) + for key in ('target','actor','bundle','manifest','validator','proposed-identity'): + value=deepcopy(c['ep_request']) + if key=='target':value['target_ref']='foreign' + elif key=='actor':value['effect_plan_binding']['source_actor_id']='foreign' + else: + name='engineering/bootstrap-manifest.json' if key=='manifest' else 'engineering/baselines/portable-docs/1.0.0/validate.py' if key=='validator' else '.engineering-platform/repository.json' if key=='proposed-identity' else 'README.md' + item=value['bundle']['files'][name] + if key in ('manifest','proposed-identity'): + document=json.loads(item['utf8']) + if key=='manifest':document['manifest_digest']='f'*64 + else:document['repository']['id']='foreign-core' + item['utf8']=Q.canonical(document).decode() + else:item['utf8']+='\n# changed synthetic negative\n' + item['sha256']=sha256(item['utf8'].encode()).hexdigest() + status,_=peer(Q,c,'wrong-original-'+key,'POST',value) + if status not in (400,403,409,422):raise RuntimeError('changed original EP request accepted') + labels.append(key) + # Issue a genuine separate INSPECT_READ bearer; never run an inspection. + ep.command('preproject_cli','init') + host=ep.root/'host';(host/'.engineering').mkdir(parents=True,mode=0o700) + Q.private(host/'.engineering/engineering-platform.local.json',{'workspace':{'workspace_authorization':{ + 'allowed_roots':[{'path':str(ep.workspace),'repository_scope':'direct_children'}], + 'allowed_repositories':[],'denied_repositories':[],'symlink_policy':'reject','case_sensitivity':'host'}}}) + binding=ep.root/'inspection-binding.private.json' + Q.private(binding,dict(source_ref='r57-read-only-source',host_root=str(host),workspace_root=str(ep.workspace),child='r')) + ep.command('preproject_cli','bind-source','--config',str(binding)) + config=ep.root/'inspection-grant.private.json' + Q.private(config,dict(grant_id='r57-inspect-only',source_ref='r57-read-only-source',consumer_id='forge-r57', + principal='synthetic-r57-owner',audience='genesis-bootstrap',paths=['README.md'],content_paths=[],ttl_seconds=3600,max_operations=1)) + old=ep.command('preproject_cli','issue','--config',str(config)) + status,_=peer(Q,c,'real-inspection-grant-cannot-apply','POST',c['ep_request'],token=old['credential']) + if status!=401:raise RuntimeError('real inspection bearer promoted into Genesis authority') + if ep.facts()!=baseline:raise RuntimeError('pre-admission negative performed an EP operation') + return {'actual_read_grant_refused':True,'actual_inspection_grant_refused':True,'binding_refusals':labels,'no_admission':True} + + +def availability_and_progress(Q,c,operation): + """Original reads cannot launder unavailable peers or other-purpose bearers.""" + ep=c['ep'];baseline=ep.facts();path='/v1/genesis-apply/operations/original-apply?dossier_id=synthetic-dossier' + ep.stop() + status,_=Q.retained_call(c['output'],'unavailable-peer-not-cached-complete',c['port'],c['forge']/'apply.private','GET',path) + if status!=503:raise RuntimeError('unavailable peer returned a healthy cached result') + ep.start(fault=False) + status,value=Q.retained_call(c['output'],'restored-peer-original-read',c['port'],c['forge']/'apply.private','GET',path) + if status!=200 or value['original_ep_operation']['commit']!=operation['commit']:raise RuntimeError('original peer recovery differs') + ids=c['documents']['identity'];config=ep.root/'progress-grant.private.json' + Q.private(config,dict(grant_id='r57-progress-only',consumer_id='forge-r57',workspace_instance_id='synthetic-workspace', + selections=[dict(project_id=ids['project_id'],authority_repository_id=ids['authority_repository_id'], + repository_ids=[ids['repository_id']],mission_ids=['unselected-synthetic-scope'],producer_ids=['synthetic-producer'])], + fields=['identity','progress','result','history'],operation_id='owner-issue-progress-only',ttl_seconds=3600)) + old=ep.command('execution_progress_cli','issue','--config',str(config)) + status,_=peer(Q,c,'real-progress-grant-cannot-apply','POST',c['ep_request'],token=old['credential']) + if status!=401:raise RuntimeError('real progress bearer promoted into Genesis authority') + if ep.facts()!=baseline:raise RuntimeError('availability or purpose probe performed an EP effect') + return {'unavailable503_then_original200':True,'actual_progress_grant_refused':True,'no_extra_effect':True} + + +def source_change(Q,c,kind): + """Supersede real synthetic content/effect decisions through owning HTTP.""" + ep=c['ep'];baseline=ep.facts();port=c['port'];forge=c['forge'] + if kind=='R54': + status,view=Q.C.call(port,forge/'decide.private','GET','/v1/project-proposal-decisions/proposals/'+c['proposal']['plan_id']+'?dossier_id=synthetic-dossier') + if status!=200:raise RuntimeError('own content source unavailable') + request=Q.C.request_for(view,'BUSINESS','synthetic-source-changes');request['decision']='REQUEST_CHANGES' + status,_=Q.C.call(port,forge/'decide.private','POST','/v1/project-proposal-decisions/decisions',request) + else: + request={**c['effect_request'],'operation_id':'synthetic-effect-changes','decision':'REQUEST_CHANGES', + 'expected_decision_revision':1,'previous_decision_id':c['decision']['decision_id']} + status,_=Q.C.call(port,forge/'effect.private','POST','/v1/genesis-bootstrap/decisions',request) + if status!=200:raise RuntimeError('own synthetic source supersession failed') + status,_=Q.retained_call(c['output'],'changed-'+kind+'-cannot-apply',port,forge/'apply.private','POST','/v1/genesis-apply/operations',c['body']) + if status!=409:raise RuntimeError('superseded source still authorized an effect') + status,view=Q.retained_call(c['output'],'changed-'+kind+'-historical-original',port,forge/'apply.private','GET', + '/v1/genesis-apply/operations/original-apply?dossier_id=synthetic-dossier') + if status!=200 or view['effect_decision_currently_applicable'] or view['state']!='COMPLETE':raise RuntimeError('historical original versus current applicability lost') + if ep.facts()!=baseline:raise RuntimeError('changed content/effect caused a new EP effect') + return {'changed_source':kind,'new_effect_refused409':True,'historical_complete_currently_inapplicable':True,'no_extra_effect':True} + + +def before_refusal(Q,c,kind): + """No real admission for expired/revoked permission or a raced target.""" + import time + ep=c['ep'];baseline=ep.facts();witness=None + if kind=='REVOKE': + ep.command('genesis_cli','revoke','--grant-id','r57-owner-genesis','--operation-id','owner-before-apply-revoke') + elif kind=='EXPIRE': + remaining=(datetime.fromisoformat(c['ep_grant']['expires_at'])-datetime.now(UTC)).total_seconds() + if remaining>35:raise RuntimeError('finite actual expiry fixture required') + if remaining>0:time.sleep(remaining+.1) + elif kind=='TARGET_RACE': + ep.target.mkdir(mode=0o700);witness=ep.target/'race.private';Q.private(witness,b'original synthetic raced target\n') + identity=[ep.target.stat().st_dev,ep.target.stat().st_ino] + status,value=Q.retained_call(c['output'],'actual-before-'+kind,c['port'],c['forge']/'apply.private','POST','/v1/genesis-apply/operations',c['body']) + expected=403 if kind=='EXPIRE' else 424 + if status!=expected:raise RuntimeError('before-admission negative did not refuse at its precise boundary') + if kind!='EXPIRE' and value.get('error',{}).get('code')!=('EP_GENESIS_HTTP_401' if kind=='REVOKE' else 'EP_GENESIS_HTTP_409'): + raise RuntimeError('unrelated producer failure cannot prove this authority/target refusal') + if ep.facts()!=baseline:raise RuntimeError('before-admission refusal created an EP operation/effect') + if witness and (witness.read_bytes()!=b'original synthetic raced target\n' or [ep.target.stat().st_dev,ep.target.stat().st_ino]!=identity): + raise RuntimeError('raced target was replaced or changed') + if kind in ('EXPIRE','REVOKE'): + ep_status,ep_value=peer(Q,c,'actual-current-owner-'+kind) + if ep_status!=401 or ep_value!={'error':'BOOTSTRAP_GRANT_INACTIVE'}:raise RuntimeError('actual EP did not independently enforce current authority') + return {'kind':kind,'forge_status':status,'no_ep_admission_or_effect':True,'raced_target_preserved':witness is not None} + + +def reservation_refusal(Q,c): + """Seed one genuine other EP admission, never fabricated SQL or target.""" + import time + ep=c['ep'];args=ep.args + if args.crash_boundary!='ACCEPT':raise RuntimeError('reservation fixture requires explicit owning admission barrier') + seed=deepcopy(c['ep_request']);seed['operation_id']='reservation-seed' + config=json.loads((ep.root/'grant.private.json').read_bytes()) + config.update(grant_id='r57-other-reservation',issuance_id='owner-other-reservation',target_name='seed',request=seed) + path=ep.root/'reservation-owner.private.json';Q.private(path,config) + grant=ep.command('genesis_cli','issue','--config',str(path)) + with ThreadPoolExecutor(max_workers=1) as pool: + pending=pool.submit(peer,Q,c,'actual-other-reservation','POST',seed,token=grant['credential']) + marker=ep.root/'barrier.private.json';deadline=time.monotonic()+30 + while not marker.exists(): + if time.monotonic()>deadline:raise TimeoutError('real other reservation not reached') + time.sleep(.05) + value=json.loads(marker.read_bytes()) + if value!={'pid':ep.process.pid,'point':'ACCEPT'}:raise RuntimeError('own seed reservation child differs') + before=ep.facts() + rows=before[2]['ep_genesis_operations'] + if len(rows)!=1 or rows[0]['operation_id']!='reservation-seed' or rows[0]['state']!='RESERVED' or json.loads(rows[0]['original_request'])!=seed: + raise RuntimeError('actual other original reservation not established') + Q.private(c['output']/'real-reservation-before-crash.private.json',dict(counts=before[0],evidence=before[1],rows=before[2])) + ep.stop() + # The physically interrupted seed request is explicitly uncertain. + try:pending.result(timeout=40) + except (OSError,http.client.HTTPException):pass + ep.start(fault=False) + status,refusal=Q.retained_call(c['output'],'actual-conflicting-reservation-refused',c['port'],c['forge']/'apply.private', + 'POST','/v1/genesis-apply/operations',c['body']) + if status!=424 or refusal.get('error',{}).get('code')!='EP_GENESIS_HTTP_409':raise RuntimeError('conflicting actual EP reservation not precisely refused') + after=ep.facts() + if after!=before or after[0]['ep_genesis_operations']!=1 or any(after[0][t] for t in ('ep_project_registrations','ep_repository_registrations','ep_local_repository_bindings')): + raise RuntimeError('conflicting reservation caused new admission/effects') + if ep.target.exists() or (ep.workspace/'seed').exists():raise RuntimeError('reservation fixture performed physical target effects') + return {'seed':'actual-owner-issued-other-original-admission','conflicting_forge_status':status, + 'seed_admission_only':True,'no_targets_commits_or_attachments':True} + + +def concurrent_original(Q,c): + def submit(index): + body=deepcopy(c['body']) + if index==2:body['plan_digest']='f'*64 + return Q.retained_call(c['output'],'concurrent-intent-'+str(index),c['port'],c['forge']/'apply.private', + 'POST','/v1/genesis-apply/operations',body) + with ThreadPoolExecutor(max_workers=3) as pool:values=list(pool.map(submit,range(3))) + if values[2][0] not in (403,409):raise RuntimeError('concurrent changed intent was accepted') + if any(status not in (200,409,424,503) for status,value in values[:2]):raise RuntimeError('unexpected concurrent original outcome') + for status,value in values[:2]: + if ((status==503 and value.get('error',{}).get('code') not in ('EP_GENESIS_TRANSPORT_UNCERTAIN','EP_GENESIS_DEADLINE_EXCEEDED')) + or (status==424 and value.get('error',{}).get('code')!='EP_GENESIS_HTTP_404') + or (status==409 and value.get('error',{}).get('code')!='GENESIS_LATE_RESPONSE_FENCED')): + raise RuntimeError('unrelated source/storage/authority failure cannot prove concurrency') + for status,value in values[:2]: + if status==200:return status,value,{'original_statuses':[v[0] for v in values[:2]],'conflict_status':values[2][0]} + for status,value in values[:2]: + if status==503:return status,value,{'original_statuses':[v[0] for v in values[:2]],'conflict_status':values[2][0]} + raise RuntimeError('no actual original response or transport uncertainty to recover') + + +def late_publication(Q,c): + """A real owning EP read completes before Forge's original bearer is revoked.""" + import time + ep=c['ep'];baseline=ep.facts();args=ep.args;old=getattr(args,'crash_boundary',None) + ep.stop();args.crash_boundary='READ_THREAD';ep.start() + marker=ep.root/'barrier.private.json' + if marker.exists():raise RuntimeError('late response requires an exclusive unused barrier') + try: + with ThreadPoolExecutor(max_workers=1) as pool: + future=pool.submit(Q.retained_call,c['output'],'actual-late-publication',c['port'],c['forge']/'apply.private', + 'GET','/v1/genesis-apply/operations/original-apply?dossier_id=synthetic-dossier') + try: + deadline=time.monotonic()+15 + while not marker.exists(): + if time.monotonic()>deadline:raise TimeoutError('real late EP read boundary unavailable') + time.sleep(.02) + value=json.loads(marker.read_bytes()) + if value!={'pid':ep.process.pid,'point':'READ_THREAD'}:raise RuntimeError('own late-response child identity differs') + c['command'](['-m','forge.genesis_apply_cli',*c['data'],'revoke','--grant-id',c['apply_issue']['grant_id']],c['forge']) + finally: + # Release only our exact read barrier, also on a failed revoke. + import sys + primary=sys.exc_info()[1] + try:Q.private(ep.root/'barrier.private.json.release',b'release own finite synthetic barrier\n') + except BaseException as release: + if primary is None:raise + primary.add_note('own barrier release failed: '+type(release).__name__) + status,_=future.result(timeout=35) + if status!=403:raise RuntimeError('late actual EP response escaped revoked Forge publication') + if ep.facts()!=baseline:raise RuntimeError('late response probe performed an EP effect') + finally: + import sys + primary=sys.exc_info()[1];errors=[];stopped=False + try:ep.stop();stopped=True + except BaseException as error:errors.append(error) + args.crash_boundary=old + if stopped: + try:ep.start(fault=False) + except BaseException as error:errors.append(error) + if errors: + if primary is not None: + for error in errors:primary.add_note('late-publication cleanup: '+type(error).__name__) + else: + for error in errors[1:]:errors[0].add_note('additional cleanup: '+type(error).__name__) + raise errors[0] + return {'actual_peer_read_before_owner_revoke':True,'actual_protected_status':403,'no_extra_ep_effect':True} + + +def concurrent_readbacks(Q,c,operation): + values=[] + for index in range(2): + status,value=Q.retained_call(c['output'],'original-concurrent-readback-'+str(index),c['port'],c['forge']/'apply.private', + 'GET','/v1/genesis-apply/operations/original-apply?dossier_id=synthetic-dossier') + if (status!=200 or value['state']!='COMPLETE' or value['original_request']!=c['body'] + or value['original_ep_operation']['commit']!=operation['commit'] + or value['original_ep_operation']['tree']!=operation['tree'] or value['explicit_attempts']!=1): + raise RuntimeError('concurrent original caller did not recover the same single committed operation') + values.append({'status':status,'operation_id':value['operation_id'],'commit':operation['commit'],'explicit_attempts':1}) + return values + + +def retain_owned(Q,root,ep,output): + """Private primary bytes after own processes stop, before exclusive cleanup. + + Raw bearer/config containers and credentials directories are excluded. + Nothing is extracted or followed through an archived symlink. + """ + import os + import tarfile + selected=[('ep-target',ep.target),('ep-native-evidence',ep.data/'g/evidence'), + ('forge-onboarding',root/'f/runtime/onboarding')] + for base,path in (('ep-central',ep.data/'epdata.sqlite'),('forge-central',root/'f/runtime/forge.db')): + for suffix in ('','-wal','-shm'): + value=Path(str(path)+suffix) + if value.exists():selected.append((base+suffix,value)) + archive=output/'owned-primary-evidence.private.tar' + import stat + def add_owned(saved,fd,archive_name): + # Every component is opened relative to a pinned own directory. No + # symlink, special file or externally hardlinked payload is admissible. + try: + before=os.fstat(fd) + if before.st_uid!=os.getuid():raise RuntimeError('archive ownership differs') + item=tarfile.TarInfo(archive_name);item.mode=stat.S_IMODE(before.st_mode) + item.uid=before.st_uid;item.gid=before.st_gid;item.mtime=before.st_mtime + if stat.S_ISDIR(before.st_mode): + item.type=tarfile.DIRTYPE;saved.addfile(item) + for child in sorted(os.listdir(fd)): + childfd=os.open(child,os.O_RDONLY|os.O_NOFOLLOW|os.O_NONBLOCK,dir_fd=fd) + add_owned(saved,childfd,archive_name+'/'+child) + elif stat.S_ISREG(before.st_mode) and before.st_nlink==1: + item.size=before.st_size + with os.fdopen(os.dup(fd),'rb') as payload:saved.addfile(item,payload) + after=os.fstat(fd) + if (before.st_size,before.st_mtime_ns,before.st_ctime_ns)!=(after.st_size,after.st_mtime_ns,after.st_ctime_ns): + raise RuntimeError('archive payload changed during capture') + else:raise RuntimeError('archive symlink/special/hardlink refused') + finally:os.close(fd) + rootfd=os.open(root,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW) + try: + marker=__import__('json').loads((root/'.r57-owner.json').read_bytes()) + info=os.fstat(rootfd) + if marker['uid']!=os.getuid() or marker['identity']!=[info.st_dev,info.st_ino]:raise RuntimeError('archive root identity differs') + with archive.open('xb') as stream: + os.chmod(archive,0o600) + with tarfile.open(fileobj=stream,mode='w',dereference=False) as saved: + for name,path in selected: + parts=path.relative_to(root).parts + parent=os.dup(rootfd) + try: + for part in parts[:-1]: + child=os.open(part,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=parent) + os.close(parent);parent=child + selectedfd=os.open(parts[-1],os.O_RDONLY|os.O_NOFOLLOW|os.O_NONBLOCK,dir_fd=parent) + except FileNotFoundError:continue # Only absent optional selection, never recursive payload loss. + finally:os.close(parent) + add_owned(saved,selectedfd,name) + stream.flush();os.fsync(stream.fileno()) + finally:os.close(rootfd) + with tarfile.open(archive,'r') as saved: + members=saved.getmembers() + if any(p.name.startswith('/') or '..' in Path(p.name).parts for p in members):raise RuntimeError('own archive member boundary differs') + # Consume every retained regular payload to reject truncated tar data. + for member in members: + if member.isfile(): + with saved.extractfile(member) as payload: + while payload.read(65536):pass + result={'archive':archive.name,'sha256':sha256(archive.read_bytes()).hexdigest(),'members':len(members), + 'private':True,'raw_bearers_and_config_containers_excluded':True,'owned_processes_stopped_before_capture':True} + Q.private(output/'owned-primary-evidence.private.json',result) + return result + + +def interrupted_recovery(Q,c,servers,report,forge_pid,step): + """Actual owning execute() error handling persists the literal partial state.""" + ep=c['ep'];path='/v1/genesis-apply/operations/original-apply?dossier_id=synthetic-dossier' + status,value=Q.retained_call(c['output'],'actual-interrupted-post',c['port'],c['forge']/'apply.private','POST','/v1/genesis-apply/operations',c['body']) + if status!=424 or value.get('error',{}).get('code')!='EP_GENESIS_HTTP_409':raise RuntimeError('genuine owning controlled interruption unavailable') + status,original=Q.retained_call(c['output'],'actual-interrupted-original-get',c['port'],c['forge']/'apply.private','GET',path) + expected='EFFECT_UNCERTAIN' if step=='PREFLIGHT' else 'PARTIAL' + if (status!=200 or original['state']!=expected or original['original_ep_operation']['last_error']!='BOOTSTRAP_QUALIFICATION_CONTROLLED_INTERRUPT' + or original['original_request']!=c['body'] or original['explicit_attempts']!=1):raise RuntimeError('literal original interrupted state not preserved') + baseline=ep.facts();old_ep=ep.process.pid + ep.stop();servers.close();ep.start(fault=False) + port,new_forge,_=servers.enter_context(Q.C.server(ep.args.python,c['forge'],c['output'])) + if ep.process.pid==old_ep or new_forge==forge_pid:raise RuntimeError('interruption recovery lacks independent OS restart') + status,readback=Q.retained_call(c['output'],'interrupted-after-os-restarts-original-get',port,c['forge']/'apply.private','GET',path) + if status!=200 or readback['state']!=expected or readback['original_request']!=c['body'] or ep.facts()!=baseline:raise RuntimeError('original interrupted readback changed effects or identity') + resume={'contract_version':'forge-genesis-apply/v1','instance_id':c['body']['instance_id'],'dossier_id':c['body']['dossier_id'], + 'operation_id':c['body']['operation_id'],'attempt_id':'explicit-interrupted-recovery','original_request_digest':original['original_request_digest']} + status,value=Q.retained_call(c['output'],'explicit-interrupted-resume',port,c['forge']/'apply.private','POST','/v1/genesis-apply/operations/original-apply/resume',resume) + report['literal_interrupted_recovery']={'state':expected,'actual_get_status':200,'controlled_error_after_real_step':step, + 'old_ep_pid':old_ep,'new_ep_pid':ep.process.pid,'old_forge_pid':forge_pid,'new_forge_pid':new_forge,'no_effect_on_original_get':True,'explicit_original_resume':True} + c['port']=port + return port,status,value diff --git a/scripts/qualification/qualify_genesis_apply_ci.sh b/scripts/qualification/qualify_genesis_apply_ci.sh new file mode 100755 index 0000000..d5eb829 --- /dev/null +++ b/scripts/qualification/qualify_genesis_apply_ci.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# Actual qualified producer on the normal disposable hosted runner. +set -euo pipefail +test "${GITHUB_ACTIONS:-}" = true +test "${RUNNER_ENVIRONMENT:-}" = github-hosted +test "${RUNNER_OS:-}" = macOS +qualification_source="$(pwd)" +qualification_root="$RUNNER_TEMP/forge-genesis-apply-ci" +qualification_ep_sha=4d0c0461d7a5ec7fb7a50aa42f775cc4be09faa5 +mkdir "$qualification_root" "$qualification_root/ep-source" "$qualification_root/forge-wheel" "$qualification_root/ep-wheel" +git -C "$qualification_root/ep-source" init -q +git -C "$qualification_root/ep-source" remote add origin https://github.com/pcvantol/engineering-platform.git +git -C "$qualification_root/ep-source" fetch --depth=1 origin "$qualification_ep_sha" +git -C "$qualification_root/ep-source" checkout --detach FETCH_HEAD +test "$(git -C "$qualification_root/ep-source" rev-parse HEAD)" = "$qualification_ep_sha" +test "$(git -C "$qualification_root/ep-source" rev-parse 'HEAD^{tree}')" = b095ba006beccd79171115a7deab55ef7b7c4ff9 +python "$qualification_root/ep-source/tools/qualification/build_platform_wheel.py" --source-root "$qualification_root/ep-source" --version 2.3.123 --wheel-directory "$qualification_root/ep-wheel" +python -m pip wheel . --no-deps --wheel-dir "$qualification_root/forge-wheel" +# Exclusive short runtime and scratch names retain the owning finite placement policy. +test -d /private/tmp +test ! -e /private/tmp/g57 +python -m venv /private/tmp/g57 +python -m venv "$qualification_root/forge-installed" +/private/tmp/g57/bin/python -m pip install --no-deps "$qualification_root/ep-wheel/engineering_platform-2.3.123-py3-none-any.whl" +qualification_wheels=("$qualification_root"/forge-wheel/forge_autonomy-*.whl) +test "${#qualification_wheels[@]}" -eq 1 +"$qualification_root/forge-installed/bin/python" -m pip install --no-deps "${qualification_wheels[0]}" +"$qualification_root/forge-installed/bin/python" -m pip install jsonschema==4.26.0 +npm install --prefix "$qualification_root/native" --ignore-scripts --no-audit --no-fund @openai/codex@0.160.1 +mkdir "$qualification_root/native/bin" +python - "$qualification_root/native" <<'NATIVE' +from pathlib import Path +import os,sys +root=Path(sys.argv[1]) +found=[p for p in (root/'node_modules/@openai').rglob('codex') if '/vendor/' in str(p) and p.is_file() and os.access(p,os.X_OK)] +if len(found)!=1:raise SystemExit('one genuine platform native executable required') +(root/'bin/codex').symlink_to(found[0].resolve()) +NATIVE +# The qualified native chain is macOS. Linux namespace setup is not applicable. +qualification_index=70 +for qualification_boundary in source-r54 source-r56 verify registered concurrent-late revoke-accept expire-accept revoke-gitref before-revoke before-expire target-race reservation uncertain partial; do + qualification_index=$((qualification_index+1)) + qualification_flags=() + case "$qualification_boundary" in + source-r54) qualification_flags=(--crash-boundary ACCEPT --source-change R54);; + source-r56) qualification_flags=(--crash-boundary GIT_REF --source-change R56);; + verify) qualification_flags=(--crash-boundary VERIFY_COMMIT --security-matrix);; + registered) qualification_flags=(--crash-boundary REGISTER --extended-matrix);; + concurrent-late) qualification_flags=(--concurrent --late-publication);; + revoke-accept) qualification_flags=(--crash-boundary ACCEPT --authority-fault REVOKE);; + expire-accept) qualification_flags=(--crash-boundary ACCEPT --authority-fault EXPIRE --ep-ttl 30);; + revoke-gitref) qualification_flags=(--crash-boundary GIT_REF --authority-fault REVOKE);; + before-revoke) qualification_flags=(--before-refusal REVOKE);; + before-expire) qualification_flags=(--before-refusal EXPIRE --ep-ttl 30);; + target-race) qualification_flags=(--before-refusal TARGET_RACE);; + reservation) qualification_flags=(--crash-boundary ACCEPT --reservation-race);; + uncertain) qualification_flags=(--interruption-step PREFLIGHT);; + partial) qualification_flags=(--interruption-step WRITE_FILES);; + *) exit 2;; + esac + "$qualification_root/forge-installed/bin/python" -I \ + "$qualification_source/scripts/qualification/qualify_installed_genesis_apply.py" \ + --python "$qualification_root/forge-installed/bin/python" --wheel "${qualification_wheels[0]}" \ + --source-revision "$(git -C "$qualification_source" rev-parse HEAD)" \ + --ep-python /private/tmp/g57/bin/python \ + --ep-wheel "$qualification_root/ep-wheel/engineering_platform-2.3.123-py3-none-any.whl" \ + --ep-wheel-sha256 bc49e7a3b15c9021b16546a5ca96e5fbf280072d900d8d3dc173dee4b79d5b9e \ + --ep-source-revision "$qualification_ep_sha" --ep-source-tree b095ba006beccd79171115a7deab55ef7b7c4ff9 \ + --ep-source-root "$qualification_root/ep-source" --ep-native "$qualification_root/native" \ + --external-validator "$qualification_source/forge/project_bootstrap/assets/validate.py" \ + --output-dir "$qualification_root/proof-$qualification_boundary" --scratch-root "/private/tmp/h$qualification_index" \ + --modeled-os-identity "${qualification_flags[@]}" +done diff --git a/scripts/qualification/qualify_installed_genesis_apply.py b/scripts/qualification/qualify_installed_genesis_apply.py new file mode 100644 index 0000000..887c4ed --- /dev/null +++ b/scripts/qualification/qualify_installed_genesis_apply.py @@ -0,0 +1,848 @@ +"""First small real installed Forge/EP Genesis chain; own synthetic targets only. + +This does not replace the later negative/recovery matrix or final-main proof. +It requires the separately qualified producer pin before initializing a target. +""" +import argparse +from contextlib import contextmanager, ExitStack +from datetime import UTC, datetime, timedelta +from hashlib import sha256 +from importlib.util import spec_from_file_location, module_from_spec +import json +import fnmatch +import configparser +from types import SimpleNamespace +from concurrent.futures import ThreadPoolExecutor +import signal +import tomllib +import os +from pathlib import Path +import shutil +import socket +import sqlite3 +import subprocess +import sys +import time +import zipfile + +HERE = Path(__file__).resolve().parent +spec = spec_from_file_location('genesis_owning_qualification', HERE/'qualify_installed_genesis_bootstrap.py') +G = module_from_spec(spec); spec.loader.exec_module(G) +C = G.C +CONTRACT = 'forge-genesis-apply/v1' + + +class ExpectedNegativeComplete(Exception): + """Internal completed probe; final cleanup/retention still decides exit.""" + + +# Test-only crash barriers wrap actual owning producer persistence boundaries. +# No ledger state, authority, effect result or product byte is fabricated. +FAULT_LAUNCHER = r"""import json,os,signal,sys,time +from pathlib import Path +from engineering_platform import genesis_service as service,server +point,marker,root,instance=sys.argv[1:] +def barrier(): + pending=Path(marker+'.pending') + with pending.open('x') as stream: + os.chmod(pending,0o600);json.dump({'pid':os.getpid(),'point':point},stream) + stream.flush();os.fsync(stream.fileno()) + os.replace(pending,marker) + if point=='READ_THREAD': + deadline=time.monotonic()+120 + while not Path(marker+'.release').exists(): + if time.monotonic()>deadline:raise TimeoutError('finite owning read barrier not released') + time.sleep(.01) + else:os.kill(os.getpid(),signal.SIGSTOP) +if point=='ACCEPT': + original=service.admit + def wrapped(*a,**k): + result=original(*a,**k) + if result['newly_admitted']:barrier() + return result + service.admit=wrapped +elif point=='READ_THREAD': + original=service.read + def wrapped(*a,**k): + result=original(*a,**k);barrier();return result + service.read=wrapped +elif point.startswith('FAIL_'): + original=service._execute_step + def wrapped(*a,**k): + result=original(*a,**k) + if k['step']==point.removeprefix('FAIL_'): + from engineering_platform.genesis_contract import BootstrapError + raise BootstrapError('BOOTSTRAP_QUALIFICATION_CONTROLLED_INTERRUPT',409) + return result + service._execute_step=wrapped +elif point=='REGISTER': + original=service.execute + def wrapped(*a,**k): + result=original(*a,**k);barrier();return result + service.execute=wrapped +elif point!='OBSERVE_ONLY': + original=service._execute_step + def wrapped(*a,**k): + result=original(*a,**k) + if k['step']==point:barrier() + return result + service._execute_step=wrapped +observed_execute=service.execute +def observed(*a,**k): + try:return observed_execute(*a,**k) + except BaseException as error: + try: + import traceback,re + names={'genesis_service.py','genesis_native.py','subprocess.py','provider_process_identity.py','server.py'} + def bounded(error,depth=0): + value={'type':type(error).__name__,'errno':getattr(error,'errno',None), + 'code':getattr(error,'code',None) if isinstance(getattr(error,'code',None),str) and re.fullmatch(r'BOOTSTRAP_[A-Z0-9_]{1,100}',error.code) else None, + 'frames':[{'file':Path(f.filename).name,'function':f.name,'line':f.lineno} for f in traceback.extract_tb(error.__traceback__) if Path(f.filename).name in names and re.fullmatch(r'[A-Za-z_][A-Za-z0-9_]{0,100}',f.name)]} + if depth<3: + primary=getattr(error,'primary',None) + if isinstance(primary,BaseException):value['primary']=bounded(primary,depth+1) + cleanup=getattr(error,'cleanup_errors',()) + if isinstance(cleanup,(tuple,list)):value['cleanup_errors']=[bounded(e,depth+1) for e in cleanup[:16] if isinstance(e,BaseException)] + return value + target=Path(root)/'g/evidence/r57-primary-error.private.json' + with target.open('x') as stream: + os.chmod(target,0o600);json.dump(bounded(error),stream) + except BaseException as retention:error.add_note('bounded owning error observation unavailable: '+type(retention).__name__) + raise +service.execute=observed +raise SystemExit(server.main(['serve','--data-root',root,'--expected-instance-id',instance])) +""" + + +def apply_outcome(*args): + try:return apply_call(*args) + except (OSError, TimeoutError) as lost: + return 503, {'transport_failure':type(lost).__name__} + + +def retained_call(output, name, *args): + status, value = apply_outcome(*args) + private(output/(name+'.http.private.json'), {'status':status, 'response':value}) + return status, value + + +def failure_diagnostics(ep,result): + """Only closed numeric/enum diagnostics, never tokens, commands or paths.""" + import re + def code(value): + return value if isinstance(value,str) and re.fullmatch(r'(?:EP_GENESIS_|GENESIS_|BOOTSTRAP_)[A-Z0-9_]{1,100}',value) else None + answer={'consumer_error_code':code(result.get('error',{}).get('code')) if isinstance(result,dict) else None} + if ep is not None: + diagnostic=ep.data/'g/evidence/r57-primary-error.private.json' + if diagnostic.exists(): + value=json.loads(diagnostic.read_bytes()) + allowed_files={'genesis_service.py','genesis_native.py','subprocess.py','provider_process_identity.py','server.py'} + def closed(value,depth=0): + answer={'type':value.get('type') if isinstance(value.get('type'),str) and re.fullmatch(r'[A-Za-z_][A-Za-z0-9_]{0,100}',value['type']) else None, + 'errno':value.get('errno') if isinstance(value.get('errno'),int) else None,'code':code(value.get('code')), + 'frames':[{'file':f['file'],'function':f['function'],'line':f['line']} for f in value.get('frames',[]) if f.get('file') in allowed_files and isinstance(f.get('function'),str) and re.fullmatch(r'[A-Za-z_][A-Za-z0-9_]{0,100}',f['function']) and isinstance(f.get('line'),int)]} + if depth<3: + if isinstance(value.get('primary'),dict):answer['primary']=closed(value['primary'],depth+1) + if isinstance(value.get('cleanup_errors'),list):answer['cleanup_errors']=[closed(e,depth+1) for e in value['cleanup_errors'][:16] if isinstance(e,dict)] + return answer + answer['owning_exception']=closed(value) + counts,evidence,rows=ep.facts() + allowed_states={'RESERVED','EXECUTING','PARTIAL','EFFECT_UNCERTAIN','COMPLETE'} + steps={'PREFLIGHT','CREATE_TARGET','WRITE_FILES','VALIDATE_DOCS','GIT_INIT','GIT_PREPARE','GIT_OBJECT','GIT_REF','VERIFY_COMMIT','REGISTER_AND_ATTACH'} + answer['producer_counts']={k:v for k,v in counts.items() if isinstance(v,int) and not isinstance(v,bool)} + answer['producer_operations']=[{'state':row['state'] if row['state'] in allowed_states else 'INVALID', + 'completed_steps':[step for step in value.get('completed_steps',[]) if step in steps], + 'last_error':code(value.get('last_error')), + 'attempts':[{'step':a.get('step') if a.get('step') in steps else 'INVALID','exit_code':a.get('exit_code') if isinstance(a.get('exit_code'),int) else None} for a in value.get('attempts',[])]} + for row,value in zip(rows['ep_genesis_operations'],evidence)] + return answer + + +def original_recovery(args, ep, servers, forge, body, report, port, forge_pid): + """Kill/restart the exact own OS children after a real durable boundary.""" + marker=ep.root/'barrier.private.json' + with ThreadPoolExecutor(max_workers=1) as pool: + pending=pool.submit(retained_call,args.output_dir,'lost-original-post',port,forge/'apply.private', + 'POST','/v1/genesis-apply/operations',body) + deadline=time.monotonic()+120 + while not marker.exists(): + if time.monotonic()>deadline:raise TimeoutError('actual EP crash boundary not reached') + time.sleep(.05) + boundary=json.loads(marker.read_bytes()) + if boundary!={'pid':ep.process.pid,'point':args.crash_boundary}:raise RuntimeError('own crash boundary identity differs') + counts,evidence,rows=ep.facts() + private(args.output_dir/'before-crash-owning-evidence.private.json',{'counts':counts,'evidence':evidence,'rows':rows}) + before_state=rows['ep_genesis_operations'][0]['state'] + if getattr(args,'authority_fault',None)=='REVOKE': + ep.command('genesis_cli','revoke','--grant-id','r57-owner-genesis','--operation-id','owner-boundary-revoke') + elif getattr(args,'authority_fault',None)=='EXPIRE': + remaining=(datetime.fromisoformat(args.ep_grant_expiry)-datetime.now(UTC)).total_seconds() + if remaining>35:raise RuntimeError('finite real expiry fixture required') + if remaining>0:time.sleep(remaining+.1) + old_ep=ep.process.pid + ep.stop() # exact original unreaped child and identity-checked workers + servers.close() # independent real Forge Server stop, same durable stores + pending.result(timeout=40) + ep.start(fault=False) + new_port,new_forge,_=servers.enter_context(C.server(args.python,forge,args.output_dir)) + if new_forge==forge_pid or ep.process.pid==old_ep:raise RuntimeError('separate OS restart not demonstrated') + path='/v1/genesis-apply/operations/original-apply?dossier_id=synthetic-dossier' + status,original=retained_call(args.output_dir,'after-two-os-restarts-original-get',new_port,forge/'apply.private','GET',path) + if getattr(args,'authority_fault',None): + if status not in (403,424):raise RuntimeError('expired/revoked boundary accepted a new operation/read') + if ep.facts()[1]!=evidence or ep.facts()[0]!=counts:raise RuntimeError('authority refusal changed original physical evidence') + report['authority_boundary_refusal']={'fault':args.authority_fault,'boundary':args.crash_boundary, + 'actual_status':status,'original_evidence_unchanged':True,'before_state':before_state, + 'old_ep_pid':old_ep,'new_ep_pid':ep.process.pid,'old_forge_pid':forge_pid,'new_forge_pid':new_forge} + return new_port,status,original + if status!=200:raise RuntimeError('original operation unavailable after independent OS restarts') + if original['operation_id']!=body['operation_id'] or original['original_request']!=body or original['explicit_attempts']!=1: + raise RuntimeError('original intent changed after restart') + if ep.facts()[1]!=evidence:raise RuntimeError('Forge GET performed an EP effect') + report['actual_crash_recovery']={'boundary':args.crash_boundary,'before_state':before_state, + 'before_forge_pid':forge_pid,'after_forge_pid':new_forge,'before_ep_pid':old_ep,'after_ep_pid':ep.process.pid, + 'original_get_state':original['state'],'no_effect_on_get':True} + if original['state']!='COMPLETE': + resume={'contract_version':CONTRACT,'instance_id':body['instance_id'],'dossier_id':body['dossier_id'], + 'operation_id':body['operation_id'],'attempt_id':'explicit-owning-recovery','original_request_digest':original['original_request_digest']} + status,original=retained_call(args.output_dir,'explicit-original-resume',new_port,forge/'apply.private', + 'POST','/v1/genesis-apply/operations/original-apply/resume',resume) + return new_port,status,original + + + +def retain_rejected_ledger(ledger,destination,original_identity): + """Retain only an owned replacement; atomic link refuses any existing slot.""" + import stat + if not os.path.lexists(ledger):return + info=ledger.lstat() + if (info.st_dev,info.st_ino)==original_identity or info.st_uid!=os.getuid() or not stat.S_ISREG(info.st_mode) or info.st_nlink!=1: + raise RuntimeError('rejected ledger ownership differs') + os.link(ledger,destination,follow_symlinks=False) + if (ledger.lstat().st_dev,ledger.lstat().st_ino)!=(info.st_dev,info.st_ino):raise RuntimeError('rejected ledger changed') + ledger.unlink() + + +def restore_owning_ledger(ledger,retained,destination,original_identity,primary): + failures=[] + try:retain_rejected_ledger(ledger,destination,original_identity) + except BaseException as error:failures.append(error) + try: + import stat + info=retained.lstat() + if (info.st_dev,info.st_ino)!=original_identity or info.st_uid!=os.getuid() or not stat.S_ISREG(info.st_mode) or info.st_nlink!=1: + raise RuntimeError('original retained ledger identity differs') + # link() never replaces any occupied slot, including a dangling link. + os.link(retained,ledger,follow_symlinks=False) + retained.unlink() + except BaseException as error:failures.append(error) + if failures: + if primary is not None: + for error in failures:primary.add_note('own ledger retention/restoration failed: '+type(error).__name__) + else: + for error in failures[1:]:failures[0].add_note('additional original-ledger restoration failed: '+type(error).__name__) + raise failures[0] + + +def completion_matrix(args, ep, servers, forge, body, operation, report, port): + """Actual HTTP refusals and causal restored installed defect; own data only.""" + baseline=ep.facts() + failures=[] + for label,change in [('instance',{'instance_id':'foreign'}),('context',{'dossier_id':'foreign'}), + ('plan',{'plan_digest':'f'*64}),('decision',{'decision_digest':'f'*64}), + ('pairing',{'pairing_id':'foreign'}),('caller-role',{'role':'OWNER'}), + ('caller-target',{'target_ref':'foreign'})]: + status,value=retained_call(args.output_dir,'refusal-'+label,port,forge/'apply.private','POST','/v1/genesis-apply/operations',{**body,**change}) + if status not in (400,403,404,409):raise RuntimeError('changed context/intent unexpectedly accepted: '+label) + failures.append(label) + status,_=retained_call(args.output_dir,'refusal-old-purpose',port,forge/'effect.private','POST','/v1/genesis-apply/operations',body) + if status!=403:raise RuntimeError('old effect grant upgraded into apply authority') + item='/v1/genesis-apply/operations/original-apply?dossier_id=synthetic-dossier' + for label,relative in [('own-journal','onboarding/genesis-apply/store.json'),('own-source','onboarding/genesis-bootstrap/store.json'),('own-content-source','onboarding/content-decisions/store.json')]: + path=forge/'runtime'/relative + raw=path.read_bytes();retained=path.with_name(path.name+'.r57-retained') + path.rename(retained) + try: + status,_=retained_call(args.output_dir,'missing-'+label,port,forge/'apply.private','GET',item) + if status not in (409,503):raise RuntimeError('missing initialized source treated as healthy') + private(path,b'{}\n') + status,_=retained_call(args.output_dir,'corrupt-'+label,port,forge/'apply.private','GET',item) + if status not in (409,503):raise RuntimeError('corrupt initialized source treated as healthy') + finally: + if path.exists():path.unlink() + retained.rename(path) + if path.read_bytes()!=raw:raise RuntimeError('original owning source restoration differs') + ledger=ep.data/'epdata.sqlite';raw_ledger=ledger.read_bytes();retained_ledger=ledger.with_name('epdata.r57-original.sqlite') + info=ledger.lstat();original_identity=(info.st_dev,info.st_ino) + ledger.rename(retained_ledger) + try: + status,_=retained_call(args.output_dir,'missing-owning-ep-ledger',port,forge/'apply.private','GET',item) + if status not in (403,424,503):raise RuntimeError('missing initialized EP ledger treated as healthy') + retain_rejected_ledger(ledger,ep.data/'g/evidence/r57-missing-replacement-ledger.private.sqlite',original_identity) + private(ledger,b'NOT_A_SQLITE_LEDGER\n') + status,_=retained_call(args.output_dir,'corrupt-owning-ep-ledger',port,forge/'apply.private','GET',item) + if status not in (403,424,503):raise RuntimeError('corrupt initialized EP ledger treated as healthy') + finally: + restore_owning_ledger(ledger,retained_ledger,ep.data/'g/evidence/r57-corrupt-replacement-ledger.private.sqlite',original_identity,sys.exc_info()[1]) + if ledger.read_bytes()!=raw_ledger or ep.facts()!=baseline:raise RuntimeError('original EP ledger restoration differs') + def detector(current_port): + status,value=retained_call(args.output_dir,'detector-'+str(time.monotonic_ns()),current_port,forge/'apply.private','GET',item) + if status!=200:raise RuntimeError('actual original operation detector unavailable') + if value['original_ep_operation']['commit']!=operation['commit'] or value['state']!='COMPLETE':raise RuntimeError('original operation differs') + assert value['project_readiness']=='NOT_ASSESSED','R57_FALSE_PROJECT_READINESS' + return value + detector(port) + module=Path(subprocess.check_output([str(args.python),'-I','-c','import forge.genesis_apply_service as m;print(m.__file__)'],cwd=args.wheel.parent).decode().strip()) + original=module.read_bytes();needle=b"'project_readiness':'NOT_ASSESSED'" + if original.count(needle)!=1:raise RuntimeError('exact isolated installed defect target unavailable') + servers.close() + try: + module.write_bytes(original.replace(needle,b"'project_readiness':'READY'")) + faulty_port,_,_=servers.enter_context(C.server(args.python,forge,args.output_dir)) + try:detector(faulty_port) + except AssertionError as error: + if str(error)!='R57_FALSE_PROJECT_READINESS':raise + report['restored_defect_red']={'category':'AssertionError','detector':'R57_FALSE_PROJECT_READINESS','installed_module_original_sha256':sha256(original).hexdigest()} + else:raise RuntimeError('restored real installed defect escaped its detector') + finally: + primary=sys.exc_info()[1];cleanup_errors=[] + try:servers.close() + except BaseException as error:cleanup_errors.append(error) + try: + module.write_bytes(original) + if module.read_bytes()!=original:raise RuntimeError('isolated installed bytes not restored') + except BaseException as error:cleanup_errors.append(error) + if cleanup_errors: + if primary is not None: + for error in cleanup_errors:primary.add_note('isolated fault cleanup: '+type(error).__name__) + else: + for error in cleanup_errors[1:]:cleanup_errors[0].add_note('additional cleanup: '+type(error).__name__) + raise cleanup_errors[0] + port,_,_=servers.enter_context(C.server(args.python,forge,args.output_dir)) + detector(port);report['restored_defect_green']=True + if ep.facts()!=baseline:raise RuntimeError('negative/read/fault detector produced an EP effect') + # Owning producer revocation is performed by the separate synthetic EP owner. + ep.command('genesis_cli','revoke','--grant-id','r57-owner-genesis','--operation-id','owner-revoke-original-apply') + status,_=retained_call(args.output_dir,'revoked-ep-no-disclosure',port,forge/'apply.private','GET',item) + if status not in (403,424):raise RuntimeError('revoked EP authority disclosed a successful result') + if ep.facts()!=baseline:raise RuntimeError('revocation/read changed original physical effects') + report['negative_matrix']={'changed_bindings_refused':failures,'old_effect_grant_refused':True, + 'missing_and_corrupt_initialized_journal_and_source_refused':True,'missing_and_corrupt_owning_ep_ledger_refused':True,'missing_and_corrupt_r54_source_refused':True,'ep_revocation_refused':True, + 'no_extra_ep_effects':True} + return port + + +def canonical(value): + return (json.dumps(value,sort_keys=True,separators=(',',':'),ensure_ascii=False,allow_nan=False)+'\n').encode() + + +def private(path, value): + with path.open('xb') as stream: + os.chmod(path,0o600) + stream.write(value if isinstance(value,bytes) else canonical(value)) + + +def probe(python, code, cwd): + return json.loads(subprocess.check_output([str(python),'-I','-c',code],cwd=cwd,timeout=20)) + + +def ep_payload(args): + """Noneditable original wheel payload and actual packaged schema readback.""" + if sha256(args.ep_wheel.read_bytes()).hexdigest()!=args.ep_wheel_sha256: + raise ValueError('qualified EP wheel digest differs') + def git(*command): + return subprocess.check_output(['git','-C',str(args.ep_source_root),*command],timeout=30) + actual_tree=git('rev-parse',args.ep_source_revision+'^{tree}').decode().strip() + if actual_tree!=args.producer_pin['producer_tree']: + raise ValueError('actual qualified EP Git tree differs') + manifest=tomllib.loads(git('show',args.ep_source_revision+':pyproject.toml').decode()) + patterns=manifest['tool']['setuptools']['package-data']['engineering_platform'] + def selected(name): + relative=name.removeprefix('src/engineering_platform/') + return relative.endswith('.py') or any(len(relative.split('/'))==len(pattern.split('/')) + and all(fnmatch.fnmatchcase(part,rule) for part,rule in zip(relative.split('/'),pattern.split('/'))) for pattern in patterns) + names=git('ls-tree','-r','--name-only',args.ep_source_revision,'--','src/engineering_platform').decode().splitlines() + expected={name.removeprefix('src/'):git('show',args.ep_source_revision+':'+name) for name in names if selected(name)} + with zipfile.ZipFile(args.ep_wheel) as wheel: + members=wheel.namelist() + if len(members)!=len(set(members)) or any(name.startswith('/') or '..' in Path(name).parts for name in members): + raise ValueError('EP wheel member alias') + if any(not name.startswith('engineering_platform/') and '.dist-info/' not in name for name in members): + raise ValueError('unexpected EP wheel payload namespace') + from email.parser import BytesParser + metadata=[n for n in wheel.namelist() if n.endswith('.dist-info/METADATA')] + if len(metadata)!=1:raise ValueError('EP metadata ambiguity') + name=BytesParser().parsebytes(wheel.read(metadata[0]))['Name'] + if name!=manifest['project']['name']:raise ValueError('EP package identity differs') + code="import json,engineering_platform,importlib.metadata;from pathlib import Path;d=importlib.metadata.distribution("+repr(name)+");print(json.dumps({'package':str(Path(engineering_platform.__file__).resolve().parent),'version':d.version,'direct':json.loads(d.read_text('direct_url.json')),'entries':{e.name:e.value for e in d.entry_points if e.group=='console_scripts'}}))" + info=probe(args.ep_python,code,args.ep_wheel.parent); package=Path(info['package']) + if package.is_relative_to(args.ep_source_root.resolve()) or package.is_relative_to(G.SOURCE):raise ValueError('EP must be outside development checkout') + if info['direct'].get('dir_info',{}).get('editable') or info['direct'].get('archive_info',{}).get('hashes',{}).get('sha256')!=args.ep_wheel_sha256: + raise ValueError('noneditable qualified EP wheel required') + if info['version']!=args.producer_pin['producer_version']: + raise ValueError('EP installed version differs') + payload=[n for n in wheel.namelist() if n.startswith('engineering_platform/') and not n.endswith('/')] + if set(payload)!=set(expected):raise ValueError('EP source/wheel inventory differs') + for name in payload: + if wheel.read(name)!=expected[name] or (package.parent/name).read_bytes()!=expected[name]: + raise ValueError('EP source/wheel/installed payload differs') + installed={str(path.relative_to(package.parent)) for path in package.rglob('*') if path.is_file() and '__pycache__' not in path.parts} + if installed!=set(expected):raise ValueError('EP installed inventory differs') + entries=[name for name in members if name.endswith('.dist-info/entry_points.txt')] + if len(entries)!=1:raise ValueError('EP entrypoint ambiguity') + parser=configparser.ConfigParser();parser.read_string(wheel.read(entries[0]).decode()) + if dict(parser['console_scripts'])!=manifest['project']['scripts'] or info['entries']!=manifest['project']['scripts']: + raise ValueError('EP source/wheel/installed entrypoints differ') + args.ep_package=package + + schema=wheel.read('engineering_platform/schemas/genesis-local-bootstrap-v1.schema.json') + if sha256(schema).hexdigest()!=args.producer_pin['producer_schema_sha256']: + raise ValueError('EP qualified schema differs') + return {'version':info['version'],'payload_files_verified':len(payload),'wheel_sha256':args.ep_wheel_sha256, + 'source_revision':args.ep_source_revision,'source_tree':actual_tree,'source_payload_entrypoints_exact':True} + + +def apply_call(port, token, method, path, body=None): + from urllib.request import Request, urlopen + from urllib.error import HTTPError + request=Request('http://127.0.0.1:'+str(port)+path,method=method, + data=canonical(body) if body is not None else None, + headers={'Authorization':'Bearer '+token.read_text().strip(), + 'X-Forge-Genesis-Apply-Audience':'synthetic-workspace','Content-Type':'application/json'}) + try: + with urlopen(request,timeout=35) as response:return response.status,json.loads(response.read()) + except HTTPError as error: + try:return error.code,json.loads(error.read()) + finally:error.close() + + +def verify_b8r(rows, request, operation, documents, target, grant_id): + """Compare actual owning registration rows with the committed declaration.""" + identity=documents['identity'] + expected_project=identity['project_id'];expected_repository=identity['repository_id'] + if any(len(rows[table])!=1 for table in ('ep_genesis_operations','ep_project_registrations','ep_repository_registrations','ep_local_repository_bindings')): + raise ValueError('one original registered authority required') + original=rows['ep_genesis_operations'][0] + if (original['operation_id']!=request['operation_id'] or original['grant_id']!=grant_id + or original['request_digest']!=sha256(canonical(request)).hexdigest() + or json.loads(original['original_request'])!=request or original['state']!='COMPLETE' + or original['project_id']!=expected_project or original['repository_id']!=expected_repository): + raise ValueError('original EP operation/registration binding differs') + project=rows['ep_project_registrations'][0];repository=rows['ep_repository_registrations'][0];binding=rows['ep_local_repository_bindings'][0] + declaration=json.loads(request['bundle']['files']['.engineering-platform/repository.json']['utf8']) + expected_surface={**declaration,'validation':{**declaration['validation'],'description':declaration['validation'].get('description')}, + 'requirements':{'host':{},'tools':{}},'integrations':{}} + if (project['project_id']!=expected_project or project['status']!='ACTIVE' + or json.loads(project['attachment_contract'])!={'schema_version':declaration['schema_version'],'authority_repository_id':identity['authority_repository_id']} + or (repository['project_id'],repository['repository_id'],repository['authority_repository_id'],repository['role']) + !=(expected_project,expected_repository,identity['authority_repository_id'],'authority') + or json.loads(repository['attachment_contract'])!=expected_surface + or (binding['project_id'],binding['repository_id'],binding['state'])!=(expected_project,expected_repository,'BOUND') + or Path(binding['local_root'])!=target.resolve()): + raise ValueError('committed B8R registration/declaration/physical binding differs') + known=json.loads(original['evidence'])['known'] + if (known.get('commit_ref')!=operation['commit'] or known.get('tree')!=operation['tree'] + or known.get('committed_declaration_verified') is not True): + raise ValueError('committed B8R operation evidence differs') + return {'project_id':expected_project,'repository_id':expected_repository,'committed_registration_and_target_binding_verified':True} + + +def group_active(group): + """A zombie is exited evidence, never a running worker or signal target.""" + listing=subprocess.check_output(['ps','-axo','pgid=,stat='],text=True,timeout=3) + for line in listing.splitlines(): + parts=line.split() + if len(parts)!=2:raise RuntimeError('process group state unavailable') + if int(parts[0])==group and not parts[1].startswith(('Z','X')):return True + return False + + +def require_worker_groups_exited(evidence): + for value in evidence: + for attempt in value['attempts']: + group=attempt.get('process',{}).get('process_group') + if group: + try:os.killpg(group,0) + except ProcessLookupError:continue + except PermissionError: + if not group_active(group):continue + raise + if group_active(group):raise RuntimeError('own EP worker group remains active or uncertain') + + +class EPServer: + def __init__(self,args,root,output): + self.args,self.root,self.output=args,root,output + self.data=root/'s'; self.workspace=root/'w'; self.workspace.mkdir(mode=0o700) + self.target=self.workspace/'r' + self.env={k:v for k,v in os.environ.items() if k not in ('PYTHONPATH','PYTHONHOME')} + self.env.update(PATH=str(args.ep_python.parent)+':'+str(args.ep_native/'bin')+':/usr/bin:/bin', + EP_MANAGED_CODEX_CLI_PREFIX=str(args.ep_native),PYTHONDONTWRITEBYTECODE='1') + with socket.socket() as sock:sock.bind(('127.0.0.1',0));self.port=sock.getsockname()[1] + self.command_counter=0 + self.instance=self.command('server','init','--bind-host','127.0.0.1','--bind-port',str(self.port))['instance_id'] + self.command('genesis_cli','init') + self.process=None;self.log=None + + def command(self,module,*arguments): + command=[str(self.args.ep_python),'-I','-m','engineering_platform.'+module,*arguments,'--data-root',str(self.data)] + if hasattr(self,'instance'):command+=['--expected-instance-id',self.instance] + self.command_counter+=1 + case='ep-owner-'+str(self.command_counter) + report,stdout,stderr=C.BINDING.CAPTURE.capture(command,case=case,output=self.output,cwd=self.root,env=self.env,expected=0) + primary=None + if report['primary_failure'] or not report['capture_complete']: + primary=RuntimeError('own EP owner command failed: '+module) + primary.capture_report=report + try: + C.BINDING.CAPTURE.retain(self.output/(case+'.capture.private.json'),canonical(report)) + except OSError as retention: + if primary is None:primary=retention + else:primary.add_note('EP command report retention failed: '+type(retention).__name__) + if primary is not None:raise primary + value=json.loads(stdout) + if arguments and arguments[0]=='issue' and 'credential' in value: + # A successful credential-bearing stdout is a secret container. + # Preserve its digest and redacted metadata, then remove only that + # exact newly owned container; failed diagnostics remain private. + raw_path=self.output/('private-'+case+'-0.log') + if raw_path.read_bytes()!=stdout:raise RuntimeError('own credential capture changed') + C.BINDING.CAPTURE.retain(self.output/(case+'.redacted.private.json'),canonical({ + 'stdout_sha256':sha256(stdout).hexdigest(),'result':{k:v for k,v in value.items() if k!='credential'},'credential_container_removed':True})) + raw_path.unlink() + return value + + def issue(self,request): + validator=self.root/'v.py' + raw=self.args.external_validator.read_bytes() + if sha256(raw).hexdigest()!=self.args.validator_digest:raise ValueError('original qualified external validator differs') + private(validator,raw) + config=self.root/'grant.private.json' + private(config,dict(grant_id='r57-owner-genesis',issuance_id='r57-owner-issuance',consumer_id='forge-r57',principal='synthetic-r57-owner', + target_name='r',workspace_root=str(self.workspace),validator_path=str(validator),request=request,ttl_seconds=getattr(self.args,'ep_ttl',3600))) + answer=self.command('genesis_cli','issue','--config',str(config)) + token=self.root/'ep-token.private';private(token,answer.pop('credential').encode()) + return answer,token + + def start(self, *, fault=True): + self.log=(self.output/'ep-server.private.log').open('ab');os.chmod(self.output/'ep-server.private.log',0o600) + command=[str(self.args.ep_python),'-I','-m','engineering_platform.server','serve','--data-root',str(self.data),'--expected-instance-id',self.instance] + point=getattr(self.args,'crash_boundary',None) or ('FAIL_'+self.args.interruption_step if getattr(self.args,'interruption_step',None) else None) + command=[str(self.args.ep_python),'-I','-c',FAULT_LAUNCHER,point if fault and point else 'OBSERVE_ONLY',str(self.root/'barrier.private.json'),str(self.data),self.instance] + self.process=subprocess.Popen(command,cwd=self.root,env=self.env,stdout=self.log,stderr=self.log,start_new_session=True) + deadline=time.monotonic()+20 + while time.monotonic()=deadline:raise TimeoutError('own descendant freeze did not stabilize') + for identity in reversed(list(owned.values())): + if identity.pid==process.pid:continue + state=module.verify_process_identity(identity) + if state=='MATCH':os.kill(identity.pid,signal.SIGKILL) + elif state!='NOT_ACTIVE':raise RuntimeError('own descendant identity changed before stop') + except BaseException as error:primary=error + finally: + # This exact still-unreaped child's original PGID remains owned, + # including an already-exited leader and inherited-pipe children. + try: + os.waitid(os.P_PID,process.pid,os.WEXITED|os.WNOHANG|os.WNOWAIT) + try:os.killpg(process.pid,signal.SIGKILL) + except ProcessLookupError:pass + except PermissionError: + # Darwin can refuse signaling a group containing only + # already-exited zombies. Never waive a live/unknown group. + if group_active(process.pid):raise + process.wait(timeout=15) + if 'groups' in locals(): + deadline=time.monotonic()+5 + for group in groups: + while True: + try:os.killpg(group,0) + except ProcessLookupError:break + except PermissionError: + if not group_active(group):break + raise + if not group_active(group):break + if time.monotonic()>=deadline:raise RuntimeError('own process group active or uncertain after stop') + time.sleep(.05) + except BaseException as stop_error: + if primary is None:primary=stop_error + else:primary.add_note('own Server stop incomplete: '+type(stop_error).__name__) + self.process=None + if self.log is not None:self.log.close();self.log=None + if primary is not None:raise primary + + +def run(args): + args.output_dir.mkdir(mode=0o700) + report={'qualification':'R57_FIRST_SMALL_REAL_INSTALLED_CHAIN','result':'FAIL','cleanup':False,'cases':[], + 'scope':'SYNTHETIC_NEW_LOCAL_DOCUMENT_ONLY; NOT_PROJECT_READINESS; NOT_FULL_MATRIX', + 'human_decisions':'SYNTHETIC_TECHNICAL_NOT_REAL_OWNER_APPROVAL', + 'operator_identity_evidence':'MODELED_TRUSTED_OS_FIXTURE' if C.MODELED_OS else 'ACTUAL_MACOS_GENERATED_UID'} + primary=None;ep=None;root=args.scratch_root + try: + pin=probe(args.python,'import json;from forge.genesis_apply_peer import QUALIFIED_PRODUCER;print(json.dumps(QUALIFIED_PRODUCER))',args.wheel.parent) + if not isinstance(pin,dict) or pin.get('producer_revision')!=args.ep_source_revision or pin.get('producer_tree')!=args.ep_source_tree: + raise ValueError('actual qualified producer handoff/pin required before test effects') + args.producer_pin=pin + report['forge']=C.BINDING.installed_binding(args.python,args.wheel,args.source_revision) + report['ep']=ep_payload(args) + if root.parent!=Path('/private/tmp') or not root.name.isalnum() or len(root.name)>5: + raise ValueError('exclusive finite short synthetic root required') + root.mkdir(mode=0o700) + private(root/'.r57-owner.json',dict(assignment='L3-GENESIS-EP-APPLY-COORDINATOR-V1-20261016',source=args.source_revision,uid=os.getuid(),identity=[root.stat().st_dev,root.stat().st_ino])) + ep=EPServer(args,root,args.output_dir) + counter=0 + def command(arguments,cwd): + nonlocal counter + counter+=1 + return G.capture_command(args.python,arguments,cwd,args.output_dir,'r57-setup-'+str(counter)) + forge=root/'f';setup=command([str(HERE/'proposal_content_fixture.py'),str(forge)],root) + data=['--data-root',str(forge/'runtime')];expiry=(datetime.now(UTC)+timedelta(hours=1)).isoformat() + command(['-m','forge.proposal_content_cli',*data,'init'],forge) + command(['-m','forge.proposal_content_cli',*data,'issue','--principal-id',setup['human_principal_id'],'--proposal-principal-id','synthetic-service', + '--audience','synthetic-workspace','--dossier-id','synthetic-dossier','--permission','READ','--permission','DECIDE','--role','BUSINESS','--role','ARCHITECTURE', + '--expires-at',expiry,'--token-file',str(forge/'decide.private')],forge) + with ExitStack() as servers: + port,forge_pid,_=servers.enter_context(C.server(args.python,forge,args.output_dir)) + original=json.loads(subprocess.check_output(['git','-C',str(G.SOURCE),'show',args.source_revision+':examples/project-bootstrap/authority-input.json'])) + status,proposal=C.call(port,forge/'prepare.private','POST','/v1/onboarding/plans',dict(contract_version='forge-project-proposal-http/v1', + instance_id=setup['instance_id'],dossier_id='synthetic-dossier',operation_id='original-proposal',proposal_kind='pb-render/1.0',input=original)) + assert status==200,(status,proposal) + status,content=C.call(port,forge/'decide.private','GET','/v1/project-proposal-decisions/proposals/'+proposal['plan_id']+'?dossier_id=synthetic-dossier');assert status==200 + for role in ('BUSINESS','ARCHITECTURE'): + status,content=C.call(port,forge/'decide.private','POST','/v1/project-proposal-decisions/decisions',C.request_for(content,role,'content-'+role.lower()));assert status==200 + scope=['--proposal-principal-id','synthetic-service','--audience','synthetic-workspace','--dossier-id','synthetic-dossier','--source-plan-id',proposal['plan_id'],'--expires-at',expiry] + command(['-m','forge.genesis_bootstrap_cli',*data,'init'],forge) + command(['-m','forge.genesis_bootstrap_cli',*data,'issue','--principal-id','synthetic-service',*scope,'--permission','READ','--permission','PREPARE','--token-file',str(forge/'bootstrap.private')],forge) + effects=['CREATE_NEW_TARGET','INITIALIZE_LOCAL_GIT','WRITE_EXACT_MANIFEST_FILES','RUN_EXTERNALLY_PINNED_VALIDATION','CREATE_LOCAL_COMMIT','REGISTER_COMMITTED_AUTHORITY_ATTACHMENT'] + request=dict(contract_version=G.CONTRACT,instance_id=setup['instance_id'],dossier_id='synthetic-dossier',operation_id='original-plan', + source_plan_id=proposal['plan_id'],source_subject=content['subject'],expected_content_revision=2,target=dict(ep_instance_id=ep.instance,placement_ref='owner-placement',target_ref='owner-target'), + effects=effects,expires_at=expiry,expected_plan_revision=0,previous_plan_id=None,blocking_open_points=[]) + status,view=C.call(port,forge/'bootstrap.private','POST','/v1/genesis-bootstrap/plans',request);assert status==200,(status,view) + plan=view['plan'];exact=forge/'exact-plans.private';private(exact,[dict(plan_id=plan['plan_id'],plan_digest=plan['plan_digest'])]) + command(['-m','forge.genesis_bootstrap_cli',*data,'issue','--principal-id',setup['human_principal_id'],*scope,'--permission','READ','--permission','DECIDE_EFFECT','--exact-plans-file',str(exact),'--token-file',str(forge/'effect.private')],forge) + effect_request=dict(contract_version=G.CONTRACT,instance_id=setup['instance_id'],dossier_id='synthetic-dossier',operation_id='original-effect-decision',plan_id=plan['plan_id'],plan_digest=plan['plan_digest'], + purpose='GENESIS_BOOTSTRAP_EFFECT_ONLY/1.0',decision='APPROVE_EFFECT',reason='Synthetic exact technical effect qualification only.',expected_decision_revision=0,previous_decision_id=None) + status,view=C.call(port,forge/'effect.private','POST','/v1/genesis-bootstrap/decisions',effect_request);assert status==200,(status,view) + decision=view['original_decision'];documents=plan['documents'] + ep_request=dict(contract_version='1.0',operation_id='original-apply',target_ref=plan['target']['target_ref'], + effect_plan_binding=dict(plan_id=plan['plan_id'],plan_revision=plan['plan_revision'],plan_digest=plan['plan_digest'],decision_id=decision['decision_id'],decision_digest=decision['decision_digest'], + proposal_id=proposal['plan_id'],proposal_digest=plan['source_subject']['result_digest'],source_instance_id=setup['instance_id'],source_actor_id=decision['principal_id'],context_id='synthetic-dossier'), + bundle=dict(contract_version='pb-render/1.0',kind='RENDERED_PROPOSAL_EXAMPLE',files={r['path']:dict(sha256=r['sha256'],utf8=r['content']) for r in documents['files']})) + args.validator_digest=documents['validation']['validator_sha256'] + ep_grant,ep_token=ep.issue(ep_request);args.ep_grant_expiry=ep_grant['expires_at'];ep.start() + command(['-m','forge.genesis_apply_cli',*data,'init'],forge) + config={**pin,'pairing_id':'original-owner-pair','endpoint':'http://127.0.0.1:'+str(ep.port), + 'binding':dict(instance_id=ep.instance,host_id=sha256((os.uname().nodename+':'+str(os.geteuid())).encode()).hexdigest(),consumer_id='forge-r57',principal='synthetic-r57-owner',operation_id='original-apply'), + 'ep_grant_id':ep_grant['grant_id'],'expires_at':ep_grant['expires_at'],'plan_id':plan['plan_id'],'plan_digest':plan['plan_digest'], + 'decision_id':decision['decision_id'],'decision_digest':decision['decision_digest'],'placement_ref':'owner-placement','target_ref':'owner-target', + 'request_digest':sha256(canonical(ep_request)).hexdigest(),'input_digest':plan['source_subject']['input_digest']} + config_path=forge/'peer-config.private';private(config_path,config) + command(['-m','forge.genesis_apply_cli',*data,'bind-peer','--config-file',str(config_path),'--credential-file',str(ep_token)],forge) + apply_issue=command(['-m','forge.genesis_apply_cli',*data,'issue','--principal-id',setup['human_principal_id'],*scope,'--permission','READ','--permission','REQUEST_APPLY','--permission','RESUME','--exact-plans-file',str(exact),'--token-file',str(forge/'apply.private')],forge) + body=dict(contract_version=CONTRACT,instance_id=setup['instance_id'],dossier_id='synthetic-dossier',operation_id='original-apply',plan_id=plan['plan_id'],plan_digest=plan['plan_digest'],decision_id=decision['decision_id'],decision_digest=decision['decision_digest'],pairing_id='original-owner-pair') + extra_spec=spec_from_file_location('r57_actual_peer_matrix',HERE/'genesis_apply_matrix.py') + extra=module_from_spec(extra_spec);extra_spec.loader.exec_module(extra) + helpers=SimpleNamespace(C=C,private=private,canonical=canonical,retained_call=retained_call) + context=dict(config=config,ep=ep,ep_token=ep_token,ep_request=ep_request,output=args.output_dir, + command=command,data=data,scope=scope,setup=setup,forge=forge,port=port,body=body, + documents=documents,proposal=proposal,effect_request=effect_request,decision=decision,ep_grant=ep_grant,apply_issue=apply_issue) + if getattr(args,'extended_matrix',False):report['pre_admission_negative_matrix']=extra.prechecks(helpers,context) + if getattr(args,'before_refusal',None) or getattr(args,'reservation_race',False): + if getattr(args,'reservation_race',False):report['before_refusal']=extra.reservation_refusal(helpers,context) + else:report['before_refusal']=extra.before_refusal(helpers,context,args.before_refusal) + report['qualification']='R57_REAL_PRE_ADMISSION_REFUSAL' + report['cases']=['actual-current-owner-authority-and-original-intent','no-extra-ep-admission-or-effect'] + evidence=ep.facts()[1] + raise ExpectedNegativeComplete + if getattr(args,'concurrent',False): + status,result,report['actual_concurrency']=extra.concurrent_original(helpers,context) + elif getattr(args,'interruption_step',None): + port,status,result=extra.interrupted_recovery(helpers,context,servers,report,forge_pid,args.interruption_step) + elif getattr(args,'crash_boundary',None): + port,status,result=original_recovery(args,ep,servers,forge,body,report,port,forge_pid) + else: + status,result=retained_call(args.output_dir,'initial-apply',port,forge/'apply.private','POST','/v1/genesis-apply/operations',body) + report['initial_apply_status']=status + if report.get('authority_boundary_refusal'): + ep_status,ep_value=extra.peer(helpers,context,'actual-inactive-boundary-owner') + if ep_status!=401 or ep_value!={'error':'BOOTSTRAP_GRANT_INACTIVE'}:raise RuntimeError('actual EP boundary authority not inactive') + report['actual_ep_auth_refusal']={'status':ep_status,'response':ep_value} + evidence=ep.facts()[1] + report['qualification']='R57_REAL_AUTHORITY_BOUNDARY_REFUSAL' + report['cases']=['original-admission-preserved','real-current-ep-authority-refused','no-further-effect-on-read-or-restart'] + raise ExpectedNegativeComplete + deadline=time.monotonic()+300 + reads=0 + # Only bounded authenticated original GETs follow uncertainty or + # in-progress evidence. No retry POST and no inferred completion. + while status!=200 or result['state']!='COMPLETE': + if reads>=40 or time.monotonic()>=deadline:raise TimeoutError('original EP operation did not prove completion') + if status not in (200,503):raise RuntimeError('original operation refused') + time.sleep(5);reads+=1 + status,result=retained_call(args.output_dir,'original-read-'+str(reads),port,forge/'apply.private','GET','/v1/genesis-apply/operations/original-apply?dossier_id=synthetic-dossier') + if status==200 and result['state'] in ('PARTIAL','EFFECT_UNCERTAIN'): + raise RuntimeError('own EP result requires explicit recovery, not automatic resume') + report['bounded_original_reads']=reads + from jsonschema import Draft202012Validator + schema=json.loads(subprocess.check_output([str(args.python),'-I','-c',"from importlib.resources import files;print(files('forge').joinpath('api/genesis-apply-v1.json').read_text())"],cwd=root)) + Draft202012Validator(schema).validate(result) + operation=result['original_ep_operation'];assert result['project_readiness']=='NOT_ASSESSED' and result['mission']=='NOT_REQUESTED' + status,replay=apply_call(port,forge/'apply.private','POST','/v1/genesis-apply/operations',body);assert status==200 + assert replay['original_ep_operation']['commit']==operation['commit'] and replay['explicit_attempts']==(2 if getattr(args,'interruption_step',None) or getattr(args,'crash_boundary',None) not in (None,'REGISTER') else 1) + counts,evidence,registered_rows=ep.facts();assert all(counts[t]==1 for t in ('ep_genesis_operations','ep_project_registrations','ep_repository_registrations','ep_local_repository_bindings')) + assert all(counts[t]==0 for t in ('ep_submissions','ep_execution_runs','engineering_transactions')) + git=lambda *a:subprocess.check_output(['/usr/bin/git','-C',str(ep.target),*a],timeout=20) + assert git('rev-parse','HEAD').decode().strip()==operation['commit'] + assert git('rev-parse','HEAD^{tree}').decode().strip()==operation['tree'] + assert len(git('rev-list','--all').splitlines())==1 + for row in documents['files']: + assert git('show','HEAD:'+row['path'])==row['content'].encode() + assert git('ls-tree','HEAD','--',row['path']).startswith(b'100644 blob ') + b8r=verify_b8r(registered_rows,ep_request,operation,documents,ep.target,ep_grant['grant_id']) + assert operation['validator_sha256']==documents['validation']['validator_sha256']==sha256(args.external_validator.read_bytes()).hexdigest() + assert operation['validation']['artifacts']==37 and operation['validation']['manifest_digest']==documents['manifest_digest'] + assert len(evidence)==1 and 'VALIDATE_DOCS' in evidence[0]['completed_steps'] and 'REGISTER_AND_ATTACH' in evidence[0]['completed_steps'] + report.update(cases=['actual-r52-r54-r56-original-http-chain','separate-test-owner-issued-ep-grant','actual-forge-to-ep-post-get', + 'independent-37-committed-byte-and-mode-readback','one-commit-tree-trusted-validator-and-b8r','same-original-replay-no-second-admission'], + operation_id=operation['operation_id'],commit=operation['commit'],tree=operation['tree'],ep_counts=counts,b8r=b8r,explicit_attempts=replay['explicit_attempts']) + context['port']=port + if getattr(args,'concurrent',False):report['actual_concurrency']['both_original_readbacks']=extra.concurrent_readbacks(helpers,context,operation) + if getattr(args,'extended_matrix',False):report['availability_and_progress']=extra.availability_and_progress(helpers,context,operation) + if getattr(args,'late_publication',False):report['late_publication']=extra.late_publication(helpers,context) + if getattr(args,'source_change',None):report['changed_source_matrix']=extra.source_change(helpers,context,args.source_change) + if getattr(args,'security_matrix',False): + port=completion_matrix(args,ep,servers,forge,body,operation,report,port) + report['post_matrix_forge']=C.BINDING.installed_binding(args.python,args.wheel,args.source_revision) + report['post_matrix_ep']=ep_payload(args) + report['chain_assertions']='PASS' + except ExpectedNegativeComplete as completed: + if getattr(completed,'__notes__',None): + primary=RuntimeError('expected negative probe has unresolved Server cleanup failure') + for note in completed.__notes__:primary.add_note(note) + report['failure']={'category':type(primary).__name__,'stage':'negative-context-cleanup'} + except BaseException as error: + primary=error;report['failure']={'category':type(error).__name__} + if hasattr(error,'capture_report'):report['failure']['command_outcome']=error.capture_report + try: + report['failure']['bounded_diagnostics']=failure_diagnostics(ep,locals().get('result')) + except BaseException as secondary:error.add_note('bounded failure diagnostics unavailable: '+type(secondary).__name__) + finally: + try: + if ep is not None:ep.stop() + if root.exists(): + # Preserve failures, scratch and credentials for safe owning + # recovery; successful cleanup is exclusive own-root only. + if primary is None: + marker=json.loads((root/'.r57-owner.json').read_bytes());info=root.stat() + if marker['uid']!=os.getuid() or marker['identity']!=[info.st_dev,info.st_ino]:raise RuntimeError('own scratch identity changed') + require_worker_groups_exited(evidence) + report['retained_primary_evidence']=extra.retain_owned(helpers,root,ep,args.output_dir) + shutil.rmtree(root);report['cleanup']=True + else:report['cleanup']=True + except BaseException as cleanup: + report['cleanup_failure']={'category':type(cleanup).__name__} + if primary is None:primary=cleanup + else:primary.add_note('own cleanup failed: '+type(cleanup).__name__) + report['result']='PASS' if primary is None and report['cleanup'] else 'FAIL' + try:private(args.output_dir/'first-real-installed-chain.public.json',report) + except OSError as retention: + if primary is None:primary=retention + else:primary.add_note('receipt retention failed: '+type(retention).__name__) + if primary is not None:raise primary + return report + + +def main(): + p=argparse.ArgumentParser(description=__doc__) + for field in ('python','wheel','ep-python','ep-wheel','ep-native','external-validator','ep-source-root','output-dir','scratch-root'): + p.add_argument('--'+field,type=Path,required=True) + for field in ('source-revision','ep-source-revision','ep-source-tree','ep-wheel-sha256'): + p.add_argument('--'+field,required=True) + p.add_argument('--concurrent',action='store_true') + p.add_argument('--late-publication',action='store_true') + p.add_argument('--before-refusal',choices=('REVOKE','EXPIRE','TARGET_RACE')) + p.add_argument('--reservation-race',action='store_true') + p.add_argument('--ep-ttl',type=int,choices=(30,3600),default=3600) + p.add_argument('--authority-fault',choices=('REVOKE','EXPIRE')) + p.add_argument('--extended-matrix',action='store_true') + p.add_argument('--source-change',choices=('R54','R56')) + p.add_argument('--modeled-os-identity',action='store_true') + p.add_argument('--security-matrix',action='store_true') + p.add_argument('--interruption-step',choices=('PREFLIGHT','WRITE_FILES')) + p.add_argument('--crash-boundary',choices=('ACCEPT','GIT_REF','VERIFY_COMMIT','REGISTER')) + args=p.parse_args() + if args.authority_fault and not args.crash_boundary:p.error('authority fault requires an explicit real persisted boundary') + if (args.authority_fault=='EXPIRE' or args.before_refusal=='EXPIRE') and args.ep_ttl!=30:p.error('actual expiry case requires finite thirty-second EP grant') + if args.interruption_step and (args.crash_boundary or args.authority_fault or args.before_refusal or args.reservation_race or args.concurrent or args.late_publication or args.source_change or args.security_matrix or args.extended_matrix):p.error('interruption recovery requires an exclusive genuine execution error') + extra_flags=(args.concurrent or args.late_publication or args.source_change or args.security_matrix or args.extended_matrix) + if args.before_refusal and (args.crash_boundary or args.authority_fault or args.reservation_race or extra_flags):p.error('before-refusal is an exclusive probe') + if args.reservation_race and (args.crash_boundary!='ACCEPT' or args.authority_fault or extra_flags):p.error('reservation probe requires only its ACCEPT barrier') + if args.authority_fault and extra_flags:p.error('authority-boundary refusal is an exclusive probe') + if args.concurrent and args.crash_boundary:p.error('concurrency cannot select a crash barrier') + if args.late_publication and (args.crash_boundary or args.source_change or args.security_matrix):p.error('late publication requires an exclusive unused read barrier') + if args.source_change and args.security_matrix:p.error('source supersession and grant revocation require separate probes') + C.MODELED_OS=args.modeled_os_identity;print(json.dumps(run(args),sort_keys=True)) + + +if __name__=='__main__':main() diff --git a/tests/test_genesis_apply_contract.py b/tests/test_genesis_apply_contract.py new file mode 100644 index 0000000..b515ea6 --- /dev/null +++ b/tests/test_genesis_apply_contract.py @@ -0,0 +1,61 @@ +"""Synthetic binding checks; these fixtures confer no real effect authority.""" +from copy import deepcopy +from unittest import TestCase +from unittest.mock import patch +from tests.test_genesis_bootstrap_contract import plan_fixture +from forge.genesis_bootstrap_contract import plan_document, EFFECT_PURPOSE, CONTRACT as PLAN_CONTRACT +from forge.genesis_apply_contract import CONTRACT, apply_request, ep_request +from forge.project_bootstrap.schema import canonical, ContractError +from forge.project_bootstrap.service import digest + + +def dispatch_fixture(): + args = plan_fixture() + plan = plan_document(*args) + decision = dict(contract_version=PLAN_CONTRACT, purpose=EFFECT_PURPOSE, decision='APPROVE_EFFECT', + instance_id=plan['instance_id'], dossier_id=plan['dossier_id'], audience=plan['audience'], + plan_id=plan['plan_id'], plan_digest=plan['plan_digest'], + decision_id='synthetic-effect-decision', principal_id='synthetic-human-owner') + decision['decision_digest'] = digest(canonical(decision)) + return args[0], plan, decision + + +class GenesisDispatchBindingTests(TestCase): + def test_exact_original_bytes_and_distinct_digests_without_render(self): + args = dispatch_fixture(); before = deepcopy(args) + with patch('forge.project_bootstrap.service.render', side_effect=AssertionError('no second renderer')): + request, request_digest = ep_request(*args, 'synthetic-original-operation') + self.assertEqual(args, before) + self.assertEqual(request_digest, digest(canonical(request))) + self.assertEqual(len(request['bundle']['files']), 37) + self.assertEqual(digest(canonical(request['bundle'])), '0c2c0fb41a68793be0960bb4289d7590060c8092ebb976852e1c4674b0f66b1d') + self.assertEqual(request['effect_plan_binding']['proposal_digest'], args[1]['source_subject']['result_digest']) + self.assertNotEqual(request['effect_plan_binding']['proposal_digest'], digest(canonical(request['bundle']))) + self.assertEqual(request['effect_plan_binding']['source_actor_id'], args[2]['principal_id']) + for row in args[0]['result']['artifacts']: + self.assertEqual(request['bundle']['files'][row['path']], {'sha256': row['sha256'], 'utf8': row['content']}) + + def test_changed_original_plan_decision_and_scope_rejected(self): + for choice in ('plan', 'decision', 'bytes', 'source', 'negative', 'purpose', 'scope', 'actor'): + original, plan, decision = dispatch_fixture() + if choice == 'plan': plan['target']['target_ref'] = 'changed' + elif choice == 'decision': decision['decision_id'] = 'changed' + elif choice == 'bytes': original['result']['artifacts'][0]['content'] += 'changed' + elif choice == 'source': original['plan_id'] = 'changed' + else: + field, value = {'negative': ('decision', 'REQUEST_CHANGES'), 'purpose': ('purpose', 'CONTENT_ONLY'), + 'scope': ('dossier_id', 'foreign'), 'actor': ('principal_id', '/private/credential')}[choice] + decision[field] = value + decision['decision_digest'] = digest(canonical({k: v for k, v in decision.items() if k != 'decision_digest'})) + with self.subTest(choice=choice), self.assertRaises(ValueError): + ep_request(original, plan, decision, 'synthetic-original-operation') + + def test_closed_input_references_only(self): + _, plan, decision = dispatch_fixture() + value = dict(contract_version=CONTRACT, instance_id=plan['instance_id'], dossier_id=plan['dossier_id'], + operation_id='synthetic-original-operation', plan_id=plan['plan_id'], plan_digest=plan['plan_digest'], + decision_id=decision['decision_id'], decision_digest=decision['decision_digest'], pairing_id='owner-pairing') + self.assertEqual(apply_request(value), (value, digest(canonical(value)))) + for update in ({'credential': 'secret'}, {'target_ref': '/tmp/target'}, {'operation_id': '/tmp/path'}, + {'plan_digest': 'wrong'}, {'contract_version': 'other'}, {'pairing_id': 'x\r\nHeader:bad'}): + with self.subTest(update=update), self.assertRaises(ValueError): apply_request({**value, **update}) diff --git a/tests/test_genesis_apply_grant.py b/tests/test_genesis_apply_grant.py new file mode 100644 index 0000000..72ac493 --- /dev/null +++ b/tests/test_genesis_apply_grant.py @@ -0,0 +1,51 @@ +"""Actual owning runtime grants; no transport or EP effect claims.""" +import unittest +from datetime import UTC, datetime, timedelta +import tests.test_genesis_bootstrap_http as bootstrap_tests +BASE = bootstrap_tests.BASE +from forge.genesis_apply_grant import GenesisApplyGrant +from forge.genesis_bootstrap_grant import GenesisBootstrapGrant + + +class GenesisApplyGrantTests(unittest.TestCase): + def setUp(self): + self.owner = bootstrap_tests.GenesisBootstrapHttpTest(); self.addCleanup(self.owner.doCleanups); self.owner.setUp() + self.owner.approve_content(); _, self.view = self.owner.prepare() + self.owner.effect_grant(self.view) + status, self.view = self.owner.call('POST', BASE+'/decisions', self.owner.effect(self.view), token='effect') + self.assertEqual(status, 200, self.view) + self.grant = GenesisApplyGrant(self.owner.root/'runtime', self.owner.setup['instance_id']) + self.grant.initialize() + + def issue(self, **updates): + return self.grant.issue(**dict(principal_id=self.owner.setup['human_principal_id'], audience='synthetic-workspace', + proposal_principal_id='synthetic-service', dossier_ids=['synthetic-dossier'], source_plan_ids=[self.owner.source['plan_id']], + permissions=['READ', 'REQUEST_APPLY', 'RESUME'], + exact_plans=[{'plan_id': self.view['plan']['plan_id'], 'plan_digest': self.view['plan']['plan_digest']}], + expires_at=self.owner.expiry, token_path=self.owner.root/'apply.private', **updates)) + + def test_separate_purpose_exact_current_owner_plan_and_revoke(self): + issued = self.issue() + auth = 'Bearer '+(self.owner.root/'apply.private').read_text().strip() + principal = self.grant.authorize(auth, audience='synthetic-workspace', dossier_id='synthetic-dossier', permission='REQUEST_APPLY', plan=self.view['plan']) + self.assertEqual(principal.grant_id, issued['grant_id']) + self.assertEqual(principal.purpose, 'GENESIS_APPLY_ONLY/1.0') + with self.assertRaises(PermissionError): self.grant.authorize(auth, permission='REQUEST_APPLY', plan={**self.view['plan'], 'plan_digest':'f'*64}) + with self.assertRaises(PermissionError): GenesisBootstrapGrant(self.grant.root,self.grant.instance_id).authorize(auth) + old = 'Bearer '+(self.owner.root/'effect.private').read_text().strip() + with self.assertRaises(PermissionError): self.grant.authorize(old) + self.grant.revoke(issued['grant_id']) + with self.assertRaises(PermissionError): self.grant.authorize(auth) + + def test_rejects_foreign_sources_unfrozen_plan_permissions_and_credential_destination(self): + base = dict(principal_id=self.owner.setup['human_principal_id'], audience='synthetic-workspace', proposal_principal_id='synthetic-service', + dossier_ids=['synthetic-dossier'], source_plan_ids=[self.owner.source['plan_id']], permissions=['READ','REQUEST_APPLY'], + exact_plans=[{'plan_id': self.view['plan']['plan_id'], 'plan_digest': self.view['plan']['plan_digest']}], + expires_at=self.owner.expiry, token_path=self.owner.root/'apply.private') + for updates in ({'source_plan_ids':['foreign']}, {'exact_plans':[{'plan_id':'foreign','plan_digest':'f'*64}]}, + {'permissions':['READ','DECIDE_EFFECT']}, {'permissions':['READ','REQUEST_APPLY'],'exact_plans':[]}, + {'principal_id':'synthetic-service'}, {'token_path':self.grant.root/'overwrite'}, + {'expires_at':(datetime.now(UTC)-timedelta(seconds=1)).isoformat()}): + with self.subTest(updates=updates),self.assertRaises((ValueError,PermissionError)): + self.grant.issue(**{**base,**updates}) + self.assertFalse((self.owner.root/'apply.private').exists()) diff --git a/tests/test_genesis_apply_http.py b/tests/test_genesis_apply_http.py new file mode 100644 index 0000000..1cb1839 --- /dev/null +++ b/tests/test_genesis_apply_http.py @@ -0,0 +1,199 @@ +"""Real Forge HTTP with synthetic peer responses; not installed EP qualification.""" +from copy import deepcopy +from datetime import UTC, datetime +from importlib.resources import files +from pathlib import Path +import unittest +from unittest.mock import patch +import tests.test_genesis_bootstrap_http as bootstrap_tests +from forge.ep_inspection_peer import PeerError +from forge.genesis_apply_contract import CONTRACT, ep_request +from forge.genesis_apply_grant import GenesisApplyGrant +from forge.genesis_apply_pairing import GenesisApplyPairings +from forge.genesis_apply_service import GenesisApplyService, BASE +from forge.project_bootstrap.schema import canonical, load_json +from forge.project_bootstrap.service import digest + + +def response_for(pairing, ep, plan, state='COMPLETE'): + schema = load_json(files('forge').joinpath('api/ep-genesis-local-bootstrap-v1.schema.json').read_bytes())['oneOf'][0] + now = datetime.now(UTC).isoformat() + return {'contract_version':'1.0', 'operation_kind':'CREATE_GENESIS_LOCAL', 'operation_id':ep['operation_id'], + 'producer_version':'2.3.123', 'execution_mode':'GENESIS', 'baseline':'portable-docs/1.0.0', + 'authorization_ref':pairing['ep_grant_id'], 'placement_precondition':'ABSENT_VERIFIED_AT_ADMISSION', + 'request_digest':digest(canonical(ep)), 'target_ref':ep['target_ref'], 'state':state, + 'effect_plan_binding':ep['effect_plan_binding'], 'bundle_digest':digest(canonical(ep['bundle'])), + 'manifest_digest':plan['documents']['manifest_digest'], 'input_digest':pairing['input_digest'], + 'instance_id':pairing['binding']['instance_id'], 'host_id':pairing['binding']['host_id'], + 'identity':{**plan['documents']['identity'], 'state':'REGISTERED' if state == 'COMPLETE' else 'RESERVED'}, + 'completed_steps':schema['properties']['completed_steps']['items']['enum'] if state == 'COMPLETE' else [], + 'commit':'a'*40 if state == 'COMPLETE' else None, 'tree':'b'*40 if state == 'COMPLETE' else None, + 'step_effects':[], 'validator_sha256':plan['documents']['validation']['validator_sha256'] if state == 'COMPLETE' else None, + 'validation':{'state':'VALIDATED_PROPOSAL', 'manifest_digest':plan['documents']['manifest_digest'], + 'product_code_coverage':'NOT_APPLICABLE_NO_PRODUCT_CODE','artifacts':37} if state == 'COMPLETE' else None, + 'last_error':None, 'source_updated_at':now, 'observed_at':now, 'forge_readiness':'NOT_OWNED_BY_EP', + 'remote_governance':'NOT_APPLICABLE_GENESIS', 'product_code_coverage':'NOT_APPLICABLE_NO_PRODUCT_CODE', + 'in_repository_validation_command':'NOT_QUALIFIED_AFTER_GIT_METADATA', + 'effect_result_semantics':'ORIGINAL_OPERATION_AS_OF_RECORDED_STEP_EVIDENCE'} + + +class GenesisApplyHttpTests(unittest.TestCase): + def setUp(self): + self.owner = bootstrap_tests.GenesisBootstrapHttpTest(); self.addCleanup(self.owner.doCleanups); self.owner.setUp() + self.owner.approve_content(); _, view = self.owner.prepare(); self.owner.effect_grant(view) + status, self.view = self.owner.call('POST', bootstrap_tests.BASE+'/decisions', self.owner.effect(view), token='effect') + self.assertEqual(status, 200, self.view) + self.grant = GenesisApplyGrant(self.owner.root/'runtime', self.owner.setup['instance_id']); self.grant.initialize() + plan = self.view['plan']; decision = self.view['original_decision'] + self.request = dict(contract_version=CONTRACT, instance_id=plan['instance_id'], dossier_id=plan['dossier_id'], + operation_id='original-apply', plan_id=plan['plan_id'], plan_digest=plan['plan_digest'], + decision_id=decision['decision_id'], decision_digest=decision['decision_digest'], pairing_id='owner-pair') + from forge.genesis_bootstrap_service import GenesisBootstrapService + with self.owner.grant.boundary(): + source_principal = self.owner.grant.authorize("Bearer "+(self.owner.root/"bootstrap.private").read_text().strip()) + original, _, _ = GenesisBootstrapService(self.owner.grant).source(source_principal, plan["dossier_id"], plan["source_subject"]["plan_id"]) + self.ep, _ = ep_request(original, plan, decision, self.request['operation_id']) + pins = {'producer_revision':'a'*40, 'producer_tree':'b'*40, 'producer_schema_sha256':digest(files('forge').joinpath('api/ep-genesis-local-bootstrap-v1.schema.json').read_bytes()), 'producer_version':'2.3.123'} + self.pin = patch('forge.genesis_apply_peer.QUALIFIED_PRODUCER', pins); self.pin.start(); self.addCleanup(self.pin.stop) + self.pairing = {**pins, 'pairing_id':'owner-pair', 'endpoint':'http://127.0.0.1:12345', + 'binding':{'instance_id':'ep-test','host_id':'c'*64,'consumer_id':'forge-test','principal':'test-owner','operation_id':'original-apply'}, + 'ep_grant_id':'separate-ep-grant', 'expires_at':self.owner.expiry, + **{k:self.request[k] for k in ('plan_id','plan_digest','decision_id','decision_digest')}, + 'placement_ref':'placement-test','target_ref':'new-test','request_digest':digest(canonical(self.ep)), + 'input_digest':plan['source_subject']['input_digest']} + credential = self.owner.root/'separate-ep.private'; credential.write_text('independent-synthetic-ep-token'); credential.chmod(0o600) + GenesisApplyPairings(self.grant).bind(self.pairing, credential) + self.issued = self.grant.issue(principal_id=self.owner.setup['human_principal_id'], audience='synthetic-workspace', + proposal_principal_id='synthetic-service', dossier_ids=['synthetic-dossier'], source_plan_ids=[self.owner.source['plan_id']], + permissions=['READ','REQUEST_APPLY','RESUME'], exact_plans=[{'plan_id':plan['plan_id'],'plan_digest':plan['plan_digest']}], + expires_at=self.owner.expiry, token_path=self.owner.root/'apply.private') + self.reply = response_for(self.pairing, self.ep, plan) + self.peer_original = None; self.posts = 0; self.fail_get = False; self.lose_post = False + self.revoke_before_send = False; self.deny_publication = False + self.peer_patch = patch('forge.genesis_apply_service.exchange', side_effect=self.exchange) + self.peer_patch.start(); self.addCleanup(self.peer_patch.stop) + + def exchange(self, pairing, method, *, body=None, resume=False, deadline=None, before_send=None): + # This explicit test seam never substitutes for installed EP evidence. + if method == 'POST' and self.revoke_before_send: + self.grant.revoke(self.issued['grant_id']) + if before_send is not None: + import time + before_send(time.monotonic()+30) + if method == 'GET': + if self.deny_publication and self.peer_original is not None: raise PeerError('EP_GENESIS_HTTP_401',424) + if self.fail_get: raise PeerError('EP_GENESIS_TRANSPORT_UNCERTAIN') + if self.peer_original is None: raise PeerError('EP_GENESIS_HTTP_404',424) + return canonical(self.peer_original) + self.posts += 1; self.peer_original = deepcopy(self.reply) + if self.lose_post: raise PeerError('EP_GENESIS_TRANSPORT_UNCERTAIN') + return canonical(self.peer_original) + + def call(self, method='POST', path=None, body=None, token='apply'): + from urllib.request import Request, urlopen + from urllib.error import HTTPError + import json + from jsonschema import Draft202012Validator + value = self.request if body is None and method == 'POST' else body + request = Request('http://127.0.0.1:'+str(self.owner.owner.server.server_port)+(path or BASE+'/operations'), + method=method, data=canonical(value) if value is not None else None, + headers={'Authorization':'Bearer '+(self.owner.root/(token+'.private')).read_text().strip(), + 'X-Forge-Genesis-Apply-Audience':'synthetic-workspace','Content-Type':'application/json'}) + try: + with urlopen(request,timeout=15) as response: status, answer = response.status,json.loads(response.read()) + except HTTPError as error: + try: status,answer = error.code,json.loads(error.read()) + finally: error.close() + if answer.get('contract_version') == CONTRACT: + schema = load_json(files('forge').joinpath('api/genesis-apply-v1.json').read_bytes()) + Draft202012Validator(schema).validate(answer) + if status == 200 and value is not None: Draft202012Validator(schema).validate(value) + return status,answer + + def test_http_original_replay_read_no_second_post_and_cross_purpose_denial(self): + owning_files=[self.owner.grant.store,self.owner.owner.grant.store,self.owner.grant.root/'onboarding/proposals/store.json'] + owning_before=[path.read_bytes() for path in owning_files] + status, one = self.call(); self.assertEqual(status,200,one) + self.assertEqual(one['state'],'COMPLETE'); self.assertEqual(one['project_readiness'],'NOT_ASSESSED') + status, two = self.call(); self.assertEqual(status,200,two); self.assertEqual(one,two) + status, read = self.call('GET', BASE+'/operations/original-apply?dossier_id=synthetic-dossier') + self.assertEqual(status,200,read); self.assertEqual(one,read); self.assertEqual(self.posts,1) + self.assertEqual(self.call(token='effect')[0],403) + self.assertEqual(self.call('GET',bootstrap_tests.BASE+'/capability')[0],403) + self.assertEqual(self.call('GET',BASE+'/operations/original-apply?dossier_id=foreign')[0],403) + self.assertEqual([path.read_bytes() for path in owning_files],owning_before) + + def test_lost_accepted_reply_recreated_service_recovers_original_no_second_create(self): + self.lose_post = True + status, lost = self.call(); self.assertEqual(status,503,lost); self.assertEqual(self.posts,1) + # Recreated service reads sealed original journal, not any process-local flag. + service = GenesisApplyService(GenesisApplyGrant(self.grant.root,self.grant.instance_id)) + auth = 'Bearer '+(self.owner.root/'apply.private').read_text().strip() + answer = service.submit(auth,'synthetic-workspace',self.request) + self.assertEqual(answer['original_ep_operation'],self.reply); self.assertEqual(self.posts,1) + + def test_transient_failure_after_prepare_before_claim_can_recover_first_send(self): + self.fail_get = True + status, lost = self.call(); self.assertEqual(status,503,lost); self.assertEqual(self.posts,0) + self.assertEqual(GenesisApplyService(self.grant).journal.store()['records']['original-apply']['attempts'],[]) + self.fail_get = False + status, done = self.call(); self.assertEqual(status,200,done); self.assertEqual(self.posts,1) + self.assertEqual(len(GenesisApplyService(self.grant).journal.store()['records']),1) + + def test_current_decision_or_local_pairing_revoke_stops_effect_and_disclosure(self): + GenesisApplyPairings(self.grant).revoke('owner-pair') + status, refused = self.call(); self.assertEqual(status,403,refused); self.assertEqual(self.posts,0) + + def test_original_applygrant_revoked_cannot_be_replaced_for_effect_resume(self): + self.reply = response_for(self.pairing,self.ep,self.view['plan'],'PARTIAL') + status, first = self.call(); self.assertEqual(status,200,first) + self.grant.revoke(self.issued['grant_id']) + self.grant.issue(principal_id=self.owner.setup['human_principal_id'], audience='synthetic-workspace', + proposal_principal_id='synthetic-service', dossier_ids=['synthetic-dossier'],source_plan_ids=[self.owner.source['plan_id']], + permissions=['READ','RESUME'],exact_plans=[{'plan_id':self.view['plan']['plan_id'],'plan_digest':self.view['plan']['plan_digest']}], + expires_at=self.owner.expiry,token_path=self.owner.root/'replacement.private') + request = dict(contract_version=CONTRACT,instance_id=self.grant.instance_id,dossier_id='synthetic-dossier', + operation_id='original-apply',attempt_id='explicit-resume',original_request_digest=first['original_request_digest']) + status, refused = self.call('POST',BASE+'/operations/original-apply/resume',request,token='replacement') + self.assertEqual(status,403,refused);self.assertEqual(self.posts,1) + status, historical = self.call('GET',BASE+'/operations/original-apply?dossier_id=synthetic-dossier',token='replacement') + self.assertEqual(status,200,historical);self.assertEqual(historical['state'],'PARTIAL');self.assertEqual(self.posts,1) + + def test_current_content_and_effect_revoke_and_changed_binding_deny_before_post(self): + for update in ({'plan_digest':'f'*64},{'decision_digest':'f'*64},{'dossier_id':'foreign'},{'instance_id':'foreign'}): + status, refused = self.call(body={**self.request,**update}) + self.assertIn(status,(403,409),refused) + self.assertEqual(self.posts,0) + self.owner.grant.revoke(next(r['grant_id'] for r in self.owner.grant._records() if 'DECIDE_EFFECT' in r['permissions'])) + status, refused = self.call();self.assertEqual(status,409,refused);self.assertEqual(self.posts,0) + + def test_cli_private_request_original_read_and_late_failure_no_second_json(self): + import io,json + from forge.genesis_apply_cli import main + request_file = self.owner.root/'apply-request.private';request_file.write_bytes(canonical(self.request));request_file.chmod(0o600) + common = ['--data-root',str(self.grant.root)] + auth = ['--token-file',str(self.owner.root/'apply.private'),'--audience','synthetic-workspace'] + def cli(args): + output=io.StringIO() + with patch('sys.stdout',output): code=main([*common,*args]) + return code,output.getvalue() + code,raw=cli(['request',*auth,'--request-file',str(request_file)]) + self.assertEqual(code,0,raw);first=json.loads(raw);self.assertEqual(self.posts,1) + code,raw=cli(['read',*auth,'--operation-id','original-apply','--dossier-id','synthetic-dossier']) + self.assertEqual(code,0,raw);self.assertEqual(json.loads(raw),first);self.assertEqual(self.posts,1) + def interrupted(*args,**kwargs): + print('{"partial":',end='');raise PermissionError('revoked after first chunk') + with patch('forge.genesis_apply_cli.publish_cli',side_effect=interrupted): + code,raw=cli(['read',*auth,'--operation-id','original-apply','--dossier-id','synthetic-dossier']) + self.assertEqual(code,1);self.assertEqual(raw,'{"partial":') + + def test_revoke_during_connect_is_rechecked_before_effect_send(self): + self.revoke_before_send=True + status,refused=self.call();self.assertEqual(status,403,refused);self.assertEqual(self.posts,0) + original=GenesisApplyService(self.grant).journal.store()['records']['original-apply'] + self.assertEqual(len(original['attempts']),1);self.assertIsNone(original['observation']) + + def test_peer_revoke_before_publication_withholds_original_success(self): + self.deny_publication=True + status,refused=self.call();self.assertEqual(status,403,refused);self.assertEqual(self.posts,1) + self.assertNotIn('original_ep_operation',refused) diff --git a/tests/test_genesis_apply_journal.py b/tests/test_genesis_apply_journal.py new file mode 100644 index 0000000..08ffb0e --- /dev/null +++ b/tests/test_genesis_apply_journal.py @@ -0,0 +1,77 @@ +"""Durable intent rules; a memory store is not installed EP evidence.""" +from copy import deepcopy +from types import SimpleNamespace +from unittest import TestCase +from forge.genesis_apply_contract import CONTRACT, ep_request +from forge.genesis_apply_journal import GenesisDispatchJournal +from tests.test_genesis_apply_contract import dispatch_fixture + + +class MemoryStore: + def __init__(self): self.value = {'records': {}} + def _store(self): return deepcopy(self.value) + def save_store(self, value): self.value = deepcopy(value) + + +class GenesisJournalTests(TestCase): + def setUp(self): + self.store = MemoryStore(); self.journal = GenesisDispatchJournal(self.store) + original, plan, decision = dispatch_fixture() + self.request = dict(contract_version=CONTRACT, instance_id=plan['instance_id'], dossier_id=plan['dossier_id'], + operation_id='original-operation', plan_id=plan['plan_id'], plan_digest=plan['plan_digest'], + decision_id=decision['decision_id'], decision_digest=decision['decision_digest'], pairing_id='owner-pairing') + self.ep, _ = ep_request(original, plan, decision, 'original-operation') + self.principal = SimpleNamespace(principal_id='requester', audience='consumer', grant_id='grant') + + def prepare(self, **updates): + return self.journal.prepare({**self.request, **updates}, self.principal, 'a'*64, self.ep) + + def test_original_intent_and_send_claim_survive_recreated_coordinator(self): + one, new = self.prepare(); self.assertTrue(new); self.assertEqual(one['attempts'], []) + claim, send = self.journal.claim('original-operation', attempt_id='original-create') + self.assertTrue(send) + self.journal = GenesisDispatchJournal(self.store) + replay, new = self.prepare(); self.assertFalse(new) + self.assertEqual(replay['ep_request'], self.ep) + self.assertEqual(self.journal.claim('original-operation', attempt_id='original-create'), (claim, False)) + self.assertEqual(self.journal.claim('original-operation', attempt_id='different-create'), (claim, False)) + self.assertEqual(len(self.store.value['records']), 1) + + def test_conflicting_input_or_principal_or_ep_bytes_cannot_rebind_original(self): + self.prepare() + for choice in ('plan', 'principal', 'ep'): + with self.subTest(choice=choice): + journal = GenesisDispatchJournal(self.store) + p = deepcopy(self.principal); ep = deepcopy(self.ep); request = deepcopy(self.request) + if choice == 'plan': request['plan_digest'] = 'f'*64 + elif choice == 'principal': p.principal_id = 'foreign' + else: ep['target_ref'] = 'other' + with self.assertRaisesRegex(ValueError, 'GENESIS_ORIGINAL_INTENT_CONFLICT'): + journal.prepare(request, p, 'a'*64, ep) + + def test_explicit_resume_fences_late_response_and_replay_never_resends(self): + self.prepare() + with self.assertRaisesRegex(ValueError, 'GENESIS_ORIGINAL_ADMISSION_REQUIRED'): + self.journal.claim('original-operation', attempt_id='resume', resume=True) + first, _ = self.journal.claim('original-operation', attempt_id='create') + second, new = self.journal.claim('original-operation', attempt_id='resume', resume=True) + self.assertTrue(new); self.assertNotEqual(first, second) + self.assertEqual(self.journal.claim('original-operation', attempt_id='resume', resume=True), (second, False)) + with self.assertRaisesRegex(ValueError, 'GENESIS_ATTEMPT_KIND_CONFLICT'): + self.journal.claim('original-operation', attempt_id='resume') + with self.assertRaisesRegex(ValueError, 'GENESIS_LATE_RESPONSE_FENCED'): + self.journal.observe('original-operation', first, {'state': 'COMPLETE'}) + result = self.journal.observe('original-operation', second, {'state': 'PARTIAL'}) + self.assertEqual(result['observation']['response']['state'], 'PARTIAL') + self.assertEqual(self.journal.store()['records']['original-operation'], result) + + def test_corrupt_original_is_unavailable_not_empty(self): + self.prepare(); original = deepcopy(self.store.value) + for choice in ('fields', 'digest', 'ep', 'attempt', 'observation'): + self.store.value = deepcopy(original); row = self.store.value['records']['original-operation'] + if choice == 'fields': row['extra'] = True + elif choice == 'digest': row['request_digest'] = '0'*64 + elif choice == 'ep': row['ep_request']['operation_id'] = 'other' + elif choice == 'attempt': row['attempts'] = [{'attempt_id': 'a', 'kind': 'RESUME', 'claim': 'claim'}] + else: row['observation'] = {'claim': 'unknown', 'response': {}, 'response_digest': '0'*64} + with self.subTest(choice=choice), self.assertRaisesRegex(RuntimeError, 'unavailable'): self.journal.store() diff --git a/tests/test_genesis_apply_peer.py b/tests/test_genesis_apply_peer.py new file mode 100644 index 0000000..af9d58b --- /dev/null +++ b/tests/test_genesis_apply_peer.py @@ -0,0 +1,91 @@ +"""Causal transport boundary tests; fake sockets are not installed EP evidence.""" +from types import SimpleNamespace +from unittest import TestCase +from unittest.mock import MagicMock, patch +from forge.genesis_apply_peer import exchange +from forge.ep_inspection_peer import PeerError +from forge.project_bootstrap.schema import canonical +from forge.project_bootstrap.service import digest + + +class GenesisPeerDeadlineTests(TestCase): + def setUp(self): + self.clock = 0 + self.request = {'operation_id':'original'} + self.pairing = {'endpoint':'http://127.0.0.1:8765', 'credential':'synthetic-private', + 'request_digest':digest(canonical(self.request)), 'binding':{'operation_id':'original', + 'instance_id':'ep-test','host_id':'a'*64,'consumer_id':'forge','principal':'owner'}} + self.connection = MagicMock(); self.response = MagicMock(status=200) + self.connection.getresponse.return_value = self.response + self.response.read.side_effect = [b'{}\n',b''] + self.factory = patch('forge.genesis_apply_peer.http.client.HTTPConnection', return_value=self.connection) + self.factory.start(); self.addCleanup(self.factory.stop) + self.time = patch('forge.genesis_apply_peer.time.monotonic', side_effect=lambda:self.clock) + self.time.start(); self.addCleanup(self.time.stop) + self.timer = patch('forge.genesis_apply_peer.Timer'); self.timer.start(); self.addCleanup(self.timer.stop) + + def test_late_connect_does_not_send_effect(self): + self.connection.connect.side_effect = lambda:setattr(self,'clock',31) + with self.assertRaisesRegex(PeerError,'DEADLINE_EXCEEDED'): + exchange(self.pairing,'POST',body=self.request,before_send=lambda _:None) + self.connection.request.assert_not_called() + self.connection.close.assert_called_once() + + def test_late_complete_response_is_not_success_even_if_watchdog_has_not_run(self): + def read(_): + self.clock = 31 + return b'{}\n' if self.response.read.call_count == 1 else b'' + self.response.read.side_effect = read + with self.assertRaisesRegex(PeerError,'DEADLINE_EXCEEDED'): + exchange(self.pairing,'GET') + self.connection.request.assert_called_once() + + def test_current_authority_failure_after_connect_sends_no_post(self): + def revoked(_): raise PermissionError('current grant revoked') + with self.assertRaises(PermissionError): + exchange(self.pairing,'POST',body=self.request,before_send=revoked) + self.connection.connect.assert_called_once(); self.connection.request.assert_not_called() + self.connection.close.assert_called_once() + + def test_post_without_current_authority_callback_or_rebound_request_is_refused(self): + with self.assertRaises(ValueError): exchange(self.pairing,'POST',body=self.request) + with self.assertRaises(ValueError): exchange(self.pairing,'POST',body={'operation_id':'other'},before_send=lambda _:None) + self.connection.request.assert_not_called() + + def test_http_refusal_is_typed_and_never_redirected_or_retried(self): + self.response.status = 302 + with self.assertRaisesRegex(PeerError,'HTTP_302'): + exchange(self.pairing,'GET') + self.connection.request.assert_called_once() + + def test_one_explicit_resume_uses_original_path_no_new_body(self): + result = exchange(self.pairing,'POST',resume=True,before_send=lambda _:None) + self.assertEqual(result,b'{}\n') + args, kwargs = self.connection.request.call_args + self.assertEqual(args,('POST','/v1/bootstrap/genesis/operations/original/resume')) + self.assertIsNone(kwargs['body']) + self.assertEqual(kwargs['headers']['EP-Audience'],'genesis-bootstrap') + self.assertEqual(self.connection.request.call_count,1) + + def test_actual_http_resume_obeys_owning_zero_length_body_guard(self): + from http.server import BaseHTTPRequestHandler,HTTPServer + from threading import Thread + observed={} + class StrictSyntheticResume(BaseHTTPRequestHandler): + def do_POST(self): + observed.update(path=self.path,length=self.headers.get('Content-Length')) + raw=b'{"synthetic_original":true}\n' + self.send_response(200 if observed['length']=='0' else 400) + self.send_header('Content-Length',str(len(raw)));self.end_headers();self.wfile.write(raw) + def log_message(self,*args):pass + server=HTTPServer(('127.0.0.1',0),StrictSyntheticResume) + thread=Thread(target=server.serve_forever,daemon=True);thread.start() + try: + pairing={**self.pairing,'endpoint':'http://127.0.0.1:'+str(server.server_port)} + # Use actual stdlib connection/clock, not the fixture transport mocks. + self.factory.stop();self.time.stop();self.timer.stop() + raw=exchange(pairing,'POST',resume=True,before_send=lambda _:None) + self.assertEqual(raw,b'{"synthetic_original":true}\n') + self.assertEqual(observed,{'path':'/v1/bootstrap/genesis/operations/original/resume','length':'0'}) + finally: + server.shutdown();thread.join(3);server.server_close() diff --git a/tests/test_genesis_apply_qualification.py b/tests/test_genesis_apply_qualification.py new file mode 100644 index 0000000..88be7c2 --- /dev/null +++ b/tests/test_genesis_apply_qualification.py @@ -0,0 +1,296 @@ +"""First-chain evidence guard regressions; no actual EP grant/server/effects.""" +from copy import deepcopy +from hashlib import sha256 +from importlib.util import spec_from_file_location,module_from_spec +import json +from pathlib import Path +import subprocess +from tempfile import TemporaryDirectory +from types import SimpleNamespace +from unittest import TestCase +from unittest.mock import patch +import zipfile +from tests.test_genesis_apply_contract import dispatch_fixture +from forge.genesis_apply_contract import ep_request + +ROOT=Path(__file__).resolve().parents[1] +spec=spec_from_file_location('r57_first_chain',ROOT/'scripts/qualification/qualify_installed_genesis_apply.py') +Q=module_from_spec(spec);spec.loader.exec_module(Q) + + +class FirstChainEvidenceGuards(TestCase): + def test_missing_qualified_pin_initializes_no_peer_or_scratch_and_retains_failure(self): + with TemporaryDirectory() as temp: + root=Path(temp);args=SimpleNamespace(output_dir=root/'evidence',scratch_root=root/'untouched',python=Path('/unused'),wheel=root/'unused.whl') + with patch.object(Q,'probe',return_value=None),patch.object(Q,'EPServer') as peer: + with self.assertRaisesRegex(ValueError,'qualified producer'):Q.run(args) + peer.assert_not_called() + report=json.loads((args.output_dir/'first-real-installed-chain.public.json').read_bytes()) + self.assertEqual(report['result'],'FAIL');self.assertTrue(report['cleanup']);self.assertFalse(args.scratch_root.exists()) + + def test_primary_pin_refusal_survives_secondary_receipt_write_error(self): + with TemporaryDirectory() as temp: + root=Path(temp);args=SimpleNamespace(output_dir=root/'evidence',scratch_root=root/'untouched',python=Path('/unused'),wheel=root/'unused.whl') + with patch.object(Q,'probe',return_value=None),patch.object(Q,'private',side_effect=OSError('sink unavailable')): + with self.assertRaisesRegex(ValueError,'qualified producer') as observed:Q.run(args) + self.assertTrue(any('receipt retention failed' in note for note in observed.exception.__notes__)) + + def test_ep_owner_nonzero_preserved_when_report_retention_also_fails(self): + server=Q.EPServer.__new__(Q.EPServer);server.args=SimpleNamespace(ep_python=Path('/unused')) + server.root=Path('/unused');server.output=Path('/unused');server.data=Path('/unused/s');server.env={};server.command_counter=0 + report={'primary_failure':{'category':'COMMAND_NONZERO','observed_exit':7},'capture_complete':False,'actual_exit':7} + with patch.object(Q.C.BINDING.CAPTURE,'capture',return_value=(report,b'',b'diagnostic')),patch.object(Q.C.BINDING.CAPTURE,'retain',side_effect=OSError('sink unavailable')): + with self.assertRaises(RuntimeError) as observed:server.command('genesis_cli','issue') + self.assertEqual(observed.exception.capture_report['primary_failure']['observed_exit'],7) + self.assertTrue(any('report retention failed' in note for note in observed.exception.__notes__)) + + def test_b8r_must_match_actual_original_commit_declaration_and_physical_target(self): + original,plan,decision=dispatch_fixture();request,_=ep_request(original,plan,decision,'original') + declaration=json.loads(request['bundle']['files']['.engineering-platform/repository.json']['utf8']) + ids=plan['documents']['identity'];operation={'commit':'a'*40,'tree':'b'*40};target=Path('/private/tmp/synthetic-target') + surface={**declaration,'validation':{**declaration['validation'],'description':None},'requirements':{'host':{},'tools':{}},'integrations':{}} + rows={'ep_genesis_operations':[dict(operation_id='original',grant_id='grant',request_digest=sha256(Q.canonical(request)).hexdigest(),original_request=json.dumps(request),state='COMPLETE',project_id=ids['project_id'],repository_id=ids['repository_id'],evidence=json.dumps({'known':dict(commit_ref=operation['commit'],tree=operation['tree'],committed_declaration_verified=True)}))], + 'ep_project_registrations':[dict(project_id=ids['project_id'],status='ACTIVE',attachment_contract=json.dumps(dict(schema_version='1.0',authority_repository_id=ids['authority_repository_id'])))], + 'ep_repository_registrations':[dict(**ids,role='authority',attachment_contract=json.dumps(surface))], + 'ep_local_repository_bindings':[dict(project_id=ids['project_id'],repository_id=ids['repository_id'],local_root=str(target),state='BOUND')]} + self.assertTrue(Q.verify_b8r(rows,request,operation,plan['documents'],target,'grant')['committed_registration_and_target_binding_verified']) + for choice in ('target','declaration','authority','commit','grant'): + changed=deepcopy(rows) + if choice=='target':changed['ep_local_repository_bindings'][0]['local_root']='/private/tmp/foreign' + elif choice=='declaration':changed['ep_repository_registrations'][0]['attachment_contract']='{}' + elif choice=='authority':changed['ep_repository_registrations'][0]['authority_repository_id']='foreign' + elif choice=='commit':changed['ep_genesis_operations'][0]['evidence']=json.dumps({'known':dict(commit_ref='f'*40,tree=operation['tree'],committed_declaration_verified=True)}) + else:changed['ep_genesis_operations'][0]['grant_id']='foreign' + with self.subTest(choice=choice),self.assertRaises(ValueError):Q.verify_b8r(changed,request,operation,plan['documents'],target,'grant') + + def test_supplied_wheel_hash_cannot_substitute_changed_source_or_extra_installed_file(self): + # Tiny synthetic Git/package fixture, not an EP release or qualification. + with TemporaryDirectory() as temp: + root=Path(temp);source=root/'source';source.mkdir();package=source/'src/engineering_platform';(package/'schemas').mkdir(parents=True) + (package/'__init__.py').write_text('VALUE=1\n');(package/'schemas/genesis-local-bootstrap-v1.schema.json').write_text('{}\n') + (source/'pyproject.toml').write_text('[project]\nname="engineering-platform"\nversion="2.3.123"\n[project.scripts]\nep="engineering_platform:main"\n[tool.setuptools.package-data]\nengineering_platform=["schemas/*.json"]\n') + def git(*args):return subprocess.check_output(['git','-C',str(source),*args],stderr=subprocess.DEVNULL) + git('init');git('add','.');git('-c','user.name=Synthetic','-c','user.email=synthetic@example.invalid','commit','-m','synthetic fixture') + revision=git('rev-parse','HEAD').decode().strip();tree=git('rev-parse','HEAD^{tree}').decode().strip() + installed=root/'installed/engineering_platform';(installed/'schemas').mkdir(parents=True) + payload={'engineering_platform/__init__.py':b'VALUE=1\n','engineering_platform/schemas/genesis-local-bootstrap-v1.schema.json':b'{}\n'} + for name,raw in payload.items():(installed.parent/name).write_bytes(raw) + wheel=root/'synthetic.whl' + def build(changed=False): + with zipfile.ZipFile(wheel,'w') as archive: + for name,raw in payload.items():archive.writestr(name,b'VALUE=2\n' if changed and name.endswith('__init__.py') else raw) + archive.writestr('engineering_platform-2.3.123.dist-info/METADATA','Name: engineering-platform\nVersion: 2.3.123\n') + archive.writestr('engineering_platform-2.3.123.dist-info/entry_points.txt','[console_scripts]\nep = engineering_platform:main\n') + args=SimpleNamespace(ep_source_root=source,ep_source_revision=revision,ep_source_tree=tree,ep_wheel=wheel,ep_python=Path('/unused'),producer_pin=dict(producer_tree=tree,producer_version='2.3.123',producer_schema_sha256=sha256(b'{}\n').hexdigest())) + def info():return dict(package=str(installed),version='2.3.123',direct={'archive_info':{'hashes':{'sha256':args.ep_wheel_sha256}}},entries={'ep':'engineering_platform:main'}) + build();args.ep_wheel_sha256=sha256(wheel.read_bytes()).hexdigest() + with patch.object(Q,'probe',side_effect=lambda *a:info()):self.assertTrue(Q.ep_payload(args)['source_payload_entrypoints_exact']) + (installed/'extra.py').write_text('extra=1\n') + with patch.object(Q,'probe',side_effect=lambda *a:info()),self.assertRaisesRegex(ValueError,'inventory'):Q.ep_payload(args) + (installed/'extra.py').unlink();build(changed=True);args.ep_wheel_sha256=sha256(wheel.read_bytes()).hexdigest() + (installed/'__init__.py').write_text('VALUE=2\n') + with patch.object(Q,'probe',side_effect=lambda *a:info()),self.assertRaisesRegex(ValueError,'payload differs'):Q.ep_payload(args) + + def test_owned_session_children_stop_even_after_unreaped_leader_exit(self): + import os,sys,time,signal + if not hasattr(os,'waitid') or not hasattr(os,'WNOWAIT'):self.skipTest('nonreaping POSIX wait unavailable') + helper='''from dataclasses import dataclass +from pathlib import Path +import os,subprocess,hashlib +@dataclass(frozen=True) +class ProcessIdentity: + pid:int + process_group:int + start_fingerprint:str + executable_identity:str +def _canonical_executable_identity(value):return str(Path(value).resolve()) +def capture_process_identity(pid,group=None): + try: + actual=os.getpgid(pid) + if group is not None and actual!=group:return None + text=subprocess.check_output(['ps','-o','lstart=','-o','comm=','-p',str(pid)],text=True,stderr=subprocess.DEVNULL).strip().split(maxsplit=5) + if len(text)!=6:return None + birth=' '.join(text[:5]) + return ProcessIdentity(pid,actual,hashlib.sha256(f'{pid}|{actual}|{birth}'.encode()).hexdigest(),_canonical_executable_identity(text[5][:512])) + except (OSError,subprocess.SubprocessError):return None +def verify_process_identity(expected): + actual=capture_process_identity(expected.pid,expected.process_group) + return 'NOT_ACTIVE' if actual is None else 'MATCH' if actual==expected else 'MISMATCH' +''' + with TemporaryDirectory() as temp: + root=Path(temp);(root/'provider_process_identity.py').write_text(helper);marker=root/'child.pid' + code="import subprocess,sys;from pathlib import Path;p=subprocess.Popen([sys.executable,'-I','-c','import time;time.sleep(30)']);Path(sys.argv[1]).write_text(str(p.pid))" + parent=subprocess.Popen([sys.executable,'-I','-c',code,str(marker)],start_new_session=True) + server=Q.EPServer.__new__(Q.EPServer);server.args=SimpleNamespace(ep_package=root);server.process=parent;server.log=None + server.facts=lambda:({},[],{}) + try: + deadline=time.monotonic()+5 + while not marker.exists() or os.waitid(os.P_PID,parent.pid,os.WEXITED|os.WNOHANG|os.WNOWAIT) is None: + if time.monotonic()>deadline:raise TimeoutError('own fixture leader exit') + time.sleep(.01) + child=int(marker.read_text());os.kill(child,0) + server.stop() + self.assertIsNotNone(parent.returncode) + with self.assertRaises(ProcessLookupError):os.killpg(parent.pid,0) + finally: + if parent.returncode is None: + try:os.killpg(parent.pid,signal.SIGKILL) + except ProcessLookupError:pass + parent.wait(timeout=5) + + def test_outer_cleanup_distinguishes_exited_zombies_from_live_or_unknown_workers(self): + evidence=[{'attempts':[{'process':{'process_group':12345}}]}] + with patch.object(Q.os,'killpg',side_effect=PermissionError('exited group')),patch.object(Q,'group_active',return_value=False): + Q.require_worker_groups_exited(evidence) + with patch.object(Q.os,'killpg',return_value=None),patch.object(Q,'group_active',return_value=True): + with self.assertRaisesRegex(RuntimeError,'active or uncertain'):Q.require_worker_groups_exited(evidence) + with patch.object(Q.os,'killpg',side_effect=PermissionError('live group')),patch.object(Q,'group_active',return_value=True): + with self.assertRaises(PermissionError):Q.require_worker_groups_exited(evidence) + + def test_expected_negative_completion_cannot_hide_final_receipt_retention_failure(self): + # Exercise actual run() exception/cleanup boundary, not an EP outcome. + with TemporaryDirectory() as temp: + root=Path(temp);args=SimpleNamespace(output_dir=root/'evidence',scratch_root=root/'untouched',python=Path('/unused'),wheel=root/'unused.whl') + with patch.object(Q,'probe',side_effect=Q.ExpectedNegativeComplete),patch.object(Q,'private',side_effect=OSError('deliberate receipt failure')): + with self.assertRaisesRegex(OSError,'deliberate receipt failure'):Q.run(args) + self.assertFalse(args.scratch_root.exists()) + + def test_expected_negative_completion_cannot_hide_own_cleanup_identity_failure(self): + import os + with TemporaryDirectory() as temp: + root=Path(temp);scratch=root/'own-scratch';scratch.mkdir() + (scratch/'.r57-owner.json').write_text(json.dumps({'uid':os.getuid(),'identity':[-1,-1]})) + args=SimpleNamespace(output_dir=root/'evidence',scratch_root=scratch,python=Path('/unused'),wheel=root/'unused.whl') + with patch.object(Q,'probe',side_effect=Q.ExpectedNegativeComplete): + with self.assertRaisesRegex(RuntimeError,'own scratch identity changed'):Q.run(args) + report=json.loads((args.output_dir/'first-real-installed-chain.public.json').read_bytes()) + self.assertEqual(report['result'],'FAIL');self.assertFalse(report['cleanup']);self.assertTrue(scratch.exists()) + + def test_expected_negative_context_cannot_discard_original_server_cleanup_failure(self): + with TemporaryDirectory() as temp: + root=Path(temp);args=SimpleNamespace(output_dir=root/'evidence',scratch_root=root/'untouched',python=Path('/unused'),wheel=root/'unused.whl') + completed=Q.ExpectedNegativeComplete();completed.add_note('owned Server cleanup incomplete: deliberately failed wait') + with patch.object(Q,'probe',side_effect=completed): + with self.assertRaisesRegex(RuntimeError,'unresolved Server cleanup failure') as raised:Q.run(args) + self.assertIn('deliberately failed wait',' '.join(raised.exception.__notes__)) + report=json.loads((args.output_dir/'first-real-installed-chain.public.json').read_bytes()) + self.assertEqual(report['result'],'FAIL');self.assertEqual(report['failure']['stage'],'negative-context-cleanup') + + def test_failure_diagnostics_preserve_only_closed_public_enums_and_numeric_counts(self): + row={'state':'PARTIAL'};evidence={'completed_steps':['CREATE_TARGET','/private/secret'],'last_error':'BOOTSTRAP_HOST_STEP_FAILED', + 'attempts':[{'step':'CREATE_TARGET','exit_code':7,'process':{'credential':'NEVER_PUBLIC'}}]} + temp=TemporaryDirectory();self.addCleanup(temp.cleanup);data=Path(temp.name) + (data/'g/evidence').mkdir(parents=True) + Q.private(data/'g/evidence/r57-primary-error.private.json',{'type':'FileNotFoundError','errno':2,'detail':'NEVER_PUBLIC', + 'primary':{'type':'BootstrapError','code':'BOOTSTRAP_HOST_GROUP_ACTIVE_OR_UNCERTAIN','detail':'NEVER_PUBLIC','frames':[]}, + 'cleanup_errors':[{'type':'PermissionError','errno':1,'detail':'NEVER_PUBLIC','frames':[]}], + 'frames':[{'file':'genesis_service.py','function':'_execute_step','line':343},{'file':'/private/secret','function':'NEVER_PUBLIC','line':1}]}) + ep=SimpleNamespace(data=data,facts=lambda:({'ep_genesis_operations':1},[evidence],{'ep_genesis_operations':[row]})) + value=Q.failure_diagnostics(ep,{'error':{'code':'EP_GENESIS_HTTP_409','message':'NEVER_PUBLIC'}}) + self.assertEqual(value['consumer_error_code'],'EP_GENESIS_HTTP_409') + self.assertEqual(value['owning_exception']['type'],'FileNotFoundError');self.assertEqual(value['owning_exception']['errno'],2) + self.assertEqual(value['owning_exception']['primary']['code'],'BOOTSTRAP_HOST_GROUP_ACTIVE_OR_UNCERTAIN') + self.assertEqual(value['owning_exception']['cleanup_errors'][0]['errno'],1) + self.assertEqual(value['producer_operations'][0]['state'],'PARTIAL') + self.assertEqual(value['producer_operations'][0]['last_error'],'BOOTSTRAP_HOST_STEP_FAILED') + self.assertEqual(value['producer_operations'][0]['attempts'][0]['exit_code'],7) + raw=json.dumps(value);self.assertNotIn('NEVER_PUBLIC',raw);self.assertNotIn('/private/secret',raw) + self.assertIsNone(Q.failure_diagnostics(None,{'error':{'code':'/private/secret'}})['consumer_error_code']) + + def test_failed_diagnostic_retention_cannot_replace_original_qualification_error(self): + with TemporaryDirectory() as temp: + root=Path(temp);args=SimpleNamespace(output_dir=root/'evidence',scratch_root=root/'untouched',python=Path('/unused'),wheel=root/'unused.whl') + with patch.object(Q,'probe',side_effect=OSError('original qualification failure')),patch.object(Q,'failure_diagnostics',side_effect=RuntimeError('secondary diagnostic failure')): + with self.assertRaisesRegex(OSError,'original qualification failure') as raised:Q.run(args) + self.assertIn('RuntimeError',' '.join(raised.exception.__notes__)) + report=json.loads((args.output_dir/'first-real-installed-chain.public.json').read_bytes()) + self.assertEqual(report['result'],'FAIL');self.assertEqual(report['failure']['category'],'OSError') + + def test_owning_ledger_restoration_never_overwrites_links_or_replaces_primary_failure(self): + import os + with TemporaryDirectory() as temp: + root=Path(temp);ledger=root/'db';retained=root/'original';destination=root/'rejected' + retained.write_bytes(b'exact original ledger');info=retained.stat();identity=(info.st_dev,info.st_ino) + ledger.symlink_to(root/'absent') + with self.assertRaisesRegex(RuntimeError,'ownership differs'):Q.restore_owning_ledger(ledger,retained,destination,identity,None) + self.assertTrue(ledger.is_symlink());self.assertEqual(retained.read_bytes(),b'exact original ledger');ledger.unlink() + ledger.write_bytes(b'rejected replacement');destination.symlink_to(root/'absent') + with self.assertRaises(FileExistsError):Q.restore_owning_ledger(ledger,retained,destination,identity,None) + self.assertTrue(destination.is_symlink());self.assertEqual(ledger.read_bytes(),b'rejected replacement') + destination.unlink();ledger.unlink() + primary=OSError('original probe error') + with patch.object(Q,'retain_rejected_ledger',side_effect=ValueError('first retention error')),patch.object(Q.os,'link',side_effect=PermissionError('secondary restore error')): + Q.restore_owning_ledger(ledger,retained,destination,identity,primary) + self.assertIn('ValueError',' '.join(primary.__notes__));self.assertIn('PermissionError',' '.join(primary.__notes__)) + with self.assertRaisesRegex(ValueError,'first retention error') as raised:Q.restore_owning_ledger(ledger,retained,destination,identity,None) + self.assertIn('PermissionError',' '.join(raised.exception.__notes__)) + Q.restore_owning_ledger(ledger,retained,destination,identity,None) + self.assertEqual(ledger.read_bytes(),b'exact original ledger');self.assertEqual(ledger.stat().st_ino,identity[1]);self.assertEqual(ledger.stat().st_nlink,1) + + def test_concurrency_detector_accepts_only_exact_pending_peer_or_transport_failures(self): + spec=spec_from_file_location('r57_actual_matrix_guards',ROOT/'scripts/qualification/genesis_apply_matrix.py') + matrix=module_from_spec(spec);spec.loader.exec_module(matrix) + context=dict(body={'plan_digest':'a'*64},output=Path('/unused'),port=0,forge=Path('/unused')) + statuses=[(503,{'error':{'code':'EP_GENESIS_TRANSPORT_UNCERTAIN'}}),(424,{'error':{'code':'EP_GENESIS_HTTP_404'}}),(409,{'error':{'code':'GENESIS_EXACT_ORIGINAL_DECISION_REQUIRED'}})] + def execute(values): + helpers=SimpleNamespace(retained_call=lambda output,label,*a:values[int(label.rsplit('-',1)[1])]) + return matrix.concurrent_original(helpers,context) + status,_,summary=execute(statuses) + self.assertEqual(status,503);self.assertEqual(summary['original_statuses'],[503,424]) + fenced=list(statuses);fenced[1]=(409,{'error':{'code':'GENESIS_LATE_RESPONSE_FENCED'}}) + self.assertEqual(execute(fenced)[0],503) + for status,code in ((503,'GENESIS_APPLY_STORAGE_UNAVAILABLE'),(424,'EP_GENESIS_HTTP_403'),(409,'GENESIS_ORIGINAL_INTENT_CONFLICT'),(403,'FORBIDDEN')): + changed=list(statuses);changed[1]=(status,{'error':{'code':code}}) + with self.subTest(code=code),self.assertRaises(RuntimeError):execute(changed) + + def test_private_primary_retention_excludes_bearers_and_refuses_all_link_escapes(self): + import os,tarfile + spec=spec_from_file_location('r57_retention_guards',ROOT/'scripts/qualification/genesis_apply_matrix.py') + matrix=module_from_spec(spec);spec.loader.exec_module(matrix) + with TemporaryDirectory() as temp: + base=Path(temp);root=base/'own';target=root/'w/r';target.mkdir(parents=True) + data=root/'s';(data/'g/evidence').mkdir(parents=True) + (target/'README.md').write_bytes(b'exact own document\n');(data/'g/evidence/native.stdout').write_bytes(b'original worker evidence\n') + (data/'epdata.sqlite').write_bytes(b'synthetic retained database bytes') + creds=root/'f/runtime/credentials';creds.mkdir(parents=True);(creds/'token.private').write_bytes(b'NEVER_ARCHIVE_THIS_BEARER') + (root/'ep-token.private').write_bytes(b'NEVER_ARCHIVE_EP_BEARER') + outside=base/'outside';outside.write_bytes(b'NEVER_FOLLOW_SYMLINK_SECRET') + info=root.stat();Q.private(root/'.r57-owner.json',{'uid':os.getuid(),'identity':[info.st_dev,info.st_ino]}) + output=base/'evidence';output.mkdir() + helpers=SimpleNamespace(private=Q.private);ep=SimpleNamespace(target=target,data=data) + result=matrix.retain_owned(helpers,root,ep,output) + self.assertTrue(result['private']);archive=output/result['archive'];self.assertEqual(archive.stat().st_mode&0o777,0o600) + with tarfile.open(archive,'r') as retained: + self.assertEqual(retained.extractfile('ep-target/README.md').read(),b'exact own document\n') + self.assertEqual(retained.extractfile('ep-native-evidence/native.stdout').read(),b'original worker evidence\n') + raw=archive.read_bytes() + for secret in (b'NEVER_ARCHIVE_THIS_BEARER',b'NEVER_ARCHIVE_EP_BEARER',b'NEVER_FOLLOW_SYMLINK_SECRET'):self.assertNotIn(secret,raw) + + for kind in ('leaf','ancestor','hardlink'): + with self.subTest(kind=kind): + attempt=base/('evidence-'+kind);attempt.mkdir() + if kind=='leaf':(target/'link').symlink_to(outside) + elif kind=='hardlink':os.link(outside,target/'link') + else: + (root/'w').rename(root/'saved-w') + foreign=base/'foreign';(foreign/'r').mkdir(parents=True) + (foreign/'r/token.private').write_bytes(b'NEVER_FOLLOW_SYMLINK_SECRET') + (root/'w').symlink_to(foreign,target_is_directory=True) + try: + with self.assertRaises((OSError,RuntimeError)):matrix.retain_owned(helpers,root,ep,attempt) + archive=attempt/'owned-primary-evidence.private.tar' + if archive.exists():self.assertNotIn(b'NEVER_FOLLOW_SYMLINK_SECRET',archive.read_bytes()) + finally: + if kind=='ancestor': + (root/'w').unlink();(root/'saved-w').rename(root/'w') + else:(target/'link').unlink() + + attempt=base/'evidence-disappearing-child';attempt.mkdir() + original_listdir=os.listdir;target_inode=target.stat().st_ino + def remove_listed_child(fd): + names=original_listdir(fd) + if os.fstat(fd).st_ino==target_inode:(target/'README.md').unlink() + return names + with patch.object(os,'listdir',side_effect=remove_listed_child): + with self.assertRaises(FileNotFoundError):matrix.retain_owned(helpers,root,ep,attempt) + self.assertTrue(root.exists()) diff --git a/tests/test_genesis_apply_receipt.py b/tests/test_genesis_apply_receipt.py new file mode 100644 index 0000000..d8356a6 --- /dev/null +++ b/tests/test_genesis_apply_receipt.py @@ -0,0 +1,64 @@ +"""Exact closed producer receipt checks using synthetic, non-authoritative data.""" +from copy import deepcopy +from importlib.resources import files +from unittest import TestCase +from forge.genesis_apply_contract import ep_request +from forge.genesis_apply_peer import receipt +from forge.project_bootstrap.schema import canonical, ContractError +from forge.project_bootstrap.service import digest +from tests.test_genesis_apply_contract import dispatch_fixture +from tests.test_genesis_apply_http import response_for + + +class GenesisReceiptBindingTests(TestCase): + def setUp(self): + original,self.plan,decision=dispatch_fixture() + self.request,_=ep_request(original,self.plan,decision,'original-operation') + self.pairing={'producer_schema_sha256':digest(files('forge').joinpath('api/ep-genesis-local-bootstrap-v1.schema.json').read_bytes()), + 'producer_version':'2.3.123','ep_grant_id':'owner-grant','input_digest':original['result']['input_digest'], + 'binding':{'instance_id':'synthetic-ep','host_id':'a'*64}} + self.value=response_for(self.pairing,self.request,self.plan) + + def read(self,value):return receipt(canonical(value),self.pairing,self.request,self.plan['documents']) + + def test_bound_complete_and_partial_remain_technical_original_evidence(self): + self.assertEqual(self.read(self.value),self.value) + partial=response_for(self.pairing,self.request,self.plan,'PARTIAL') + self.assertEqual(self.read(partial),partial) + self.assertEqual(partial['forge_readiness'],'NOT_OWNED_BY_EP') + + def test_wrong_owning_identity_grant_plan_bundle_or_producer_never_read_as_success(self): + for field in ('operation_id','request_digest','target_ref','authorization_ref','instance_id','host_id','bundle_digest', + 'manifest_digest','input_digest','producer_version','effect_plan_binding','identity'): + value=deepcopy(self.value) + if field=='effect_plan_binding':value[field]['decision_digest']='f'*64 + elif field=='identity':value[field]['repository_id']='foreign' + elif field in ('request_digest','host_id','bundle_digest','manifest_digest','input_digest'):value[field]='f'*64 + else:value[field]='foreign' + with self.subTest(field=field),self.assertRaises(ValueError):self.read(value) + + def test_complete_requires_all_steps_exact_validator_commit_tree_and_registration(self): + for field in ('completed_steps','commit','tree','identity','validation','validator_sha256'): + value=deepcopy(self.value) + if field=='completed_steps':value[field].pop() + elif field=='identity':value[field]['state']='RESERVED' + else:value[field]=None + with self.subTest(field=field),self.assertRaises(ValueError):self.read(value) + for field in ('validator_sha256','validation'): + value=deepcopy(self.value) + if field=='validation':value[field]['manifest_digest']='f'*64 + else:value[field]='f'*64 + with self.subTest(field=field),self.assertRaises(ValueError):self.read(value) + + def test_strict_schema_duplicates_bounds_and_private_fields_are_refused(self): + for choice in ('unknown','duplicate','exit','time','null','oversize','asset'): + value=deepcopy(self.value);pair=deepcopy(self.pairing) + if choice=='unknown':value['private_path']='/private/owner' + elif choice=='duplicate':value['completed_steps'].append(value['completed_steps'][0]) + elif choice=='exit':value['step_effects']=[dict(attempt_id='a'*32,step='CREATE_TARGET',outcome='STEP_COMPLETED',exit_code=256,private_evidence_ref='genesis-attempt:'+'a'*32)] + elif choice=='time':value['observed_at']='not-a-time' + elif choice=='null':value=None + elif choice=='oversize':value['producer_version']='x'*524288 + else:pair['producer_schema_sha256']='f'*64 + with self.subTest(choice=choice),self.assertRaises((ValueError,TypeError)): + receipt(canonical(value),pair,self.request,self.plan['documents'])