diff --git a/docs/evidence/2026-09-27-grant-aware-worker-rollout.json b/docs/evidence/2026-09-27-grant-aware-worker-rollout.json new file mode 100644 index 0000000..0553fdd --- /dev/null +++ b/docs/evidence/2026-09-27-grant-aware-worker-rollout.json @@ -0,0 +1,40 @@ +{ + "observed_at": "2026-09-27T20:01:04.485643+00:00", + "activity_revision": "428f2d71b0f5ade11457e16d7b6341f571f6735f", + "platform_revision": "c3607fffeade70acd361a4757e72a062725cb83a", + "deployment_diff": "worker image only; reuse already deployed API image; API/router/config/schedules unchanged", + "tests": { + "focused_grant_tests_passed": 21, + "queue_repair_guard_tests_passed": 9, + "native_binding_tests_passed": 5, + "actual_image_rule_emission_queue_support": true + }, + "argocd": { + "sync": "Synced", + "health": "Healthy", + "phase": "Succeeded" + }, + "deployments": [ + { + "name": "actcore-api", + "generation": 50, + "observed_generation": 50, + "ready": 1, + "image": "forgejo.coulomb.social/coulomb/activity-core@sha256:713bddad10a41950f446100a8b370fca8ccdd0b8969cccae751e3870c9c63ccd" + }, + { + "name": "actcore-worker", + "generation": 63, + "observed_generation": 63, + "ready": 1, + "image": "forgejo.coulomb.social/coulomb/activity-core@sha256:713bddad10a41950f446100a8b370fca8ccdd0b8969cccae751e3870c9c63ccd" + }, + { + "name": "actcore-event-router", + "generation": 33, + "observed_generation": 33, + "ready": 1, + "image": "forgejo.coulomb.social/coulomb/activity-core@sha256:cd4e924c2809f0d1d319e53a9ddd20db2e88a1b543d62e6442702c184b7842f4" + } + ] +} diff --git a/docs/evidence/2026-09-27-metered-acceptance-confirmations.json b/docs/evidence/2026-09-27-metered-acceptance-confirmations.json new file mode 100644 index 0000000..6de37e8 --- /dev/null +++ b/docs/evidence/2026-09-27-metered-acceptance-confirmations.json @@ -0,0 +1,44 @@ +[ + { + "memo_id": "metered-20260927-provider-apply", + "disposition": "accept", + "approval_id": "0c05bd0a-f81f-451d-840c-5565628e2edc", + "native_submission_state": "confirmed", + "approved_at": "2026-09-27T19:23:59+00:00" + }, + { + "memo_id": "metered-20260927-provider-exec", + "disposition": "accept", + "approval_id": "7b32443a-a817-400c-a130-01b9ef04c8ee", + "native_submission_state": "confirmed", + "approved_at": "2026-09-27T19:24:15+00:00" + }, + { + "memo_id": "metered-20260927-provider-verify", + "disposition": "accept", + "approval_id": "47f118a3-a86c-43ad-969d-42e09a0f45bb", + "native_submission_state": "confirmed", + "approved_at": "2026-09-27T19:24:32+00:00" + }, + { + "memo_id": "metered-20260927-worker-apply", + "disposition": "accept", + "approval_id": "2ce76d7f-01c3-4b63-8476-8d1230679769", + "native_submission_state": "confirmed", + "approved_at": "2026-09-27T19:24:43+00:00" + }, + { + "memo_id": "metered-20260927-worker-exec", + "disposition": "accept", + "approval_id": "dc22666a-d5d7-46bc-9490-ebe9fe09dc38", + "native_submission_state": "confirmed", + "approved_at": "2026-09-27T19:24:55+00:00" + }, + { + "memo_id": "metered-20260927-worker-verify", + "disposition": "accept", + "approval_id": "c2af4bcf-15b4-4305-aac1-acc7e4dcb099", + "native_submission_state": "confirmed", + "approved_at": "2026-09-27T19:25:10+00:00" + } +] diff --git a/docs/evidence/2026-09-27-metered-attended-execution.json b/docs/evidence/2026-09-27-metered-attended-execution.json new file mode 100644 index 0000000..15202b4 --- /dev/null +++ b/docs/evidence/2026-09-27-metered-attended-execution.json @@ -0,0 +1,12 @@ +{ + "phase": "failed", + "status": "failed", + "credential_values_emitted": false, + "reader_scope_verified": true, + "kv_version": 1, + "named_owner_images_verified": true, + "remote_exit_code": 1, + "owner_forwards_closed": true, + "failure_type": "ValueError", + "failure_code": "attended_execution_failed" +} diff --git a/docs/evidence/2026-09-27-metered-attended-resume.json b/docs/evidence/2026-09-27-metered-attended-resume.json new file mode 100644 index 0000000..15202b4 --- /dev/null +++ b/docs/evidence/2026-09-27-metered-attended-resume.json @@ -0,0 +1,12 @@ +{ + "phase": "failed", + "status": "failed", + "credential_values_emitted": false, + "reader_scope_verified": true, + "kv_version": 1, + "named_owner_images_verified": true, + "remote_exit_code": 1, + "owner_forwards_closed": true, + "failure_type": "ValueError", + "failure_code": "attended_execution_failed" +} diff --git a/docs/evidence/2026-09-27-metered-native-consumes.json b/docs/evidence/2026-09-27-metered-native-consumes.json new file mode 100644 index 0000000..9e75d73 --- /dev/null +++ b/docs/evidence/2026-09-27-metered-native-consumes.json @@ -0,0 +1,32 @@ +{ + "0c05bd0a-f81f-451d-840c-5565628e2edc": { + "approval_id": "0c05bd0a-f81f-451d-840c-5565628e2edc", + "valid_now": false, + "consumed": true + }, + "47f118a3-a86c-43ad-969d-42e09a0f45bb": { + "approval_id": "47f118a3-a86c-43ad-969d-42e09a0f45bb", + "valid_now": false, + "consumed": true + }, + "7b32443a-a817-400c-a130-01b9ef04c8ee": { + "approval_id": "7b32443a-a817-400c-a130-01b9ef04c8ee", + "valid_now": false, + "consumed": true + }, + "2ce76d7f-01c3-4b63-8476-8d1230679769": { + "approval_id": "2ce76d7f-01c3-4b63-8476-8d1230679769", + "valid_now": false, + "consumed": true + }, + "c2af4bcf-15b4-4305-aac1-acc7e4dcb099": { + "approval_id": "c2af4bcf-15b4-4305-aac1-acc7e4dcb099", + "valid_now": false, + "consumed": true + }, + "dc22666a-d5d7-46bc-9490-ebe9fe09dc38": { + "approval_id": "dc22666a-d5d7-46bc-9490-ebe9fe09dc38", + "valid_now": false, + "consumed": true + } +} diff --git a/docs/evidence/2026-09-27-metered-native-execution.json b/docs/evidence/2026-09-27-metered-native-execution.json new file mode 100644 index 0000000..d7d356f --- /dev/null +++ b/docs/evidence/2026-09-27-metered-native-execution.json @@ -0,0 +1,60 @@ +{ + "status": "failed", + "phase": "one_trigger_started", + "observed_at": "2026-09-27T19:48:11.627308+00:00", + "actions": [ + { + "catalog": "glas-claude-agent-dev-anthropic", + "action": "apply", + "approval_id": "0c05bd0a-f81f-451d-840c-5565628e2edc", + "exit_code": 0, + "limits": { + "token_ttl": 300, + "token_max_ttl": 900, + "secret_id_ttl": 300, + "secret_id_num_uses": 1, + "token_num_uses": 8 + } + }, + { + "catalog": "activity-core-metered-worker-token", + "action": "apply", + "approval_id": "2ce76d7f-01c3-4b63-8476-8d1230679769", + "exit_code": 0, + "limits": { + "token_ttl": 300, + "token_max_ttl": 900, + "secret_id_ttl": 300, + "secret_id_num_uses": 1, + "token_num_uses": 8 + } + }, + { + "catalog": "glas-claude-agent-dev-anthropic", + "action": "verify", + "approval_id": "47f118a3-a86c-43ad-969d-42e09a0f45bb", + "exit_code": 0, + "sibling_denied": true, + "metadata_denied": true, + "session_revoked": true, + "revoked_lookup_status": 403 + }, + { + "catalog": "activity-core-metered-worker-token", + "action": "verify", + "approval_id": "c2af4bcf-15b4-4305-aac1-acc7e4dcb099", + "exit_code": 0, + "sibling_denied": true, + "metadata_denied": true, + "session_revoked": true, + "revoked_lookup_status": 403 + } + ], + "automatic_retry": false, + "trigger": { + "workflow_id": "activity-5bae5505-77f1-5ba3-8dfa-2e10ed15531e:manual-dab6c4c7-db03-4887-a17b-9d99b752482e", + "trigger_key": "manual-dab6c4c7-db03-4887-a17b-9d99b752482e" + }, + "failure_type": "ValueError", + "failure_code": "natural_queue_binding_refused" +} diff --git a/docs/evidence/2026-09-27-metered-native-resume.json b/docs/evidence/2026-09-27-metered-native-resume.json new file mode 100644 index 0000000..f5a2f42 --- /dev/null +++ b/docs/evidence/2026-09-27-metered-native-resume.json @@ -0,0 +1,63 @@ +{ + "status": "attempt_failed", + "phase": "one_exec_returned", + "observed_at": "2026-09-27T20:01:30.849242+00:00", + "actions": [ + { + "catalog": "glas-claude-agent-dev-anthropic", + "action": "exec", + "approval_id": "7b32443a-a817-400c-a130-01b9ef04c8ee", + "exit_code": 1 + }, + { + "catalog": "activity-core-metered-worker-token", + "action": "exec", + "approval_id": "dc22666a-d5d7-46bc-9490-ebe9fe09dc38", + "exit_code": 1 + } + ], + "automatic_retry": false, + "prior_receipt": "execution.json", + "completed_actions_not_replayed": [ + "provider/apply", + "worker/apply", + "provider/verify", + "worker/verify", + "queue/trigger" + ], + "trigger": { + "workflow_id": "activity-5bae5505-77f1-5ba3-8dfa-2e10ed15531e:manual-dab6c4c7-db03-4887-a17b-9d99b752482e", + "trigger_key": "manual-dab6c4c7-db03-4887-a17b-9d99b752482e" + }, + "queued_run": { + "id": "6efa9436-6a91-47c0-94e5-b31b5a0e7e3a", + "state": "open", + "attempt": 0, + "claim_owner": null, + "target_repo": "hfact-glas-proof", + "harness_profile_ref": "harness.agent-dev-local@1.1.1", + "repository_grant": { + "publish": false, + "version": "1", + "commit_count": { + "max": 1, + "min": 1 + }, + "allowed_paths": [ + "PROOF.md" + ] + }, + "triggering_event_id": "manual-dab6c4c7-db03-4887-a17b-9d99b752482e" + }, + "exec_exit_code": 1, + "owner_results": [ + { + "ok": false, + "claimed": true, + "empty": false, + "run_id": "6efa9436-6a91-47c0-94e5-b31b5a0e7e3a", + "ops_state": "failed" + } + ], + "private_runtime_removed": true +} diff --git a/docs/evidence/2026-09-27-metered-postflight.json b/docs/evidence/2026-09-27-metered-postflight.json new file mode 100644 index 0000000..7996cc6 --- /dev/null +++ b/docs/evidence/2026-09-27-metered-postflight.json @@ -0,0 +1,39 @@ +{ + "runtime_sha256": "b6e4e8a429393d68831c996a65c0489664205df969ac9882e581983ec2da4969", + "repository_head": "679d23e0517707ba63e25399b5262f0d2f37315d", + "repository_clean": true, + "repository_lock_available": true, + "sandbox": { + "sandbox_id": "b67907f1", + "state": "destroyed", + "created_at": "2026-09-27T20:01:43.835992Z", + "destroyed_at": "2026-09-27T20:01:45.650937Z" + }, + "removed_paths": { + "workspace_dir": true + }, + "private_credential_directories_remaining": 0, + "close_outbox_pending": 0, + "close_outbox_quarantined": 0, + "spend_reservations": [ + { + "run_id": "6efa9436-6a91-47c0-94e5-b31b5a0e7e3a", + "definition_id": "5bae5505-77f1-5ba3-8dfa-2e10ed15531e", + "idempotency_key": "5bae5505-77f1-5ba3-8dfa-2e10ed15531e:execute-hfact-glas-metered-proof:manual-dab6c4c7-db03-4887-a17b-9d99b752482e", + "attempt": 1, + "state": "held", + "liability": 10000000, + "start_day": "2026-09-27", + "end_day": null, + "observed_usd": null, + "receipt": null + } + ], + "provider_request_reservations": [], + "request_routes": [ + { + "run_id": "6efa9436-6a91-47c0-94e5-b31b5a0e7e3a", + "revoked": 1 + } + ] +} diff --git a/docs/evidence/2026-09-27-metered-queue-grant-reconciliation.json b/docs/evidence/2026-09-27-metered-queue-grant-reconciliation.json new file mode 100644 index 0000000..7fa26af --- /dev/null +++ b/docs/evidence/2026-09-27-metered-queue-grant-reconciliation.json @@ -0,0 +1,27 @@ +{ + "status": "applied", + "observed_at": "2026-09-27T19:59:50.642053+00:00", + "worker_pod": "actcore-worker-659497dcf9-rvf4h", + "worker_image": "sha256:713bddad10a41950f446100a8b370fca8ccdd0b8969cccae751e3870c9c63ccd", + "run_id": "6efa9436-6a91-47c0-94e5-b31b5a0e7e3a", + "definition_id": "5bae5505-77f1-5ba3-8dfa-2e10ed15531e", + "definition_version": 1, + "triggering_event_id": "manual-dab6c4c7-db03-4887-a17b-9d99b752482e", + "source": "typed_existing_definition_rule", + "grant_id": "656911f7dff83e871434b415f0cee685", + "grant": { + "version": "1", + "allowed_paths": [ + "PROOF.md" + ], + "commit_count": { + "min": 1, + "max": 1 + }, + "publish": false + }, + "attempt": 0, + "claim_owner": null, + "new_trigger": false, + "new_task": false +} diff --git a/docs/evidence/2026-09-27-metered-renewal-installation.json b/docs/evidence/2026-09-27-metered-renewal-installation.json new file mode 100644 index 0000000..0672e3e --- /dev/null +++ b/docs/evidence/2026-09-27-metered-renewal-installation.json @@ -0,0 +1,20 @@ +{ + "status": "installed", + "observed_at": "2026-09-27T19:39:35.079399+00:00", + "old_counts": { + "reservations": 0, + "request_routes": 0, + "request_reservations": 0 + }, + "old_ledger_preserved": true, + "new_ledger": "/home/tegwick/hfact/owner-metered/spend-tool-proof-renewal-20260927.sqlite3", + "old_dispatch_pins_retired": true, + "owner_check": { + "ok": true, + "check_only": true, + "dispatch_enabled": false, + "policy_sha256": "de3d01c9f4753994e8f73c111cde328a649e05dbb2563c37b5c12d86a488b2fa", + "runtime_sha256": "b6e4e8a429393d68831c996a65c0489664205df969ac9882e581983ec2da4969" + }, + "dispatches": 0 +} diff --git a/docs/evidence/2026-09-27-metered-terminal-delivery.json b/docs/evidence/2026-09-27-metered-terminal-delivery.json new file mode 100644 index 0000000..b1d32d1 --- /dev/null +++ b/docs/evidence/2026-09-27-metered-terminal-delivery.json @@ -0,0 +1,109 @@ +{ + "source": "railiance01/native-metered-20260927/native-evidence; allowlisted fields only", + "native_records": [ + { + "action": "apply", + "catalog_id": "glas-claude-agent-dev-anthropic", + "result": "applied", + "record_id": "53136c39-5fb6-4f61-8d73-231c5d0fe57d", + "detail": { + "approval_consumed": true, + "approval_id": "0c05bd0a-f81f-451d-840c-5565628e2edc", + "approval_consume_idempotent": false + } + }, + { + "action": "apply", + "catalog_id": "activity-core-metered-worker-token", + "result": "applied", + "record_id": "f218efea-a990-4393-a99c-e405050a349e", + "detail": { + "approval_consumed": true, + "approval_id": "2ce76d7f-01c3-4b63-8476-8d1230679769", + "approval_consume_idempotent": false + } + }, + { + "action": "verify", + "catalog_id": "glas-claude-agent-dev-anthropic", + "result": "pass", + "record_id": "9a3347b1-9b1c-42a1-a504-ce791fdf1215", + "detail": { + "approval_consumed": true, + "approval_id": "47f118a3-a86c-43ad-969d-42e09a0f45bb", + "approval_consume_idempotent": false + } + }, + { + "action": "verify", + "catalog_id": "activity-core-metered-worker-token", + "result": "pass", + "record_id": "43517ef4-ab0e-47c4-ae96-4284a6f67b2e", + "detail": { + "approval_consumed": true, + "approval_id": "c2af4bcf-15b4-4305-aac1-acc7e4dcb099", + "approval_consume_idempotent": false + } + }, + { + "action": "exec", + "catalog_id": "activity-core-metered-worker-token", + "result": "exit-1", + "record_id": "7b3f492c-cf80-4e4d-8158-7e4504b136ca", + "detail": { + "approval_consumed": true, + "approval_id": "dc22666a-d5d7-46bc-9490-ebe9fe09dc38", + "approval_consume_idempotent": false, + "session": { + "established": true, + "revocation_attempted": true, + "revocation_succeeded": true, + "session_handle": "a0f9a121b064" + }, + "companion_of": "glas-claude-agent-dev-anthropic", + "exec_owner_sha256": "310600eab467ed79fc3851178a065de12d57d3ada5bcf68d9c364ed11b3ee50f" + } + }, + { + "action": "exec", + "catalog_id": "glas-claude-agent-dev-anthropic", + "result": "exit-1", + "record_id": "1b6fabda-dbe5-4fe3-a74e-b9abf62050c1", + "detail": { + "approval_consumed": true, + "approval_id": "7b32443a-a817-400c-a130-01b9ef04c8ee", + "approval_consume_idempotent": false, + "session": { + "established": true, + "revocation_attempted": true, + "revocation_succeeded": true, + "session_handle": "d7e5f59e383d" + }, + "companions": [ + "activity-core-metered-worker-token" + ], + "exec_owner_sha256": "310600eab467ed79fc3851178a065de12d57d3ada5bcf68d9c364ed11b3ee50f" + } + } + ], + "terminal_queue": { + "id": "6efa9436-6a91-47c0-94e5-b31b5a0e7e3a", + "state": "failed", + "attempt": 1, + "claim_owner": "rein-aharness-metered@railiance01", + "lease_until": null + }, + "verification": { + "full_suite": "406 passed, 9 skipped", + "focused_owner_claim_spend_glas": "99 passed (includes new close-evidence test)", + "native_procedure_and_queue_guards": "14 passed", + "sandbox_builder": "12 passed", + "activity_core_grant": "21 passed" + }, + "remaining": { + "workplan": "blocked", + "parent_eur_reservation": "10.00 held; observed USD unknown", + "retry_authorized": false, + "corrected_artifact_admitted": false + } +} diff --git a/docs/native-metered-proof.md b/docs/native-metered-proof.md new file mode 100644 index 0000000..3bd05e3 --- /dev/null +++ b/docs/native-metered-proof.md @@ -0,0 +1,97 @@ +# Attended disposable proof — 2026-09-27 + +Existing work: REINAH-WP-0003-T05/T06 and SECRETS-WP-0009-T03. +No new workplan, publication, automatic retry, or factory operating admission. + +The operator accepted replacement spend memo `infd-20260927-b02`, confirmed +USD 10 including tax/conversion fees fits EUR 10, and accepted all six +`metered-20260927-{provider,worker}-{apply,verify,exec}` decisions. Native +submission confirmations and replacement host installation are separate receipts +in `docs/evidence/2026-09-27-metered-*.json`. The earlier expired spend grant is +preserved and never used. + +`scripts/metered-native-owner.py` checks the frozen six-request packet and exact +recipient files before native claim/PDP checks. The provider's human-control +flag and the worker companion's ordinary flag remain unchanged. Apply and +verify each consume their own native approval before OpenBao effects. Exec +uses the existing Secrets Engine primary/companion consume and delivery code; +the procedure does not manufacture decisions, claims, or delivery state. + +The attended workstation wrapper `scripts/attended-metered-proof.py` follows +the existing scoped approval-client reader and nested platform-admin OIDC +procedure. The operator-controlled SSH session runs the controller on Railiance +because the approved command and owner-file bindings name that host. Client +secret, short-lived operator/negative-test tokens and PDP caller token cross +encrypted SSH stdin only. The host uses a fresh owner-only temporary directory +on `/run/user/1000` tmpfs. Approval bearer tokens are minted in memory. No +cluster reader, persistent service, credential rotation or new custody path is +created. Warden self-revokes both attended sessions; private files and the +named-pod forwarding processes are removed on normal exit, including failure. +The delivered model child receives only its catalog-bound environment plus +its separately approved provider and worker credentials. + +Before activation, the installer locks and checks all three old ledger tables, +refusing any prior reservation or liability. It preserves the old ledger and +backs up both old configuration files. A new private ledger is initialized +without resetting old evidence. Replacement file hashes retire dispatch using +the old catalog pins. The immutable runtime's backend-free owner check passes. + +The single trigger occurs after both lanes verify. The controller waits for +exactly one open, unclaimed, attempt-zero task with the admitted profile and +one-file/one-commit/no-publication grant. It records trigger and exec intent +before either action. An existing execution receipt refuses all replays, +including uncertain/failed attempts. The scheduled definition stays disabled. + +The approved maximum request hold remains USD 4.64; at most two such holds fit +the USD 10 liability cap. The [provider tariff](https://platform.claude.com/docs/en/about-claude/pricing) +was rechecked immediately before activation: Sonnet 5 global standard output is +USD 10/million tokens; the conservative input bound of USD 4/million covers +one-hour cache writes. The proof has no authority to enlarge these limits. + +After interruption, inspect the durable local/remote receipts and native +consume state before any further action. Explicitly reconcile remaining +`metered-native-*` / `metered-attended-*` private runtime directories and any +open queue item; never rerun the command as a cleanup strategy. Hard-kill +cleanup is not claimed by this wrapper. Provider-request reservations remain +conservative until reconciled. T04's tenant migrations and broader T06 +acceptance requirements remain in the existing workplan. + +## Actual execution and corrections + +All six decisions were accepted and consumed once. Both lane apply/verify +operations passed; exec delivered through two AppRole sessions whose revocation +succeeded. The single definition trigger initially produced a row with no grant: +the deployed Activity Core worker predated the API's grant-carriage support. +Activity Core `428f2d7` and Platform `c3607ff` changed only the worker image to +the already-deployed grant-aware API image through the existing GitOps parent +and child applications. Argo reports Synced/Healthy/Succeeded. + +The explicit repair script validates and locks the original disabled definition +and original open, unclaimed, attempt-zero job. It copies only the exact typed +grant from that definition; it creates no row or trigger. Nine negative/positive +guard tests pass. This repair is recorded separately and is not represented as +successful natural grant carriage by the old producer. + +The `resume` mode is limited to that recorded pre-execution queue-binding failure. +It verifies the four completed native actions, installed policies/role limits, +zero prior request/reservation counts, and the same repaired job. It repeats +neither apply/verify nor queue creation. Both remaining exec approvals were then +consumed. There is no further resume path for the attempted job. + +Job `6efa9436-6a91-47c0-94e5-b31b5a0e7e3a` closed `failed`, attempt 1, with its +lease cleared. Its installed Python launchers still referenced a deleted build +directory: uv's long-path shell trampoline had escaped the builder's direct +shebang rewrite. A no-credential synthetic probe reproduced exit 127 for both +launchers inside the actual pinned bwrap artifact, with zero provider forwards. +The runtime builder correction is Sand-boxer `81b5fcf`; relocation tests execute +after the old build directory disappears. Rein now also rejects these launchers +before claim and retains bounded gateway stage/cleanup facts on accounting refusal. + +The approved artifact itself remains unchanged. Sandbox `b67907f1` and its +workspace are destroyed; the repository is clean at its original commit, the +lock is available, and no close-outbox item or private credential directory is +left over. The request route is revoked and there are no provider-request +reservations. The parent EUR 10 reservation remains held; observed billing was +not invented and the ledger was not reset. A corrected artifact and its changed +pins need admission, held-liability reconciliation remains an owner action, and +another attempt requires separate authority. REINAH-WP-0003 stays blocked. diff --git a/docs/owner-bootstrap.md b/docs/owner-bootstrap.md index 586f9f6..0cc101a 100644 --- a/docs/owner-bootstrap.md +++ b/docs/owner-bootstrap.md @@ -47,6 +47,9 @@ profile/descriptor digests, operational readiness, model, empty-egress bwrap pro and runtime digest are checked before claim. Runtime, owner state and target checkout must not overlap. Provision parent and request ledgers as separate reviewed actions. The normal ACTIVITY_CORE/AGENT_HARNESS worker and state configuration still applies. +The governed Python console launchers must be executable regular files naming +`/opt/sandboxer/runtime/bin/python3`, rather than a build-host interpreter. +The installed bwrap proof also runs their `--help` commands before any model request. This config is not an authorization decision or a custody provenance proof. The invoking credential engine must already have passed its exact action approval, diff --git a/rein_aharness/claim_loop.py b/rein_aharness/claim_loop.py index ff5a9a4..99326b7 100644 --- a/rein_aharness/claim_loop.py +++ b/rein_aharness/claim_loop.py @@ -627,6 +627,8 @@ def _process_profiled_run_active( except GlasExecutionError as exc: execution_error = type(exc).__name__ execution_reason = str(exc) if isinstance(exc, GlasSpendError) else "profiled execution failed (GlasExecutionError)" + if isinstance(exc, GlasSpendError): + safe_evidence = exc.execution_evidence if tx is not None and tx.baseline is not None: tx_evidence = tx.evidence() diff --git a/rein_aharness/glas_execution.py b/rein_aharness/glas_execution.py index d15f4fc..e1c0e5e 100644 --- a/rein_aharness/glas_execution.py +++ b/rein_aharness/glas_execution.py @@ -66,6 +66,16 @@ class GlasExecutionError(RuntimeError): class GlasSpendError(GlasExecutionError): """Spend refusal with bounded, operator-safe reason text.""" + def __init__(self, message: str, *, execution_evidence: Any = None) -> None: + super().__init__(message) + evidence = normalise_execution_evidence_for_close(execution_evidence) + # Accounting must remain fail-closed without erasing the gateway stage + # and cleanup facts. Do not transport raw provider error/output here. + self.execution_evidence = { + key: value for key, value in evidence.items() + if key not in {"error", "artifacts"} + } + def normalise_execution_evidence_for_close(raw: Any) -> dict[str, Any]: """Retain only bounded Glas evidence fields safe for durable close state.""" @@ -85,6 +95,10 @@ def normalise_execution_evidence_for_close(raw: Any) -> dict[str, Any]: and value >= 0 ): result[key] = value + for key in ("session_cleanup", "sandbox_destroy"): + value = raw.get(key) + if isinstance(value, str) and value in {"succeeded", "failed", "not_attempted"}: + result[key] = value artifacts = raw.get("artifacts") if isinstance(artifacts, list): result["artifacts"] = [ @@ -220,7 +234,10 @@ def execute_profiled_run( if not spend.observe(run.id, raw): raise SpendAdmissionError("execution accounting requires reconciliation") except SpendAdmissionError as exc: - raise GlasSpendError(f"spend accounting refused: {exc}") from None + raise GlasSpendError( + f"spend accounting refused: {exc}", + execution_evidence=raw["evidence"], + ) from None if transfer is not None and raw["ok"]: evidence = raw["evidence"] if evidence.get("session_cleanup") != "succeeded" or evidence.get("sandbox_destroy") != "succeeded": diff --git a/rein_aharness/owner_bootstrap.py b/rein_aharness/owner_bootstrap.py index 712da11..824538c 100644 --- a/rein_aharness/owner_bootstrap.py +++ b/rein_aharness/owner_bootstrap.py @@ -25,7 +25,7 @@ class BootstrapRefused(RuntimeError): def prepare(path: Path, config: OpsRunConfig) -> tuple[MessagesPolicy, Path, str]: """Validate value-free, owner-controlled pins without a key or queue claim.""" from glas_harness.profiles import ProfileCatalog - from sandboxer.extensions.runtime import verified_runtime + from sandboxer.extensions.runtime import RUNTIME_MOUNT, verified_runtime try: _private_file(path) if not path.is_absolute() or path.resolve() != path or path.stat().st_size > 65536: @@ -64,6 +64,15 @@ def prepare(path: Path, config: OpsRunConfig) -> tuple[MessagesPolicy, Path, str runtime = verified_runtime({"runtime": data["runtime"]}) if runtime is None or not runtime.is_absolute() or runtime.resolve() != runtime: raise ValueError + # A matching artifact digest does not prove its generated launchers + # survived relocation. Refuse build-host interpreters before claiming. + for name in ("rein-aharness", "glas-harness"): + executable = runtime / "bin" / name + if executable.is_symlink() or not executable.is_file() or not os.access(executable, os.X_OK): + raise ValueError + with executable.open("rb") as stream: + if stream.readline(4096) != f"#!{RUNTIME_MOUNT}/bin/python3\n".encode(): + raise ValueError for private in (path.parent, spend.path.parent, Path(spend.policy.target_repo)): a, b = runtime.resolve(), private.resolve() if a.is_relative_to(b) or b.is_relative_to(a): diff --git a/scripts/attended-metered-proof.py b/scripts/attended-metered-proof.py new file mode 100644 index 0000000..3253177 --- /dev/null +++ b/scripts/attended-metered-proof.py @@ -0,0 +1,140 @@ +"""Contained workstation login envelope for the exact Railiance native proof. + +Run the reader through Warden. Both attended sessions self-revoke. Values stay +in private tmpfs / process memory and encrypted SSH stdin; output is suppressed. +""" +import importlib.util +import json +import os +from pathlib import Path +import socket +import subprocess +import sys +import tempfile +import time +from urllib.request import Request, build_opener, ProxyHandler, HTTPRedirectHandler + +ROOT = Path("/home/worsch/rein-aharness") +PLATFORM = Path("/home/worsch/railiance-platform") +PYTHON = "/home/worsch/secrets-engine/.venv/bin/python" +REMOTE = "/home/tegwick/hfact/native-metered-20260927" +KUBE = ["kubectl", "--kubeconfig", "/home/worsch/.kube/config-railiance01"] +RECEIPT = ROOT / "docs/evidence/2026-09-27-metered-attended-execution.json" +RESUME = len(sys.argv) > 1 and sys.argv[1] in ("reader-resume", "admin-resume") +if RESUME: + RECEIPT = ROOT / "docs/evidence/2026-09-27-metered-attended-resume.json" +spec = importlib.util.spec_from_file_location("native", ROOT / "scripts/metered-native-owner.py") +native = importlib.util.module_from_spec(spec) +spec.loader.exec_module(native) +require, private, run, write_new = native.require, native.private, native.run, native.write_new + + +class NoRedirect(HTTPRedirectHandler): + def redirect_request(self, *args, **kwargs): + return None + + +def receipt(phase, **fields): + data = json.loads(RECEIPT.read_text()) if RECEIPT.exists() else {} + data.update(phase=phase, **fields) + RECEIPT.write_text(json.dumps(data, indent=2) + "\n") + + +def helper(): + require(Path.home().parent.name == ".warden-attended-login" and not os.getenv("BAO_TOKEN") and not os.getenv("VAULT_TOKEN"), "attended_containment_required") + path = Path.home() / ".vault-token" + private(path) + return path + + +def reader(): + require(not RECEIPT.exists(), "prior_session_requires_reconciliation") + receipt("reader_preflight", status="in_progress", credential_values_emitted=False) + token_file = helper() + spec = importlib.util.spec_from_file_location("reader", PLATFORM / "scripts/approval-client-reader-preflight.py") + pre = importlib.util.module_from_spec(spec) + spec.loader.exec_module(pre) + pre.validate_identity(pre.bao("token", "lookup", "-format=json")["data"]) + for path, expected in pre.EXPECTED.items(): + require(sorted(pre.bao("token", "capabilities", "-format=json", path)) == expected, "reader_scope_failed") + runtime = Path("/run/user") / str(os.getuid()) + private(runtime, True) + require(run(["findmnt", "-n", "-o", "FSTYPE", "-T", str(runtime)]) == "tmpfs", "private_tmpfs_required") + with tempfile.TemporaryDirectory(prefix="metered-attended-", dir=runtime) as name: + directory = Path(name) + req = Request("http://127.0.0.1:18200/v1/platform/data/workloads/secrets-engine/approval-client?version=1", headers={"X-Vault-Token": token_file.read_text().strip()}) + with build_opener(ProxyHandler({}), NoRedirect()).open(req, timeout=20) as response: + raw = response.read(65537) + require(len(raw) <= 65536, "credential_response_bound") + data = json.loads(raw) + require(data["data"]["metadata"]["version"] == 1, "custody_version_drift") + value = data["data"]["data"]["CLIENT_SECRET"] + require(isinstance(value, str) and 0 < len(value) <= 16384, "credential_invalid") + write_new(directory / "client-secret", value) + del value, raw, data, req + receipt("attended_admin_login", reader_scope_verified=True, kv_version=1) + result = subprocess.run(["python3", str(PLATFORM / "scripts/openbao-attended-exec.py"), "--", PYTHON, "-B", str(Path(__file__).resolve()), "admin-resume" if RESUME else "admin", str(directory), str(token_file)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=780) + require(result.returncode == 0, "attended_execution_failed") + receipt("attended_sessions_completed", status="completed", local_private_runtime_removed=True) + + +def admin(directory, negative): + admin_file = helper() + private(directory, True) + private(directory / "client-secret") + private(negative) + forwards = [] + payload = {} + try: + for namespace, label, image, port in (("approval-engine", "approval-engine", "251941a5cb2724b57cc32cff6b693b1ab0be695bee4f56f02d51961189c0fa49", 19281), ("flex-auth", "flex-auth-secrets-engine", "05a03a8790c2210c48ea92391441c77ddf640d0cd32f5ec09838f5393171fcbd", 19282)): + pods = json.loads(run(KUBE + ["-n", namespace, "get", "pods", "-l", "app.kubernetes.io/name=" + label, "-o", "json"]))["items"] + require(len(pods) == 1, "single_owner_pod_required") + pod = pods[0] + require(any(c.get("ready") and c.get("imageID", "").endswith("@sha256:" + image) for c in pod.get("status", {}).get("containerStatuses", [])), "native_image_pin_drift") + forwards.append(subprocess.Popen(KUBE + ["-n", namespace, "port-forward", "pod/" + pod["metadata"]["name"], str(port) + ":8080", "--address=127.0.0.1"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)) + for port in (19281, 19282): + for _ in range(50): + require(all(p.poll() is None for p in forwards), "native_forward_failed") + try: + with socket.create_connection(("127.0.0.1", port), timeout=.2): + break + except OSError: + time.sleep(.1) + else: + raise ValueError("native_forward_not_ready") + bridge = subprocess.Popen(["ssh", "-N", "-o", "ExitOnForwardFailure=yes", "-R", "127.0.0.1:28200:127.0.0.1:18200", "-R", "127.0.0.1:28281:127.0.0.1:19281", "-R", "127.0.0.1:28282:127.0.0.1:19282", "railiance01"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + forwards.append(bridge) + time.sleep(1) + require(bridge.poll() is None, "ssh_bridge_failed") + payload = dict(client_secret=(directory / "client-secret").read_text(), negative_token=negative.read_text().strip(), backend_token=admin_file.read_text().strip(), pdp_token=run(KUBE + ["-n", "secrets-engine", "create", "token", "secrets-engine", "--audience=flex-auth", "--duration=10m"])) + receipt("remote_native_execution_started", named_owner_images_verified=True) + result = subprocess.run(["ssh", "railiance01", "env", "PATH=/home/tegwick/.local/bin:/usr/local/bin:/usr/bin:/bin", "PYTHONPATH=/home/tegwick/secrets-engine/src", "/home/tegwick/secrets-engine/.venv/bin/python", "-B", REMOTE + "/metered-native-owner.py", "resume" if RESUME else "execute", REMOTE], input=json.dumps(payload), text=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=650) + payload.clear() + receipt("remote_native_execution_returned", remote_exit_code=result.returncode) + require(result.returncode == 0, "remote_native_execution_failed") + finally: + payload.clear() + for process in reversed(forwards): + process.terminate() + try: + process.wait(timeout=5) + except subprocess.TimeoutExpired: + process.kill() + process.wait() + receipt("native_transport_closed", owner_forwards_closed=True) + + +if __name__ == "__main__": + try: + if sys.argv[1:] in (["reader"], ["reader-resume"]): + reader() + elif len(sys.argv) == 4 and sys.argv[1] in ("admin", "admin-resume"): + admin(Path(sys.argv[2]), Path(sys.argv[3])) + else: + raise ValueError("unsupported_mode") + except Exception as error: + fields = {"status": "failed", "failure_type": type(error).__name__} + if type(error) is ValueError and str(error).replace("_", "").isalnum(): + fields["failure_code"] = str(error) + receipt("failed", **fields) + raise SystemExit(1) from None diff --git a/scripts/metered-native-owner.py b/scripts/metered-native-owner.py new file mode 100644 index 0000000..7feebc0 --- /dev/null +++ b/scripts/metered-native-owner.py @@ -0,0 +1,347 @@ +"""Single attended, non-retrying Railiance proof under the six frozen approvals. + +Non-secret bundle is staged separately. Credentials arrive only on SSH stdin, +live in owner-only tmpfs for this process, and never enter the bound child except +through Secrets Engine's approved provider/worker delivery. +""" +from __future__ import annotations + +import argparse +import contextlib +import copy +import hashlib +import io +import json +import os +from pathlib import Path +import sqlite3 +import stat +import subprocess +import sys +import tempfile +import time +from datetime import datetime, timezone +from dataclasses import replace + +PACKET_SHA = "22e640428e3a7ae8fc2363cf193db98381cd94e4be7ab009bc6703658d6fc544" +PROVIDER = "glas-claude-agent-dev-anthropic" +WORKER = "activity-core-metered-worker-token" +IDS = { + PROVIDER: dict(apply="0c05bd0a-f81f-451d-840c-5565628e2edc", verify="47f118a3-a86c-43ad-969d-42e09a0f45bb", exec="7b32443a-a817-400c-a130-01b9ef04c8ee"), + WORKER: dict(apply="2ce76d7f-01c3-4b63-8476-8d1230679769", verify="c2af4bcf-15b4-4305-aac1-acc7e4dcb099", exec="dc22666a-d5d7-46bc-9490-ebe9fe09dc38"), +} +LIMITS = dict(token_ttl=300, token_max_ttl=900, secret_id_ttl=300, secret_id_num_uses=1, token_num_uses=8) +OWNER = Path("/home/tegwick/hfact/owner-metered") +TARGET = Path("/home/tegwick/hfact/targets/hfact-glas-proof") +BASE_HEAD = "679d23e0517707ba63e25399b5262f0d2f37315d" +OLD_FILES = {"owner.json": "e0d3fb84649fdca302eccd9415f3cc2beaee84bf07bd83205cdffbe93e5573bc", "spend-policy.json": "f31c585916de1ea8bd8e48c72421803dfde1015e6201704b9320f77d2c545d9c"} +DEFINITION = "5bae5505-77f1-5ba3-8dfa-2e10ed15531e" +RECEIPT_NAME = "execution.json" + + +def require(value, code): + if not value: + raise ValueError(code) + + +def private(path, directory=False): + s = path.lstat() + require(s.st_uid == os.getuid() and stat.S_IMODE(s.st_mode) == (0o700 if directory else 0o600) and (stat.S_ISDIR(s.st_mode) if directory else stat.S_ISREG(s.st_mode)), "private_path_required") + + +def write_new(path, value): + fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY | os.O_NOFOLLOW, 0o600) + with os.fdopen(fd, "w") as stream: + stream.write(value) + stream.flush() + os.fsync(stream.fileno()) + + +def run(args): + p = subprocess.run(args, capture_output=True, text=True, timeout=30) + require(p.returncode == 0, "metadata_command_failed") + return p.stdout.strip() + + +def save(bundle, receipt): + path = bundle / RECEIPT_NAME + temp = path.with_suffix(".tmp") + temp.write_text(json.dumps(receipt, indent=2) + "\n") + temp.chmod(0o600) + temp.replace(path) + + +def queue_rows(): + # Read-only owner metadata via the existing authenticated cluster transport. + code = '''import asyncio,json +from activity_core.db import make_engine +from sqlalchemy import text +async def main(): + e=make_engine() + async with e.connect() as c: + rows=await c.execute(text("SELECT id,state,attempt,claim_owner,target_repo,harness_profile_ref,repository_grant,triggering_event_id FROM ops_runs WHERE activity_definition_id='5bae5505-77f1-5ba3-8dfa-2e10ed15531e' OR labels @> '[\\\"hfact-metered\\\"]'::jsonb")) + print(json.dumps([dict(x) for x in rows.mappings()],default=str)) + await e.dispose() +asyncio.run(main())''' + return json.loads(run(["/usr/local/bin/kubectl", "-n", "activity-core", "exec", "deploy/actcore-api", "--", "python", "-c", code])) + + +def install_host(bundle): + """Run with the immutable admitted runtime; preserve every old ledger byte.""" + from rein_aharness.spend_admission import SpendLedger, SpendPolicy + from rein_aharness.request_admission import RequestLedger + import yaml + private(OWNER, True) + require(not (bundle / "installation.json").exists(), "prior_installation_requires_reconciliation") + require(not queue_rows(), "metered_queue_not_empty") + require(run(["git", "-C", str(TARGET), "rev-parse", "HEAD"]) == BASE_HEAD and not run(["git", "-C", str(TARGET), "status", "--porcelain"]), "target_not_pristine") + for filename, expected in OLD_FILES.items(): + private(OWNER / filename) + require(hashlib.sha256((OWNER / filename).read_bytes()).hexdigest() == expected, "old_host_pin_drift") + candidate = yaml.safe_load((bundle / "catalog" / (PROVIDER + ".yaml")).read_text())["delivery_config"]["exec_owner"] + for filename in OLD_FILES: + require(hashlib.sha256((bundle / filename).read_bytes()).hexdigest() == candidate["files"][str(OWNER / filename)]["sha256"], "candidate_pin_drift") + old = OWNER / "spend.sqlite3" + private(old) + db = sqlite3.connect(old) + try: + db.execute("BEGIN EXCLUSIVE") + counts = {table: db.execute("SELECT count(*) FROM " + table).fetchone()[0] for table in ("reservations", "request_routes", "request_reservations")} + require(not any(counts.values()), "prior_liabilities_require_reconciliation") + policy = SpendPolicy.load(bundle / "spend-policy.json") + ledger = SpendLedger(Path(candidate["environment"]["AGENT_HARNESS_SPEND_LEDGER"]), policy) + require(not ledger.path.exists(), "new_ledger_already_exists") + for filename in OLD_FILES: + write_new(OWNER / (filename + ".pre-renewal-20260927"), (OWNER / filename).read_text()) + ledger.initialize() + RequestLedger(ledger).initialize() + for filename in OLD_FILES: + staged = OWNER / (filename + ".renewal-staged") + write_new(staged, (bundle / filename).read_text()) + staged.replace(OWNER / filename) + # Changing both pins retires the old catalog's dispatch. There is no + # active metered worker; old ledger and files remain preserved. + check = subprocess.run([*candidate["command"], "--check"], env=candidate["environment"], cwd=candidate["cwd"], capture_output=True, text=True, timeout=30) + require(check.returncode == 0, "installed_owner_check_failed") + receipt = dict(status="installed", observed_at=datetime.now(timezone.utc).isoformat(), old_counts=counts, old_ledger_preserved=True, new_ledger=str(ledger.path), old_dispatch_pins_retired=True, owner_check=json.loads(check.stdout), dispatches=0) + write_new(bundle / "installation.json", json.dumps(receipt, indent=2) + "\n") + finally: + db.rollback() + db.close() + + +def prepare_configs(bundle, private_dir, trust=None): + import yaml + from secrets_engine.config import Config + from secrets_engine.catalog import get_entry + from secrets_engine.approval_consume import _expected_request + + packet = (bundle / "native-pdp-inputs.json").read_bytes() + require(hashlib.sha256(packet).hexdigest() == PACKET_SHA, "frozen_packet_drift") + data = json.loads(packet) + rows = data if isinstance(data, list) else data["requests"] + expected = {(r["catalog"], r["action"]): r["request"] for r in rows} + require(set(expected) == {(lane, action) for lane in IDS for action in IDS[lane]}, "six_exact_requests_required") + configs, entries = {}, {} + for action in ("apply", "verify", "exec"): + folder = private_dir / action + folder.mkdir(mode=0o700) + for lane in IDS: + doc = yaml.safe_load((bundle / "catalog" / (lane + ".yaml")).read_text()) + doc = copy.deepcopy(doc) + doc["approval"]["authorization_id"] = IDS[lane][action] + write_new(folder / (lane + ".yaml"), yaml.safe_dump(doc, sort_keys=False)) + cfg = replace(Config.load(), catalog_dir=folder, evidence_dir=bundle / "native-evidence", hub_url="", bao_addr="http://127.0.0.1:28200", approval_url="http://127.0.0.1:28281", approval_token_file=None, approval_client_secret_file=private_dir / "client-secret", keycape_token_url="https://kc.coulomb.social/token", keycape_issuer="https://kc.coulomb.social", keycape_client_secret_file=None, openbao_jwt_login_file=None, authorization_subject_id="secrets-engine", authorization_subject_type="service", authorization_policy_package="secrets-engine.catalog-lane.lifecycle", authorization_policy_version="v2", authorization_min_approvals=1, pdp_url="http://127.0.0.1:28282", pdp_token_file=private_dir / "pdp-caller", clock_trust_file=trust) + configs[action] = cfg + for lane in IDS: + entry = get_entry(folder, lane) + actual = _expected_request(cfg, entry, action, fields=() if action == "apply" else tuple(entry.fields), policy_targets=(entry.policy_name,), auth_targets=(entry.role_name,)) + require(actual == expected[lane, action], "frozen_action_request_drift") + entries[lane, action] = entry + return configs, entries + + +def clock_admission(bundle, directory): + from railiance_clock.admission import admit + from urllib.request import urlopen + custody = json.loads((bundle / "clock-public.json").read_text()) + require(hashlib.sha256(custody["public_key_pem"].encode()).hexdigest() == "bd583446b5ed61d086806b2a0c5aaf33a875b751e45599e75335d1f415be609a", "clock_key_drift") + with urlopen("http://127.0.0.1:8787/healthz", timeout=5) as response: + epoch = json.load(response)["epoch"] + require(epoch == "b1164ccb-a4c2-4cc8-adf8-1d5597de697b", "clock_epoch_requires_readmission") + return admit(directory=directory, authority_id="railiance01", environment="prod", kid=custody["kid"], epoch=epoch, policy_id="railiance01-online-v1", public_key_pem=custody["public_key_pem"], endpoint="http://127.0.0.1:8787/v1/time-samples", limits=dict(max_age_ns=5000000000, max_rtt_ns=2000000000, max_width_ns=3000000000, max_server_error_ns=500000000, timer_ppm=1000, timer_resolution_ns=1000, suspend_tolerance_ns=5000000, trust_session_ns=900000000000), admission_ref="CCR-2026-0028; REINAH-WP-0003; attended proof 2026-09-27", transport="admitted-loopback") + + +def verify_cleanup(client, entry, other): + from urllib.request import Request, urlopen + from urllib.error import HTTPError + with client.approle_session(entry.role_name) as session: + token = session.client.token + for path in (f"platform/data/{other.path}", "platform/metadata/workloads", "platform/data/workloads/secrets-engine/approval-client"): + require(session.client.token_capabilities(path, token=token) == ["deny"], "sibling_or_metadata_authority") + require(session.revocation_succeeded, "session_revocation_failed") + try: + with urlopen(Request(client.addr + "/v1/auth/token/lookup-self", headers={"X-Vault-Token": token}), timeout=15): + raise ValueError("revoked_token_still_usable") + except HTTPError as error: + require(error.code == 403, "revocation_not_definitive") + finally: + del token + return dict(sibling_denied=True, metadata_denied=True, session_revoked=True, revoked_lookup_status=403) + + +def validate_queued(rows, trigger): + require(len(rows) == 1 and rows[0]["state"] == "open" and rows[0]["attempt"] == 0 and rows[0]["claim_owner"] is None and rows[0]["target_repo"] == "hfact-glas-proof" and rows[0]["harness_profile_ref"] == "harness.agent-dev-local@1.1.1" and rows[0]["repository_grant"] == {"version": "1", "allowed_paths": ["PROOF.md"], "commit_count": {"min": 1, "max": 1}, "publish": False} and rows[0]["triggering_event_id"] == trigger, "natural_queue_binding_refused") + + +def execute(bundle, resume=False): + from secrets_engine.openbao import OpenBaoClient + from secrets_engine.approval_consume import authorize_action + from secrets_engine.application_time import read_window + from secrets_engine.cli import build_parser + from secrets_engine.exec_owner import validate_delivery_target + from secrets_engine.plan import build_plan + + global RECEIPT_NAME + prior = None + if resume: + prior = json.loads((bundle / "execution.json").read_text()) + require(prior.get("phase") == "one_trigger_started" and prior.get("failure_code") == "natural_queue_binding_refused" and prior.get("trigger", {}).get("trigger_key") == "manual-dab6c4c7-db03-4887-a17b-9d99b752482e", "unexpected_prior_failure") + require([(r["catalog"], r["action"], r["approval_id"], r["exit_code"]) for r in prior["actions"]] == [(lane, action, IDS[lane][action], 0) for action in ("apply", "verify") for lane in IDS], "prior_native_actions_not_verified") + RECEIPT_NAME = "execution-resume.json" + require(not (bundle / RECEIPT_NAME).exists(), "prior_attempt_requires_reconciliation") + receipt = dict(status="failed", phase="preflight", observed_at=datetime.now(timezone.utc).isoformat(), actions=[], automatic_retry=False) + save(bundle, receipt) + payload = {} + directory = None + try: + require(run(["git", "-C", str(TARGET), "rev-parse", "HEAD"]) == BASE_HEAD and not run(["git", "-C", str(TARGET), "status", "--porcelain"]), "target_not_pristine") + require(json.loads((bundle / "installation.json").read_text())["status"] == "installed", "installation_receipt_required") + if resume: + validate_queued(queue_rows(), prior["trigger"]["trigger_key"]) + with sqlite3.connect(f"file:{OWNER}/spend-tool-proof-renewal-20260927.sqlite3?mode=ro", uri=True) as db: + require(all(db.execute("SELECT count(*) FROM " + table).fetchone()[0] == 0 for table in ("reservations", "request_routes", "request_reservations")), "prior_paid_dispatch_requires_reconciliation") + receipt.update(prior_receipt="execution.json", completed_actions_not_replayed=["provider/apply", "worker/apply", "provider/verify", "worker/verify", "queue/trigger"], trigger=prior["trigger"]) + else: + require(not queue_rows(), "metered_queue_not_empty") + runtime = Path("/run/user") / str(os.getuid()) + private(runtime, True) + require(run(["findmnt", "-n", "-o", "FSTYPE", "-T", str(runtime)]) == "tmpfs", "tmpfs_required") + with tempfile.TemporaryDirectory(prefix="metered-native-", dir=runtime) as name: + directory = Path(name) + private(directory, True) + payload = json.loads(sys.stdin.buffer.read(131073)) + require(set(payload) == {"client_secret", "negative_token", "backend_token", "pdp_token"}, "credential_envelope_invalid") + for key, filename in (("client_secret", "client-secret"), ("negative_token", "negative-token"), ("pdp_token", "pdp-caller")): + require(isinstance(payload[key], str) and 0 < len(payload[key]) < 32768, "credential_envelope_invalid") + write_new(directory / filename, payload.pop(key)) + os.environ["BAO_TOKEN"] = payload.pop("backend_token") + os.environ.pop("VAULT_TOKEN", None) + os.environ["BAO_ADDR"] = "http://127.0.0.1:28200" + trust = clock_admission(bundle, directory) + configs, entries = prepare_configs(bundle, directory, trust) + read_window(configs["exec"]) + command = entries[PROVIDER, "exec"].delivery_config["exec_owner"]["command"] + validate_delivery_target(entries[PROVIDER, "exec"], "ANTHROPIC_API_KEY", command, "exec-env") + client = OpenBaoClient.resolve(configs["exec"].bao_addr) + identity = json.loads(run([client.bao_bin, "token", "lookup", "-format=json"]))["data"] + policies = set(identity["policies"]) | set(identity.get("identity_policies", [])) + require("platform-admin" in policies and "root" not in policies and identity.get("entity_id") and 0 < identity["ttl"] <= 3600, "attended_operator_required") + # Refuse drift/existing state before any native consume. An already + # applied lane needs reconciliation, never a replay of this procedure. + for lane in IDS: + entry = entries[lane, "apply"] + if resume: + require(client.read_policy(entry.policy_name).strip() == build_plan(entry, "prod").policy_hcl.strip(), "applied_policy_drift") + role = json.loads(run([client.bao_bin, "read", "-format=json", "auth/approle/role/" + entry.role_name]))["data"] + require(all(role.get(k) == v for k, v in LIMITS.items()) and role.get("token_policies") == [entry.policy_name], "role_limits_drift") + else: + require(client.read_policy(entry.policy_name) is None and not client.approle_exists(entry.role_name), "existing_lane_requires_reconciliation") + for lane in IDS: + for action in (("exec",) if resume else IDS[lane]): + entry = entries[lane, action] + require(authorize_action(configs[action], entry, action, fields=() if action == "apply" else tuple(entry.fields), policy_targets=(entry.policy_name,), auth_targets=(entry.role_name,)) is not None, "native_authorization_missing") + receipt["phase"] = "remaining_exec_checks_passed" if resume else "all_six_native_checks_passed" + save(bundle, receipt) + for action in (() if resume else ("apply", "verify")): + for lane in IDS: + receipt["phase"] = lane + "_" + action + "_started" + save(bundle, receipt) + argv = ["apply", lane, "--stage", "prod", "--auth", "env"] if action == "apply" else ["verify", lane, "--auth", "env", "--negative-token-file", str(directory / "negative-token")] + args = build_parser().parse_args(argv) + with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()): + code = args.func(configs[action], args) + require(code == 0, "native_action_failed") + row = dict(catalog=lane, action=action, approval_id=IDS[lane][action], exit_code=code) + entry = entries[lane, action] + if action == "apply": + role = json.loads(run([client.bao_bin, "read", "-format=json", "auth/approle/role/" + entry.role_name]))["data"] + require(all(role.get(k) == v for k, v in LIMITS.items()) and role.get("token_policies") == [entry.policy_name], "role_limits_drift") + require(client.read_policy(entry.policy_name).strip() == build_plan(entry, "prod").policy_hcl.strip(), "applied_policy_drift") + row["limits"] = LIMITS + else: + row.update(verify_cleanup(client, entry, entries[WORKER if lane == PROVIDER else PROVIDER, action])) + receipt["actions"].append(row) + save(bundle, receipt) + # The owner triggers exactly once only after both lanes verify. + # Persist intent first; any uncertainty blocks re-trigger/re-exec. + if not resume: + receipt["phase"] = "one_trigger_started" + save(bundle, receipt) + from urllib.request import Request, urlopen + with urlopen(Request("http://127.0.0.1:8010/activity-definitions/" + DEFINITION + "/trigger", method="POST"), timeout=30) as response: + require(response.status == 202, "trigger_refused") + receipt["trigger"] = json.load(response) + for _ in range(20): + rows = queue_rows() + if rows: + break + time.sleep(1) + validate_queued(rows, receipt["trigger"]["trigger_key"]) + receipt["queued_run"] = rows[0] + receipt["phase"] = "one_exec_started" + save(bundle, receipt) + args = build_parser().parse_args(["exec", "--catalog", PROVIDER, "--auth", "env", "--mode", "exec-env", "--", *command]) + args.command = args.command[1:] + captured = io.StringIO() + with contextlib.redirect_stdout(captured), contextlib.redirect_stderr(io.StringIO()): + code = args.func(configs["exec"], args) + # Only the closed owner result schema may leave this envelope. + results = [] + for line in captured.getvalue().splitlines(): + try: + row = json.loads(line) + except ValueError: + continue + if isinstance(row, dict) and set(row) <= {"ok", "claimed", "empty", "run_id", "ops_state", "code"} and "ok" in row: + results.append(row) + receipt.update(exec_exit_code=code, owner_results=results, phase="one_exec_returned") + receipt["actions"].extend(dict(catalog=lane, action="exec", approval_id=IDS[lane]["exec"], exit_code=code) for lane in IDS) + receipt["status"] = "passed" if code == 0 and len(results) == 1 and results[0].get("claimed") and results[0].get("ok") else "attempt_failed" + receipt["private_runtime_removed"] = True + except Exception as error: + receipt["failure_type"] = type(error).__name__ + if type(error) is ValueError and str(error).replace("_", "").isalnum(): + receipt["failure_code"] = str(error) + finally: + payload.clear() + os.environ.pop("BAO_TOKEN", None) + if directory is not None: + receipt["private_runtime_removed"] = not directory.exists() + save(bundle, receipt) + return 0 if receipt["status"] == "passed" else 1 + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("mode", choices=("validate", "install", "execute", "resume")) + parser.add_argument("bundle", type=Path) + args = parser.parse_args() + if args.mode == "validate": + with tempfile.TemporaryDirectory() as directory: + prepare_configs(args.bundle, Path(directory)) + print("Six frozen request bindings verified") + elif args.mode == "install": + install_host(args.bundle) + else: + raise SystemExit(execute(args.bundle, resume=args.mode == "resume")) diff --git a/scripts/prove-metered-runtime.py b/scripts/prove-metered-runtime.py index d5e21c0..456c0d0 100644 --- a/scripts/prove-metered-runtime.py +++ b/scripts/prove-metered-runtime.py @@ -230,6 +230,8 @@ except OSError: readonly=True else: readonly=False print(json.dumps({'runtime_readonly':readonly,'python_prefix':sys.prefix, 'cli_version':subprocess.check_output(['claude','--version'],text=True).strip(), + 'entrypoints':{name:subprocess.run([name,'--help'],capture_output=True,timeout=15).returncode + for name in ('rein-aharness','glas-harness')}, 'private_absent':not Path(sys.argv[1]).exists(),'source_absent':not Path(sys.argv[2]).exists(), 'proxy_absent':not any('proxy' in k.lower() for k in os.environ), 'interfaces':[x.split(':')[0].strip() for x in Path('/proc/net/dev').read_text().splitlines()[2:]]})) @@ -239,6 +241,7 @@ print(json.dumps({'runtime_readonly':readonly,'python_prefix':sys.prefix, assert facts["runtime_readonly"] and facts["private_absent"] and facts["source_absent"] assert facts["proxy_absent"] and facts["interfaces"] == ["lo"] assert facts["cli_version"] == "2.1.266 (Claude Code)" + assert all(code == 0 for code in facts["entrypoints"].values()), json.dumps({"entrypoint_check_failed": facts["entrypoints"], "provider_requests": server.provider_calls-before}) adapter = AgenticClaudeCodeAdapter(workdir=Path(status.inputs["workspace_dir"]), cli_path="/opt/sandboxer/runtime/bin/claude", model=profile.model.model) diff --git a/scripts/reconcile-metered-queue-grant.py b/scripts/reconcile-metered-queue-grant.py new file mode 100644 index 0000000..6c38e39 --- /dev/null +++ b/scripts/reconcile-metered-queue-grant.py @@ -0,0 +1,70 @@ +"""Repair the one unclaimed proof row from its existing typed owner definition. + +Run inside the Activity Core owner process context, first without --apply. +No trigger, claim, inferred authority, retry, or new task is created. +""" +import asyncio +import json +import sys +import uuid + +RUN = "6efa9436-6a91-47c0-94e5-b31b5a0e7e3a" +DEFINITION = "5bae5505-77f1-5ba3-8dfa-2e10ed15531e" +TRIGGER = "manual-dab6c4c7-db03-4887-a17b-9d99b752482e" +GRANT = {"version": "1", "allowed_paths": ["PROOF.md"], "commit_count": {"min": 1, "max": 1}, "publish": False} + + +def validate(definition, row, live_worker_image): + if live_worker_image != "sha256:713bddad10a41950f446100a8b370fca8ccdd0b8969cccae751e3870c9c63ccd": + raise ValueError("grant_aware_worker_required") + if str(definition.id) != DEFINITION or definition.enabled or definition.version != 1: + raise ValueError("definition_drift") + rules = definition.rules_json + if len(rules) != 1 or rules[0]["id"] != "execute-hfact-glas-metered-proof" or rules[0]["condition"] != "True": + raise ValueError("rule_drift") + action = rules[0]["action"] + if action.get("repository_grant") != GRANT or action.get("target_repo") != "hfact-glas-proof" or action.get("harness_profile_ref") != "harness.agent-dev-local@1.1.1" or action.get("labels") != ["hfact-metered"]: + raise ValueError("typed_authority_drift") + expected = dict(id=RUN, activity_definition_id=DEFINITION, state="open", attempt=0, claim_owner=None, lease_until=None, target_repo="hfact-glas-proof", harness_profile_ref="harness.agent-dev-local@1.1.1", triggering_event_id=TRIGGER, source_type="rule", source_id=rules[0]["id"], repository_grant=None) + for field, value in expected.items(): + actual = getattr(row, field) + if field in ("id", "activity_definition_id"): + actual = str(actual) + if actual != value: + raise ValueError("queue_row_drift") + if row.description != action["description"] or row.title != action["task_template"] or row.labels != action["labels"]: + raise ValueError("task_content_drift") + return action["repository_grant"] + + +async def main(apply, image): + from sqlalchemy import select + from sqlalchemy.ext.asyncio import async_sessionmaker + from activity_core.db import make_engine + from activity_core.orm import ActivityDefinition, OpsRun + from activity_core.repository_grant import RepositoryGrant + engine = make_engine() + try: + async with async_sessionmaker(engine, expire_on_commit=False)() as session: + async with session.begin(): + definition = (await session.execute(select(ActivityDefinition).where(ActivityDefinition.id == uuid.UUID(DEFINITION)).with_for_update())).scalar_one() + row = (await session.execute(select(OpsRun).where(OpsRun.id == uuid.UUID(RUN)).with_for_update())).scalar_one() + grant = RepositoryGrant.model_validate(validate(definition, row, image)) + if grant.grant_id != "656911f7dff83e871434b415f0cee685": + raise ValueError("spend_grant_binding_mismatch") + if apply: + row.repository_grant = grant.payload() + receipt = dict(status="applied" if apply else "dry_run_passed", run_id=RUN, definition_id=DEFINITION, definition_version=1, triggering_event_id=TRIGGER, source="typed_existing_definition_rule", grant_id=grant.grant_id, grant=grant.payload(), attempt=0, claim_owner=None, new_trigger=False, new_task=False) + print(json.dumps(receipt, indent=2)) + finally: + await engine.dispose() + + +if __name__ == "__main__": + args = sys.argv[1:] + apply = args[:1] == ["--apply"] + if apply: + args = args[1:] + if len(args) != 1: + raise SystemExit("Supply the independently observed live worker image digest") + asyncio.run(main(apply, args[0])) diff --git a/tests/test_claim_loop.py b/tests/test_claim_loop.py index c3a6a67..425cb5b 100644 --- a/tests/test_claim_loop.py +++ b/tests/test_claim_loop.py @@ -535,6 +535,34 @@ def test_profile_refusal_fails_terminally_without_legacy_fallback( execute.assert_not_called() +def test_spend_refusal_closes_with_gateway_cleanup_evidence(tmp_path: Path) -> None: + from rein_aharness.glas_execution import GlasSpendError + + _repo, run, client = _profiled_case(tmp_path) + client.fail.return_value = OpsRun( + id=run.id, activity_definition_id="def", idempotency_key="k", + target_repo=run.target_repo, title=run.title, description="", state="failed", + ) + refusal = GlasSpendError( + "spend accounting refused: execution accounting requires reconciliation", + execution_evidence={ + "outcome": "failed", "failure_stage": "execution", + "session_cleanup": "succeeded", "sandbox_destroy": "succeeded", + "error": "private response", "provider_response": "private payload", + }, + ) + with patch("rein_aharness.claim_loop.execute_profiled_run", side_effect=refusal): + result = process_one(client) + assert result.ok is False + assert client.fail.call_args.kwargs["reopen"] is False + evidence = client.fail.call_args.kwargs["result"]["execution_evidence"] + assert evidence == { + "outcome": "failed", "failure_stage": "execution", + "session_cleanup": "succeeded", "sandbox_destroy": "succeeded", + } + client.complete.assert_not_called() + + def test_profiled_signal_cancellation_releases_lock_and_durably_fails( tmp_path: Path, ) -> None: diff --git a/tests/test_metered_native_procedure.py b/tests/test_metered_native_procedure.py new file mode 100644 index 0000000..8708697 --- /dev/null +++ b/tests/test_metered_native_procedure.py @@ -0,0 +1,53 @@ +"""Frozen action packet safety; requires the governed secrets-engine sibling.""" +import importlib.util +from pathlib import Path +import shutil + +import pytest + +pytest.importorskip("secrets_engine") +ROOT = Path(__file__).resolve().parents[1] +SOURCE = ROOT.parent / "secrets-engine/docs/proposals/glas-metered-tool-renewal-20260927" +spec = importlib.util.spec_from_file_location("metered_native", ROOT / "scripts/metered-native-owner.py") +native = importlib.util.module_from_spec(spec) +spec.loader.exec_module(native) + + +@pytest.fixture +def bundle(tmp_path): + path = tmp_path / "bundle" + shutil.copytree(SOURCE, path) + return path + + +def test_all_six_action_bindings_match(bundle, tmp_path): + private = tmp_path / "private" + private.mkdir() + configs, entries = native.prepare_configs(bundle, private) + assert len(entries) == 6 + for (lane, action), entry in entries.items(): + assert entry.approval["authorization_id"] == native.IDS[lane][action] + assert configs["exec"].clock_trust_file is None # validation is backend-free + + +@pytest.mark.parametrize("lane", [native.PROVIDER, native.WORKER]) +def test_recipient_drift_refused_before_auth(bundle, tmp_path, lane): + path = bundle / "catalog" / (lane + ".yaml") + path.write_text(path.read_text().replace("token_max_ttl: 15m", "token_max_ttl: 16m")) + private = tmp_path / "private" + private.mkdir() + with pytest.raises(ValueError, match="frozen_action_request_drift"): + native.prepare_configs(bundle, private) + + +def test_changed_packet_refused(bundle, tmp_path): + path = bundle / "native-pdp-inputs.json" + path.write_bytes(path.read_bytes() + b"\n") + with pytest.raises(ValueError, match="frozen_packet_drift"): + native.prepare_configs(bundle, tmp_path) + + +def test_execution_never_replays_prior_attempt(bundle): + (bundle / "execution.json").write_text('{"phase":"one_exec_started"}') + with pytest.raises(ValueError, match="prior_attempt_requires_reconciliation"): + native.execute(bundle) diff --git a/tests/test_metered_queue_reconciliation.py b/tests/test_metered_queue_reconciliation.py new file mode 100644 index 0000000..7f31f7f --- /dev/null +++ b/tests/test_metered_queue_reconciliation.py @@ -0,0 +1,46 @@ +"""The one-row repair cannot widen or synthesize mutation authority.""" +import importlib.util +from pathlib import Path +from types import SimpleNamespace +from copy import deepcopy + +import pytest + +spec = importlib.util.spec_from_file_location("repair", Path(__file__).resolve().parents[1] / "scripts/reconcile-metered-queue-grant.py") +repair = importlib.util.module_from_spec(spec) +spec.loader.exec_module(repair) +IMAGE = "sha256:713bddad10a41950f446100a8b370fca8ccdd0b8969cccae751e3870c9c63ccd" + + +@pytest.fixture +def records(): + action = dict(repository_grant=deepcopy(repair.GRANT), target_repo="hfact-glas-proof", harness_profile_ref="harness.agent-dev-local@1.1.1", labels=["hfact-metered"], description="bounded task", task_template="proof") + definition = SimpleNamespace(id=repair.DEFINITION, enabled=False, version=1, rules_json=[dict(id="execute-hfact-glas-metered-proof", condition="True", action=action)]) + row = SimpleNamespace(id=repair.RUN, activity_definition_id=repair.DEFINITION, state="open", attempt=0, claim_owner=None, lease_until=None, target_repo="hfact-glas-proof", harness_profile_ref="harness.agent-dev-local@1.1.1", triggering_event_id=repair.TRIGGER, source_type="rule", source_id="execute-hfact-glas-metered-proof", repository_grant=None, description="bounded task", title="proof", labels=["hfact-metered"]) + return definition, row + + +def test_copies_only_typed_existing_authority(records): + definition, row = records + assert repair.validate(definition, row, IMAGE) is definition.rules_json[0]["action"]["repository_grant"] + assert row.repository_grant is None + + +@pytest.mark.parametrize("field,value", [("state", "claimed"), ("attempt", 1), ("claim_owner", "worker"), ("repository_grant", repair.GRANT), ("triggering_event_id", "another-trigger"), ("description", "changed task")]) +def test_changed_or_used_row_refused(records, field, value): + definition, row = records + setattr(row, field, value) + with pytest.raises(ValueError): + repair.validate(definition, row, IMAGE) + + +def test_changed_definition_cannot_grant_more(records): + definition, row = records + definition.rules_json[0]["action"]["repository_grant"]["allowed_paths"] = ["**"] + with pytest.raises(ValueError, match="typed_authority_drift"): + repair.validate(definition, row, IMAGE) + + +def test_stale_worker_blocks_repair(records): + with pytest.raises(ValueError, match="grant_aware_worker_required"): + repair.validate(*records, "sha256:old") diff --git a/tests/test_owner_bootstrap.py b/tests/test_owner_bootstrap.py index a439b9d..53d47fe 100644 --- a/tests/test_owner_bootstrap.py +++ b/tests/test_owner_bootstrap.py @@ -42,6 +42,10 @@ def prepared(ledger, tmp_path, monkeypatch): runtime = tmp_path / "runtime" (runtime / "bin").mkdir(parents=True) (runtime / "bin/python3").write_bytes(b"not executed; fixture runtime structure") + for name in ("rein-aharness", "glas-harness"): + executable = runtime / "bin" / name + executable.write_text("#!/opt/sandboxer/runtime/bin/python3\n# fixture only\n") + executable.chmod(0o755) (runtime / "pyvenv.cfg").write_text("fixture only") messages = MessagesPolicy("fixture:upper-rate", profile.model.model, 1000, 1000, 1000, 1000) data = {"version": "1", "authority_ref": policy.authority_ref, @@ -62,7 +66,8 @@ def test_prepare_pins_without_key_or_claim(prepared, monkeypatch): @pytest.mark.parametrize("case", ["authority", "policy_digest", "model", "version", "unknown_field", - "duplicate", "public", "runtime_changed", "schema_missing"]) + "duplicate", "public", "runtime_changed", "schema_missing", + "build_host_launcher", "missing_launcher", "nonexecutable_launcher"]) def test_bad_bootstrap_refuses_before_claim(prepared, monkeypatch, capsys, case): path, config, data, runtime = prepared if case == "authority": data["authority_ref"] = "unrelated" @@ -71,6 +76,16 @@ def test_bad_bootstrap_refuses_before_claim(prepared, monkeypatch, capsys, case) elif case == "version": data["version"] = "2" elif case == "unknown_field": data["upstream_url"] = "https://not-admitted.invalid" elif case == "runtime_changed": (runtime / "added-file").write_text("changed") + elif case in {"build_host_launcher", "missing_launcher", "nonexecutable_launcher"}: + executable = runtime / "bin/rein-aharness" + if case == "build_host_launcher": + executable.write_text("#!/bin/sh\nexec /old-build/bin/python3\n") + elif case == "missing_launcher": + executable.unlink() + else: + executable.chmod(0o644) + # Even a correctly pinned malformed runtime must fail before claim. + data["runtime"]["sha256"] = runtime_digest(runtime) elif case == "schema_missing": import sqlite3 with sqlite3.connect(config.spend_ledger_path) as db: db.execute("DROP TABLE request_routes") diff --git a/tests/test_spend_admission.py b/tests/test_spend_admission.py index 6ed8f67..4134445 100644 --- a/tests/test_spend_admission.py +++ b/tests/test_spend_admission.py @@ -419,6 +419,29 @@ def test_uncertain_gateway_never_imports_and_blocks_next_claim(dispatch_case, fa store.preflight() +def test_accounting_refusal_preserves_stage_and_cleanup_without_raw_error(dispatch_case): + from rein_aharness.glas_execution import GlasSpendError, execute_profiled_run + + store, config, _catalog, transfer = dispatch_case + result = success(store, run(), None) + result["ok"] = False + result["evidence"].update( + outcome="failed", failure_stage="execution", sandbox_id="fixture-sandbox", + error="private provider response", provider_response="private payload", + ) + with pytest.raises(GlasSpendError) as caught: + execute_profiled_run(run(), config, gateway=lambda *a, **k: result, report_to_hub=False) + evidence = caught.value.execution_evidence + assert evidence["failure_stage"] == "execution" + assert evidence["sandbox_destroy"] == evidence["session_cleanup"] == "succeeded" + assert "private" not in json.dumps(evidence) + assert "error" not in evidence and "cost_usd" not in evidence + transfer.import_after_teardown.assert_not_called() + assert store.status()["reservations"][0]["state"] == "held" + with pytest.raises(SpendAdmissionError): + store.preflight() + + def test_cli_reconciliation_requires_termination_attestation(ledger, capsys): from rein_aharness.cli import main diff --git a/workplans/REINAH-WP-0003-governed-runtime-integrity.md b/workplans/REINAH-WP-0003-governed-runtime-integrity.md index f6c74ff..ae3cae6 100644 --- a/workplans/REINAH-WP-0003-governed-runtime-integrity.md +++ b/workplans/REINAH-WP-0003-governed-runtime-integrity.md @@ -985,3 +985,56 @@ backend delivery/consume, natural execution and recovery acceptance. T04's existing tenant-owner gates remain. The plan stays blocked; no new task or workplan was opened. The previous unsigned-spend/requester-mandate blockers above are superseded by the confirmed evidence in this return. + +### Native activation, one failed attempt, and implemented fixes — 2026-09-27 + +All six human dispositions were accepted and confirmed. Both scoped credential +lanes passed native apply and positive/negative verification, including sibling +and metadata denial and revoked-token rejection. Each of the six exact native +approvals was consumed once. Exec delivery sessions both revoked successfully; +the attended Warden sessions exited and no private credential files remain. + +Installed the approved renewed owner/spend pins after exclusively checking the +old ledger's three tables were empty. The old ledger and configuration backups +are preserved. No prior liability was discarded and old dispatch pins were retired. + +The single trigger exposed a deployed Activity Core mismatch: its older worker +dropped the definition's typed repository grant. Corrected only its image to +the already-deployed grant-aware API artifact, using the existing GitOps parent +and child. Source commits: Activity Core `428f2d7`, Platform `c3607ff`. Both +applications are healthy/synced. A guarded, row-locked repair copied only the +existing definition's exact grant to the same unclaimed attempt-zero job. It +created no task/trigger and did not infer authority from text. Nine guard tests +and 21 Activity Core carriage tests pass; actual image carriage checks pass. + +Resumed only the two unused exec approvals. Job +`6efa9436-6a91-47c0-94e5-b31b5a0e7e3a` reached attempt 1 and closed `failed` +with its lease cleared. No provider request reservation was created. The +installed `b6e4e8a4` artifact's uv shell launchers retained a vanished temporary +build interpreter. The real bwrap no-credential probe reproduces exit 127 for +rein and Glas with zero provider forwards. The prior direct-Claude fixture did +not exercise these Python entrypoints and cannot stand in for this evidence. + +Fixed Sand-boxer's builder to relocate direct and long-path/space-containing +shell launchers (commit `81b5fcf`, 12 builder tests and focused lint pass). +Added pre-claim launcher validation and actual in-sandbox entrypoint checks in +rein. Accounting refusal now preserves bounded gateway stage/cleanup evidence +without copying raw provider errors; it still holds the liability and prevents +artifact import or another claim. The approved immutable artifact was not edited. + +Postflight: original repository clean at `679d23e0`, lock available, sandbox +`b67907f1`/workspace removed, request route revoked, no pending/quarantined close +outbox records, and no private credential directories. The parent EUR 10 +reservation remains held for owner reconciliation; no billing figure or refund +is inferred. There was no retry, publication, extra queue job, or new workplan. + +Evidence: `docs/native-metered-proof.md` and +`docs/evidence/2026-09-27-metered-{acceptance-confirmations,native-consumes,native-execution,native-resume,postflight,queue-grant-reconciliation,renewal-installation}.json`; +worker rollout is `docs/evidence/2026-09-27-grant-aware-worker-rollout.json`. + +T05/T06 remain `wait`, and the workplan remains `blocked`: a corrected immutable +artifact/pin admission, reconciliation of the held reservation, and separately +authorized successful model/tool/commit acceptance remain. The accepted decisions +must not be requested again as though still pending; they are consumed historical +authority. T04's FI/Binky owner gates remain unchanged. These residuals stay in +the existing REINAH-WP-0003, SAND-WP-0015-T04 and SECRETS-WP-0009-T03 records.