diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d853286 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,22 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + with: + python-version: "3.12" + - run: python -m pip install . + - run: gearbox --help + - run: tools/verify diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d5ac19a --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +__pycache__/ +*.py[cod] +.coverage +.pytest_cache/ +.venv/ +build/ +dist/ +*.egg-info/ +evidence/local/ diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..8dafe96 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,63 @@ +# Architecture + +```text +primary developer + | + v +request + content-addressed authority + | + v +admission and gear selection + | + +----------------------+ + | | + v v +exact deterministic argv bounded helper transport + | + staged allowlisted context + | | + +----------+-----------+ + v + raw private evidence + | + external Context Firewall adapter + | + v +compact result + value receipt + operator indicator + | + v +primary developer retains completion ownership +``` + +## Internal modules + +The reference core currently keeps its small executable surface in +`src/opsle_gearbox/core.py`: + +- authority and policy validation; +- request and budget admission; +- safe source selection and staging; +- deterministic executor; +- injected helper transport boundary; +- output-contract and staged-change validation; +- compact result and Visible Value receipt generation. + +`src/opsle_gearbox/cli.py` provides the deterministic command-line interface. + +## External dependencies + +- **Context Firewall**: deterministic adapters for command/helper raw evidence. +- **Decision Evidence Protocol**: independent receipt/result conformance. +- **Agent Trajectory Profiler**: observational execution telemetry. +- **Routing Policy**: chooses a model/provider profile after cognitive + admission; the core policy currently binds an already selected profile. +- **Execution Authorization / Resource Claims**: stronger external authority + where a deployment requires capabilities, leases, or fencing. +- **Ephemeral Workers / Verifiable Handoff**: optional isolation and durable + artifact transfer. + +## Explicit exclusions + +The repository owns no objective graph, durable scheduler, queue, wakeup, +discovery, recovery ladder, global pause, persistent hierarchy, provider pool, +or product completion state. diff --git a/BENCHMARK.md b/BENCHMARK.md new file mode 100644 index 0000000..519df69 --- /dev/null +++ b/BENCHMARK.md @@ -0,0 +1,19 @@ +# Benchmark plan + +No benchmark-ready claim is made. + +Future controlled evaluation should freeze: + +- content-addressed task identities and source context; +- deterministic-eligible and bounded-cognitive task strata; +- direct-primary and admitted-Gearbox arms; +- exact model/provider/effort identities where cognition is used; +- an independent correctness and completion oracle; +- raw evidence and compact result identities; +- primary-model turns, provider-recorded tokens, provider sessions, elapsed + transport wait, escalations, retries, and operator intervention; +- randomized or blinded allocation appropriate to the task set. + +Correctness and authority violations gate every efficiency result. Ordinary +provider-free runs and value receipts remain observational. Byte measurements +must not be converted into token, cost, latency, or causal savings claims. diff --git a/LIMITATIONS.md b/LIMITATIONS.md new file mode 100644 index 0000000..8d7c713 --- /dev/null +++ b/LIMITATIONS.md @@ -0,0 +1,21 @@ +# Known limitations + +- No production cognitive/provider transport is bundled or accepted. +- Helper isolation is an injected-transport responsibility and is not proven by + the core interface. +- Context Firewall reduction is not bundled; the compact result intentionally + exposes only terminal facts, hashes, counts, and artifact locators. +- Symbol selection supports Python only. +- The reference JSON-schema validator implements a documented subset, not full + JSON Schema. +- Deterministic commands are exact policy entries rather than a portable command + catalog or semantic tool registry. +- Raw byte ceilings are evaluated after deterministic subprocess completion; + an authorized command can temporarily produce more bytes than its ceiling. +- Run idempotence is filesystem-local and does not provide distributed locking. +- The prototype does not apply staged helper writes to the source repository. +- No controlled benchmark establishes correctness preservation, intelligence + savings, context savings, latency reduction, monetary value, or avoided + provider sessions. +- A provider-session count of zero is direct telemetry only, not a counterfactual + savings claim. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..d800946 --- /dev/null +++ b/NOTICE @@ -0,0 +1,7 @@ +Agent Gearbox +Copyright (C) 2026 Taslos + +This work includes code adapted from the AGPL-3.0-licensed Taslos Tasks +implementation at commit 7734caf208366a0515cf4d78efc17a86363f2238. + +Public extraction and canonical boundary documentation are maintained by Opsle. diff --git a/PROVENANCE.md b/PROVENANCE.md new file mode 100644 index 0000000..5e6a6e9 --- /dev/null +++ b/PROVENANCE.md @@ -0,0 +1,31 @@ +# Provenance + +The public reference core was extracted on 2026-08-29 from the private active +Taslos Tasks repository at commit +`7734caf208366a0515cf4d78efc17a86363f2238`. + +The predecessor Gearbox entered that repository in commit +`80f0830e26b938f6abe8fc688b9b1f49283ef34f` under `ops/gearbox/`. The reusable +algorithms adapted here include: + +- strict request/key validation; +- exact repository identity and normalized path checks; +- content-addressed file, line, symbol, and new-file selections; +- safe file reads and source-drift checks; +- deterministic Python symbol range resolution; +- private raw artifacts and compact result accounting; +- provider-session, command, context, output, timeout, and cleanup bounds; +- one blocking delivery with no retry or fallback. + +The public implementation was rewritten around the canonical primary-developer +Gearbox boundary established by `opsle/research` PR #7. It does not copy or +publish Taslos Tasks product code, databases, credentials, production state, +acceptance transcripts, private evidence, systemd units, installed Codex schema +paths, app-server configuration, or Durable Supervisor machinery. + +The predecessor source is AGPL-3.0. This derived repository therefore preserves +AGPL-3.0-only licensing and records the original copyright in `NOTICE`. + +Git history remains attributable through the exact source and introduction +commits above. Future extraction should cite both this public revision and the +private source revision when authorized to do so. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2d98f90 --- /dev/null +++ b/README.md @@ -0,0 +1,122 @@ +# Agent Gearbox + +> Stop using intelligence for work that does not require intelligence. + +Agent Gearbox lets a powerful primary developer delegate routine operations and +bounded work to deterministic software or less expensive models, then receive +only the compact result needed to continue. + +This repository contains the public provider-free reference core. It is a +`PROTOTYPED` mechanism, not evidence of comparative benefit or production +readiness. + +## What exists + +- exact, content-addressed authority policies; +- deterministic-versus-helper gear admission; +- exact model, effort, provider-session, command, context, raw-output, return, + and timeout budgets; +- deterministic commands admitted by exact argv rather than shell text; +- content-addressed `file`, `lines`, `symbols`, and `new` context selections; +- private staged workspaces that never write into the source repository; +- one injected helper transport, one attempt, no retry or fallback; +- raw artifact hashes and locators outside the compact result; +- one compact result on stdout and a named operator indicator on stderr; +- an `opsle.value-receipt.v1` sidecar with bounded observational claims. + +The bundled CLI executes deterministic gears only. Cognitive execution requires +a separately supplied `HelperTransport` integration. The core rejects a helper +request when no transport is present; it never silently falls back. + +## What does not exist + +Gearbox is not a durable supervisor, queue, scheduler, discovery engine, +retry/recovery controller, persistent agent hierarchy, exact-session resume +mechanism, general autonomous-task platform, or provider router. + +Context Firewall is an external integration. Gearbox decides where bounded work +executes; Context Firewall decides what decision-relevant evidence returns. The +reference core returns hashes, terminal facts, artifact locators, and escalation +state without embedding raw process output. + +## Request shape + +```json +{ + "schema": "opsle.gearbox.request.v1", + "task": { + "argv": ["git", "status", "--short", "--branch"] + }, + "task_type": "git", + "requested_gear": "git-status", + "allowed_context": { + "repository": "/absolute/git/root", + "selections": [], + "writable_paths": [] + }, + "output_contract": { + "schema": { + "type": "object", + "properties": {}, + "required": [] + } + }, + "authority": { + "policy_path": "/absolute/reviewed-policy.json", + "policy_sha256": "lowercase-sha256" + }, + "budget": { + "timeout_seconds": 30, + "max_raw_bytes": 1048576, + "max_return_bytes": 8192, + "max_context_bytes": 0, + "max_commands": 1, + "max_provider_sessions": 0 + } +} +``` + +Policies bind a gear name to one kind, authorized task types, and either an +exact deterministic argv plus executable path/hash or a helper +transport/model/effort/session profile. The request binds the policy by SHA-256. + +## CLI + +After reviewing and hashing a policy and request: + +```text +PYTHONPATH=src python3 -m opsle_gearbox.cli \ + --request /private/request.json \ + --state /private/gearbox-state \ + --receipt /private/value-receipt.json +``` + +Canonical result JSON is written to stdout. The concise `[Gearbox] ...` +indicator is written to stderr. Raw stdout/stderr and helper evidence stay under +the mode-0700 state root. + +## Verification + +```text +tools/verify +``` + +The suite is provider-free and covers deterministic execution, idempotence, +authority drift, exact command admission, context hashing and selection, +symlink/sensitive-path rejection, helper transport absence, model/effort, +command and context budgets, staged-only writes, termination, compact result, +Visible Value receipt, and stdout/stderr separation. + +## Documents + +- [THEORY.md](THEORY.md) — falsifiable problem and mechanism. +- [SPEC.md](SPEC.md) — normative request, execution, and result contract. +- [ARCHITECTURE.md](ARCHITECTURE.md) — component and dependency boundaries. +- [SECURITY.md](SECURITY.md) — trust assumptions and fail-closed behavior. +- [LIMITATIONS.md](LIMITATIONS.md) — current evidence ceiling. +- [PROVENANCE.md](PROVENANCE.md) — source extraction and licensing record. +- [BENCHMARK.md](BENCHMARK.md) — future controlled evidence plan. + +## License + +AGPL-3.0-only. See [LICENSE](LICENSE) and [NOTICE](NOTICE). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..586d2ca --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,43 @@ +# Security boundary + +## Trusted inputs + +- the primary developer or reviewed caller; +- the exact authority-policy bytes bound by SHA-256; +- the local operating system and Python runtime; +- the injected helper transport and any isolation it claims to enforce. + +## Untrusted inputs + +- task requests before admission; +- source repositories and selected files; +- deterministic command output; +- helper stdout, stderr, mutations, and final result; +- stale, malformed, partial, or over-budget evidence. + +## Enforced by this core + +- strict request and policy fields; +- exact deterministic argv; +- no shell interpolation; +- one attempt and no fallback; +- exact repository identity; +- normalized, non-sensitive, non-symlink content paths; +- source hashes and revalidation; +- helper context and write allowlists; +- model, effort, command, provider-session, output, and cleanup checks; +- raw evidence retained outside the compact return; +- terminal escalation on drift or uncertainty. + +## Not enforced by this core + +The `HelperTransport` boundary cannot itself prove filesystem, network, +credential, subprocess, provider, or model isolation. A production transport +MUST enforce those controls and SHOULD emit independently verifiable receipts. +No production helper transport is bundled in version 0.1.0. + +Deterministic policy authors are responsible for choosing commands that are safe +for the declared task. Exact admission prevents request expansion; it does not +make an unsafe authorized command safe. + +Report vulnerabilities privately through GitHub's security advisory interface. diff --git a/SPEC.md b/SPEC.md new file mode 100644 index 0000000..3fb9577 --- /dev/null +++ b/SPEC.md @@ -0,0 +1,124 @@ +# Agent Gearbox specification + +Status: normative draft +Version: `opsle.gearbox.request.v1` / `opsle.gearbox.result.v1` + +Normative terms **MUST**, **MUST NOT**, **SHOULD**, and **MAY** are used in their +ordinary specification sense. + +## Request + +A request MUST contain exactly: + +- `schema`; +- `task`; +- `task_type`; +- `requested_gear`; +- `allowed_context`; +- `output_contract`; +- `authority`; +- `budget`. + +Unknown fields MUST fail admission. + +### Authority + +`authority.policy_path` MUST identify an absolute, regular, non-symlink JSON +file. `authority.policy_sha256` MUST match its bytes. The policy MUST use +`opsle.gearbox.policy.v1`, name an `authority_id`, and define at least one gear. + +Policy drift MUST fail before execution. A request cannot authorize a new gear, +task type, command, model, effort, or provider-session count. + +### Gear admission + +A deterministic gear binds one or more task types to one exact argv array, an +absolute or policy-relative executable path, and the executable's SHA-256. No +shell interpolation or request-supplied command variation is admitted. The +executable is revalidated before launch. + +A helper gear binds task types to an exact transport identity, model, reasoning +effort, and maximum provider-session count. The helper transport is injected by +the caller and MUST be independently trusted to enforce its isolation and +provider contract. + +Gearbox MUST invoke at most one gear and MUST NOT retry, fall back, recursively +delegate, or create discovered work. + +### Context + +`allowed_context.repository` MUST be the exact Git root. A selection is one of: + +- `file`: complete file with admitted SHA-256; +- `lines`: ordered, non-overlapping ranges with admitted source SHA-256; +- `symbols`: unique qualified Python symbols with admitted source SHA-256; +- `new`: absent path explicitly declared writable. + +Paths MUST be normalized repository-relative paths. Sensitive names, symlinks, +hard-linked files, boundary traversal, ambiguous/missing symbols, source drift, +non-UTF-8 partial selections, and hard-ceiling overflow MUST fail closed. + +Only complete `file` and `new` selections MAY be writable. Writes occur only in +the staged workspace; this core never applies them to the source repository. +Unauthorized path creation or read-only mutation MUST fail the run. + +### Budgets + +The request MUST give literal bounds for timeout, raw bytes, compact return +bytes, context bytes, commands, and provider sessions. The provider-session +budget MUST equal the authorized gear profile. Missing observations MUST NOT be +invented as zeros, except where the selected implementation mechanically proves +the count (for example, the bundled deterministic executor proves zero provider +sessions). + +### Output contract + +The helper final MUST match the declared object schema. This reference validates +object properties, required fields, additional properties, enum values, arrays, +maximum item counts, strings, and maximum lengths. Unsupported schema behavior +MUST NOT be treated as validated. + +## Execution + +The normalized request hash determines the run identity. A completed identical +run MAY return its durable result. An incomplete run MUST NOT be duplicated. + +Raw output MUST be retained under the private state root and MUST NOT appear in +the compact result. The executor waits through the process or injected transport +without consuming primary model turns. Timeout or uncertain cleanup requires a +terminal failure or escalation. + +The helper's source context MUST be revalidated immediately before transport +execution. The transport MUST report model, effort, command count, provider +sessions, and termination state. Drift or budget excess MUST fail without retry. +The transport request MUST NOT expose the original repository path; it receives +the staged workspace separately and a context-manifest hash. + +## Result + +The compact result contains: + +- request, policy, run, authority, and gear identities; +- one terminal status and bounded summary; +- exit status where applicable; +- changed staged paths; +- raw artifact locators, byte counts, and SHA-256 hashes; +- observed execution, fallback, provider-session, command, cleanup, wait, and + raw-byte metrics; +- escalation state and reason; +- context packet accounting for helper runs; +- a value-receipt locator; +- a deterministic result hash. + +The complete encoded result MUST fit `max_return_bytes`; otherwise delivery MUST +fail rather than truncate semantic fields. + +## Visible Value + +Every completed core execution writes an `opsle.value-receipt.v1` sidecar. Exact +measurements cover directly verified counts and byte lengths. Terminal status +and wait mode are observational states. Zero provider sessions MUST NOT be +reported as sessions saved or avoided. + +The CLI writes canonical result JSON to stdout and one stable `[Gearbox]` +indicator to stderr. diff --git a/THEORY.md b/THEORY.md new file mode 100644 index 0000000..939a61b --- /dev/null +++ b/THEORY.md @@ -0,0 +1,54 @@ +# Theory + +## Problem + +A powerful primary developer spends scarce reasoning capacity and model context +on deterministic operations or narrowly bounded work that software or a less +expensive model could perform without taking over end-to-end project ownership. + +## Hypothesis + +A strict transmission boundary can preserve the primary developer's ownership +while moving only admitted work into a cheaper execution gear and returning one +compact, evidence-addressable result. + +## Irreducible mechanism + +One `gearbox_run` operation: + +1. binds exact authority and immutable policy identity; +2. validates the requested gear, task type, model, effort, context, output, and + literal budgets; +3. chooses deterministic execution when cognition is unnecessary; +4. stages only content-addressed context for a cognitive helper; +5. invokes at most one authorized execution transport; +6. waits outside primary model turns; +7. retains raw evidence outside the compact result; +8. validates a typed terminal result and helper cleanup; +9. performs no autonomous retry or fallback; +10. emits visible, evidence-bounded telemetry. + +## Ownership invariant + +The primary developer retains project understanding, architectural judgment, +safety decisions, ambiguity resolution, integration responsibility, and the +final determination of completion. Gearbox owns one bounded call, never the +project objective. + +## Disconfirmation + +The theory would be weakened or rejected if controlled evidence shows that: + +- bounded delegation cannot preserve correctness under its declared scope; +- admission, context, or result contracts routinely require the full project + context they are intended to avoid; +- deterministic gears produce more escalation or operator burden than direct + execution; +- helper termination, authority, or evidence completeness cannot be verified; +- the primary developer must reconstruct substantial hidden work before safely + continuing; or +- the mechanism's overhead dominates its measured value across the intended + task classes. + +The current prototype establishes feasibility only. It does not establish any +correctness, token, latency, cost, or causal benefit. diff --git a/evidence/release-001/README.md b/evidence/release-001/README.md new file mode 100644 index 0000000..0cc952f --- /dev/null +++ b/evidence/release-001/README.md @@ -0,0 +1,10 @@ +# Release 001 evidence + +This directory is generated by `tools/dogfood` from a provider-free, +deterministic Gearbox run. `verification.json` binds the public result, value +receipt, authority policy, and implementation revision. The `runs/` subtree +retains the raw artifacts at the locators named by the compact result. + +The receipt contains exact counts and byte measurements plus observed terminal +state. It makes no token, cost, latency, correctness, comparative, or causal +savings claim. diff --git a/evidence/release-001/result.json b/evidence/release-001/result.json new file mode 100644 index 0000000..88ecf89 --- /dev/null +++ b/evidence/release-001/result.json @@ -0,0 +1 @@ +{"artifacts":[{"bytes":55,"kind":"RAW_STDOUT","locator":"runs/ga587483b4894867066c18309/raw/stdout.raw","sha256":"ea0aaede35a70dfa761c635df6e251b7ef5fd2bf6ae6bf230ab5ccf5d20b7c6d"},{"bytes":0,"kind":"RAW_STDERR","locator":"runs/ga587483b4894867066c18309/raw/stderr.raw","sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}],"authority_id":"opsle.gearbox.release-fixture/v1","changed_paths":[],"context":null,"escalation":{"reason":null,"required":false},"exit_code":0,"gear":"release-fixture","gear_kind":"deterministic","metrics":{"commands":1,"execution_attempts":1,"fallback_attempts":0,"helper_terminated":true,"primary_wait_mode":"blocking_subprocess_wait","provider_sessions":0,"provider_sessions_verified":true,"raw_evidence_bytes":55},"policy_sha256":"768139d05b5910cd21c99d9cf1a5e0832ff5e4999ea831f02069ac52250c1670","request_sha256":"a587483b4894867066c18309d24f28e3b1bf2a54d3fe22626396a531f65f3e2b","result_sha256":"663e3561ecd31016228ef99f6aa9a0aa9db66874721828daf1d5e9a0a17d57e6","run_id":"ga587483b4894867066c18309","schema":"opsle.gearbox.result.v1","status":"completed","summary":"deterministic command completed","value_receipt":"runs/ga587483b4894867066c18309/value-receipt.json"} diff --git a/evidence/release-001/runs/ga587483b4894867066c18309/raw/stderr.raw b/evidence/release-001/runs/ga587483b4894867066c18309/raw/stderr.raw new file mode 100644 index 0000000..e69de29 diff --git a/evidence/release-001/runs/ga587483b4894867066c18309/raw/stdout.raw b/evidence/release-001/runs/ga587483b4894867066c18309/raw/stdout.raw new file mode 100644 index 0000000..41ba8ea --- /dev/null +++ b/evidence/release-001/runs/ga587483b4894867066c18309/raw/stdout.raw @@ -0,0 +1 @@ +{"fixture":"opsle-gearbox-release-v1","status":"PASS"} diff --git a/evidence/release-001/runs/ga587483b4894867066c18309/value-receipt.json b/evidence/release-001/runs/ga587483b4894867066c18309/value-receipt.json new file mode 100644 index 0000000..58b0e66 --- /dev/null +++ b/evidence/release-001/runs/ga587483b4894867066c18309/value-receipt.json @@ -0,0 +1 @@ +{"evidence":[{"id":"compact_result","kind":"CONTENT_HASH","locator":"sha256:663e3561ecd31016228ef99f6aa9a0aa9db66874721828daf1d5e9a0a17d57e6","trust":"VERIFIED"}],"limitations":["No token, cost, latency, correctness, or causal savings claim is made.","A zero provider-session count is observed execution telemetry, not proof that a provider session would otherwise have occurred.","Helper isolation and provider accounting depend on the separately supplied transport.","Context Firewall reduction is an external integration and was not performed by this core run."],"measurements":[{"aggregation":{"method":"SUM","safe":true},"baseline":null,"class":"EXACT","delta":null,"derivation":null,"direction":"NEUTRAL","evidence_refs":["compact_result"],"id":"execution_attempts","limitations":[],"operator_display":false,"result":1,"source_verification":"VERIFIED","unit":"count"},{"aggregation":{"method":"SUM","safe":true},"baseline":null,"class":"EXACT","delta":null,"derivation":null,"direction":"NEUTRAL","evidence_refs":["compact_result"],"id":"fallback_attempts","limitations":[],"operator_display":false,"result":0,"source_verification":"VERIFIED","unit":"count"},{"aggregation":{"method":"SUM","safe":true},"baseline":null,"class":"EXACT","delta":null,"derivation":null,"direction":"NEUTRAL","evidence_refs":["compact_result"],"id":"provider_sessions","limitations":[],"operator_display":true,"result":0,"source_verification":"VERIFIED","unit":"count"},{"aggregation":{"method":"SUM","safe":true},"baseline":null,"class":"EXACT","delta":null,"derivation":null,"direction":"NEUTRAL","evidence_refs":["compact_result"],"id":"raw_evidence_bytes","limitations":[],"operator_display":false,"result":55,"source_verification":"VERIFIED","unit":"byte"},{"aggregation":{"method":"SUM","safe":true},"baseline":null,"class":"EXACT","delta":null,"derivation":null,"direction":"NEUTRAL","evidence_refs":["compact_result"],"id":"model_visible_result_bytes","limitations":[],"operator_display":true,"result":1223,"source_verification":"VERIFIED","unit":"byte"},{"aggregation":{"method":null,"safe":false},"baseline":null,"class":"OBSERVED","delta":null,"derivation":null,"direction":"PROTECTION_SIGNAL","evidence_refs":["compact_result"],"id":"terminal_status","limitations":[],"operator_display":true,"result":"completed","source_verification":"OBSERVED","unit":"state"},{"aggregation":{"method":null,"safe":false},"baseline":null,"class":"OBSERVED","delta":null,"derivation":null,"direction":"NEUTRAL","evidence_refs":["compact_result"],"id":"primary_wait_mode","limitations":[],"operator_display":false,"result":"blocking_subprocess_wait","source_verification":"OBSERVED","unit":"state"}],"mechanism":{"id":"opsle.gearbox","name":"Agent Gearbox","revision":"6005b340a6f6fb3f8683439d6b5fd154e1fd253f","version":"0.1.0"},"operation":{"configuration_id":"sha256:768139d05b5910cd21c99d9cf1a5e0832ff5e4999ea831f02069ac52250c1670","id":"ga587483b4894867066c18309","name":"bounded-gear-execution","policy_id":"opsle.gearbox.release-fixture/v1"},"run":{"id":"ga587483b4894867066c18309"},"schema":"opsle.value-receipt.v1"} diff --git a/evidence/release-001/value-receipt.json b/evidence/release-001/value-receipt.json new file mode 100644 index 0000000..58b0e66 --- /dev/null +++ b/evidence/release-001/value-receipt.json @@ -0,0 +1 @@ +{"evidence":[{"id":"compact_result","kind":"CONTENT_HASH","locator":"sha256:663e3561ecd31016228ef99f6aa9a0aa9db66874721828daf1d5e9a0a17d57e6","trust":"VERIFIED"}],"limitations":["No token, cost, latency, correctness, or causal savings claim is made.","A zero provider-session count is observed execution telemetry, not proof that a provider session would otherwise have occurred.","Helper isolation and provider accounting depend on the separately supplied transport.","Context Firewall reduction is an external integration and was not performed by this core run."],"measurements":[{"aggregation":{"method":"SUM","safe":true},"baseline":null,"class":"EXACT","delta":null,"derivation":null,"direction":"NEUTRAL","evidence_refs":["compact_result"],"id":"execution_attempts","limitations":[],"operator_display":false,"result":1,"source_verification":"VERIFIED","unit":"count"},{"aggregation":{"method":"SUM","safe":true},"baseline":null,"class":"EXACT","delta":null,"derivation":null,"direction":"NEUTRAL","evidence_refs":["compact_result"],"id":"fallback_attempts","limitations":[],"operator_display":false,"result":0,"source_verification":"VERIFIED","unit":"count"},{"aggregation":{"method":"SUM","safe":true},"baseline":null,"class":"EXACT","delta":null,"derivation":null,"direction":"NEUTRAL","evidence_refs":["compact_result"],"id":"provider_sessions","limitations":[],"operator_display":true,"result":0,"source_verification":"VERIFIED","unit":"count"},{"aggregation":{"method":"SUM","safe":true},"baseline":null,"class":"EXACT","delta":null,"derivation":null,"direction":"NEUTRAL","evidence_refs":["compact_result"],"id":"raw_evidence_bytes","limitations":[],"operator_display":false,"result":55,"source_verification":"VERIFIED","unit":"byte"},{"aggregation":{"method":"SUM","safe":true},"baseline":null,"class":"EXACT","delta":null,"derivation":null,"direction":"NEUTRAL","evidence_refs":["compact_result"],"id":"model_visible_result_bytes","limitations":[],"operator_display":true,"result":1223,"source_verification":"VERIFIED","unit":"byte"},{"aggregation":{"method":null,"safe":false},"baseline":null,"class":"OBSERVED","delta":null,"derivation":null,"direction":"PROTECTION_SIGNAL","evidence_refs":["compact_result"],"id":"terminal_status","limitations":[],"operator_display":true,"result":"completed","source_verification":"OBSERVED","unit":"state"},{"aggregation":{"method":null,"safe":false},"baseline":null,"class":"OBSERVED","delta":null,"derivation":null,"direction":"NEUTRAL","evidence_refs":["compact_result"],"id":"primary_wait_mode","limitations":[],"operator_display":false,"result":"blocking_subprocess_wait","source_verification":"OBSERVED","unit":"state"}],"mechanism":{"id":"opsle.gearbox","name":"Agent Gearbox","revision":"6005b340a6f6fb3f8683439d6b5fd154e1fd253f","version":"0.1.0"},"operation":{"configuration_id":"sha256:768139d05b5910cd21c99d9cf1a5e0832ff5e4999ea831f02069ac52250c1670","id":"ga587483b4894867066c18309","name":"bounded-gear-execution","policy_id":"opsle.gearbox.release-fixture/v1"},"run":{"id":"ga587483b4894867066c18309"},"schema":"opsle.value-receipt.v1"} diff --git a/evidence/release-001/verification.json b/evidence/release-001/verification.json new file mode 100644 index 0000000..79783ec --- /dev/null +++ b/evidence/release-001/verification.json @@ -0,0 +1 @@ +{"fallback_attempts":0,"mechanism_revision":"6005b340a6f6fb3f8683439d6b5fd154e1fd253f","policy_sha256":"768139d05b5910cd21c99d9cf1a5e0832ff5e4999ea831f02069ac52250c1670","provider_sessions":0,"raw_artifacts":[{"bytes":55,"locator":"runs/ga587483b4894867066c18309/raw/stdout.raw","sha256":"ea0aaede35a70dfa761c635df6e251b7ef5fd2bf6ae6bf230ab5ccf5d20b7c6d"},{"bytes":0,"locator":"runs/ga587483b4894867066c18309/raw/stderr.raw","sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}],"request_sha256":"a587483b4894867066c18309d24f28e3b1bf2a54d3fe22626396a531f65f3e2b","result_sha256":"06c62c5727a33f53e3f0ee1b4b02b2ba9efd007e9c2c1060770f39ef7646eec9","schema":"opsle.gearbox.release-evidence.v1","status":"completed","test_command":"tools/verify","value_measurement_count":7,"value_receipt_sha256":"114cf24a4b3cf256c545fe477393bce49c22fef92475934c8230499b7da57bed"} diff --git a/examples/release-policy.json b/examples/release-policy.json new file mode 100644 index 0000000..ae20e30 --- /dev/null +++ b/examples/release-policy.json @@ -0,0 +1,13 @@ +{ + "schema": "opsle.gearbox.policy.v1", + "authority_id": "opsle.gearbox.release-fixture/v1", + "gears": { + "release-fixture": { + "kind": "deterministic", + "task_types": ["fixture"], + "argv_exact": ["release-fixture"], + "executable_path": "../tools/release-fixture", + "executable_sha256": "a7e7592b784f49087cc5164f718524b8e4ba36081c6c1f77a98744cbc6b80c7e" + } + } +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..761c4b8 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,22 @@ +[build-system] +requires = ["setuptools>=77"] +build-backend = "setuptools.build_meta" + +[project] +name = "opsle-gearbox" +version = "0.1.0" +description = "Bounded deterministic and lower-cost execution for a primary developer" +readme = "README.md" +requires-python = ">=3.11" +license = "AGPL-3.0-only" +license-files = ["LICENSE", "NOTICE"] +authors = [{name = "Opsle"}] +classifiers = [ + "Programming Language :: Python :: 3", +] + +[project.scripts] +gearbox = "opsle_gearbox.cli:main" + +[tool.setuptools.packages.find] +where = ["src"] diff --git a/src/opsle_gearbox/__init__.py b/src/opsle_gearbox/__init__.py new file mode 100644 index 0000000..b386eee --- /dev/null +++ b/src/opsle_gearbox/__init__.py @@ -0,0 +1,19 @@ +"""Public Opsle Gearbox contracts and provider-free reference runtime.""" + +from .core import ( + VERSION, + GearboxError, + GearboxRunner, + HelperTransport, + canonical_json, + sha256_file, +) + +__all__ = [ + "VERSION", + "GearboxError", + "GearboxRunner", + "HelperTransport", + "canonical_json", + "sha256_file", +] diff --git a/src/opsle_gearbox/cli.py b/src/opsle_gearbox/cli.py new file mode 100644 index 0000000..f76e628 --- /dev/null +++ b/src/opsle_gearbox/cli.py @@ -0,0 +1,54 @@ +"""Command-line entrypoint with canonical stdout and operator-only stderr.""" + +from __future__ import annotations + +import argparse +import json +import shutil +import sys +from pathlib import Path + +from .core import ( + GearboxError, + GearboxRunner, + canonical_json, + operator_indicator, + read_object, +) + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--request", type=Path, required=True) + parser.add_argument("--state", type=Path, required=True) + parser.add_argument("--receipt", type=Path) + parser.add_argument("--mechanism-revision") + return parser + + +def main(argv: list[str] | None = None) -> int: + args = build_parser().parse_args(argv) + try: + request = read_object(args.request) + runner = GearboxRunner(args.state, mechanism_revision=args.mechanism_revision) + result = runner.run(request) + run_dir = args.state.resolve() / "runs" / result["run_id"] + receipt_path = run_dir / "value-receipt.json" + receipt = read_object(receipt_path) + if args.receipt: + args.receipt.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(receipt_path, args.receipt) + sys.stdout.buffer.write(canonical_json(result)) + print(operator_indicator(result, receipt), file=sys.stderr) + return 0 if result["status"] == "completed" else 2 + except (GearboxError, OSError, json.JSONDecodeError) as exc: + sys.stdout.buffer.write(canonical_json({ + "schema": "opsle.gearbox.error.v1", + "status": "failed", + "error": str(exc), + })) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/opsle_gearbox/core.py b/src/opsle_gearbox/core.py new file mode 100644 index 0000000..f639f85 --- /dev/null +++ b/src/opsle_gearbox/core.py @@ -0,0 +1,1117 @@ +"""Portable Agent Gearbox core. + +The runtime executes one admitted gear exactly once. Raw evidence stays in a +private state directory; the caller receives one compact terminal result. +Provider-specific helper transports and Context Firewall adapters are external. +""" + +from __future__ import annotations + +import ast +import hashlib +import json +import os +import re +import shutil +import signal +import stat +import subprocess +import tempfile +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Protocol + +VERSION = "0.1.0" +REQUEST_SCHEMA = "opsle.gearbox.request.v1" +POLICY_SCHEMA = "opsle.gearbox.policy.v1" +RESULT_SCHEMA = "opsle.gearbox.result.v1" +VALUE_SCHEMA = "opsle.value-receipt.v1" +SHA256_RE = re.compile(r"^[0-9a-f]{64}$") +SYMBOL_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_.]*$") +SENSITIVE_PATH = re.compile( + r"(^|/)(\.env(?:\..*)?|auth\.json|credentials?|secrets?|id_[er]sa|.*\.pem)$", + re.IGNORECASE, +) + + +class GearboxError(RuntimeError): + """A fail-closed request, policy, execution, or evidence error.""" + + +class HelperTransport(Protocol): + """External bounded cognitive transport. + + Implementations must return a mapping with final, stdout, stderr, + provider_sessions, commands, terminated, model, effort, and transport_id. + Gearbox never retries or selects an alternate transport. + """ + + def execute( + self, + *, + request: dict[str, Any], + workspace: Path, + timeout_seconds: int, + ) -> dict[str, Any]: ... + + +def canonical_json(value: Any) -> bytes: + return ( + json.dumps(value, ensure_ascii=False, separators=(",", ":"), sort_keys=True) + + "\n" + ).encode("utf-8") + + +def sha256_bytes(value: bytes) -> str: + return hashlib.sha256(value).hexdigest() + + +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def atomic_write(path: Path, value: bytes, mode: int = 0o600) -> None: + path.parent.mkdir(parents=True, exist_ok=True, mode=0o700) + descriptor, temporary_name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent) + temporary = Path(temporary_name) + try: + os.fchmod(descriptor, mode) + with os.fdopen(descriptor, "wb") as handle: + handle.write(value) + handle.flush() + os.fsync(handle.fileno()) + os.replace(temporary, path) + finally: + temporary.unlink(missing_ok=True) + + +def read_object(path: Path) -> dict[str, Any]: + try: + value = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + raise GearboxError(f"invalid JSON object: {path}") from exc + if not isinstance(value, dict): + raise GearboxError(f"JSON value is not an object: {path}") + return value + + +def require_keys( + value: dict[str, Any], required: set[str], allowed: set[str], label: str +) -> None: + missing = required - set(value) + unknown = set(value) - allowed + if missing: + raise GearboxError(f"{label} is missing: {', '.join(sorted(missing))}") + if unknown: + raise GearboxError(f"{label} has unknown fields: {', '.join(sorted(unknown))}") + + +def repository_identity(value: object) -> Path: + if not isinstance(value, str): + raise GearboxError("allowed_context.repository must be a path") + repository = Path(value).resolve() + try: + top = subprocess.run( + ["git", "-C", str(repository), "rev-parse", "--show-toplevel"], + check=True, + capture_output=True, + text=True, + timeout=10, + ).stdout.strip() + except (OSError, subprocess.SubprocessError) as exc: + raise GearboxError("repository identity is unavailable") from exc + if Path(top).resolve() != repository: + raise GearboxError("repository must be the exact Git root") + return repository + + +def validate_relative_path(value: object) -> str: + if not isinstance(value, str) or not value or "\x00" in value: + raise GearboxError("context paths must be nonempty strings") + path = Path(value) + if path.is_absolute() or ".." in path.parts or value != path.as_posix(): + raise GearboxError(f"context path is not normalized: {value}") + if SENSITIVE_PATH.search(value): + raise GearboxError(f"sensitive context path is forbidden: {value}") + return value + + +def validate_selection(value: object) -> dict[str, Any]: + if not isinstance(value, dict): + raise GearboxError("context selections must be objects") + kind = value.get("kind") + common = {"path", "kind"} + if kind == "new": + require_keys(value, common, common, "new context selection") + return {"path": validate_relative_path(value["path"]), "kind": "new"} + if kind not in {"file", "lines", "symbols"}: + raise GearboxError("selection kind must be file, lines, symbols, or new") + required = common | {"source_sha256"} + allowed = set(required) + if kind == "lines": + required.add("ranges") + allowed.add("ranges") + if kind == "symbols": + required.add("symbols") + allowed.add("symbols") + require_keys(value, required, allowed, f"{kind} context selection") + source_hash = value["source_sha256"] + if not isinstance(source_hash, str) or not SHA256_RE.fullmatch(source_hash): + raise GearboxError("source_sha256 must be a lowercase SHA-256") + normalized: dict[str, Any] = { + "path": validate_relative_path(value["path"]), + "kind": kind, + "source_sha256": source_hash, + } + if kind == "lines": + ranges = value["ranges"] + if not isinstance(ranges, list) or not 1 <= len(ranges) <= 64: + raise GearboxError("line selections must contain 1 to 64 ranges") + output: list[dict[str, int]] = [] + previous_end = 0 + for item in ranges: + if not isinstance(item, dict): + raise GearboxError("line ranges must be objects") + require_keys(item, {"start_line", "end_line"}, {"start_line", "end_line"}, "line range") + start = item["start_line"] + end = item["end_line"] + if ( + not isinstance(start, int) + or isinstance(start, bool) + or not isinstance(end, int) + or isinstance(end, bool) + or start < 1 + or end < start + or start <= previous_end + ): + raise GearboxError("line ranges must be positive, ordered, and non-overlapping") + output.append({"start_line": start, "end_line": end}) + previous_end = end + normalized["ranges"] = output + if kind == "symbols": + symbols = value["symbols"] + if ( + not isinstance(symbols, list) + or not 1 <= len(symbols) <= 64 + or not all(isinstance(item, str) and SYMBOL_RE.fullmatch(item) for item in symbols) + or len(set(symbols)) != len(symbols) + ): + raise GearboxError("symbols must be 1 to 64 unique qualified names") + normalized["symbols"] = sorted(symbols) + return normalized + + +def load_policy(authority: object) -> tuple[dict[str, Any], str]: + if not isinstance(authority, dict): + raise GearboxError("authority must be an object") + require_keys(authority, {"policy_path", "policy_sha256"}, {"policy_path", "policy_sha256"}, "authority") + path_value = authority["policy_path"] + expected_hash = authority["policy_sha256"] + if not isinstance(path_value, str) or not Path(path_value).is_absolute(): + raise GearboxError("authority.policy_path must be absolute") + if not isinstance(expected_hash, str) or not SHA256_RE.fullmatch(expected_hash): + raise GearboxError("authority.policy_sha256 must be a lowercase SHA-256") + path = Path(path_value) + try: + if path.is_symlink() or not path.is_file(): + raise GearboxError("authority policy must be a regular non-symlink file") + flags = os.O_RDONLY | os.O_CLOEXEC + if hasattr(os, "O_NOFOLLOW"): + flags |= os.O_NOFOLLOW + descriptor = os.open(path, flags) + try: + before = os.fstat(descriptor) + if not stat.S_ISREG(before.st_mode): + raise GearboxError("authority policy must be a regular file") + chunks: list[bytes] = [] + while chunk := os.read(descriptor, 1024 * 1024): + chunks.append(chunk) + after = os.fstat(descriptor) + if (before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns) != ( + after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns, + ): + raise GearboxError("authority policy drifted while read") + policy_bytes = b"".join(chunks) + finally: + os.close(descriptor) + actual_hash = sha256_bytes(policy_bytes) + except OSError as exc: + raise GearboxError("authority policy is unavailable") from exc + if actual_hash != expected_hash: + raise GearboxError("authority policy hash drifted") + try: + policy = json.loads(policy_bytes.decode("utf-8")) + except (UnicodeDecodeError, json.JSONDecodeError) as exc: + raise GearboxError("authority policy is not valid UTF-8 JSON") from exc + if not isinstance(policy, dict): + raise GearboxError("authority policy is not an object") + require_keys(policy, {"schema", "authority_id", "gears"}, {"schema", "authority_id", "gears"}, "policy") + if policy["schema"] != POLICY_SCHEMA: + raise GearboxError("unsupported policy schema") + if not isinstance(policy["authority_id"], str) or not policy["authority_id"]: + raise GearboxError("policy authority_id is invalid") + gears = policy["gears"] + if not isinstance(gears, dict) or not gears: + raise GearboxError("policy gears must be a nonempty object") + for name, gear in gears.items(): + if not isinstance(name, str) or not name or not isinstance(gear, dict): + raise GearboxError("policy gear entries are malformed") + kind = gear.get("kind") + if kind == "deterministic": + required = { + "kind", "task_types", "argv_exact", "executable_path", + "executable_sha256", + } + require_keys(gear, required, required, f"policy gear {name}") + argv = gear["argv_exact"] + if not isinstance(argv, list) or not argv or not all(isinstance(item, str) and item for item in argv): + raise GearboxError(f"policy gear {name} argv_exact is invalid") + executable_value = gear["executable_path"] + if not isinstance(executable_value, str) or not executable_value: + raise GearboxError(f"policy gear {name} executable_path is invalid") + executable_path = Path(executable_value) + if not executable_path.is_absolute(): + executable_path = path.parent / executable_path + executable = executable_path.resolve() + executable_hash = gear["executable_sha256"] + if ( + executable_path.is_symlink() + or not executable.is_file() + or not os.access(executable, os.X_OK) + or not isinstance(executable_hash, str) + or not SHA256_RE.fullmatch(executable_hash) + ): + raise GearboxError(f"policy gear {name} executable identity is invalid") + if sha256_file(executable) != executable_hash: + raise GearboxError(f"policy gear {name} executable hash drifted") + gear["resolved_executable"] = str(executable) + elif kind == "helper": + required = { + "kind", "task_types", "model", "effort", "max_provider_sessions", + "transport_id", + } + require_keys(gear, required, required, f"policy gear {name}") + if ( + not isinstance(gear["model"], str) + or not isinstance(gear["effort"], str) + or not isinstance(gear["transport_id"], str) + or not gear["transport_id"] + ): + raise GearboxError(f"policy gear {name} model or effort is invalid") + if gear["max_provider_sessions"] not in {0, 1}: + raise GearboxError(f"policy gear {name} provider-session limit is invalid") + else: + raise GearboxError(f"policy gear {name} has unsupported kind") + if not isinstance(gear["task_types"], list) or not gear["task_types"] or not all( + isinstance(item, str) and item for item in gear["task_types"] + ): + raise GearboxError(f"policy gear {name} task_types are invalid") + return policy, actual_hash + + +def validate_output_schema(value: object) -> dict[str, Any]: + if not isinstance(value, dict) or value.get("type") != "object": + raise GearboxError("output_contract.schema must describe an object") + if not isinstance(value.get("required"), list) or not isinstance(value.get("properties"), dict): + raise GearboxError("output_contract.schema requires properties and required") + if len(canonical_json(value)) > 32 * 1024: + raise GearboxError("output schema exceeds 32 KiB") + return value + + +def validate_helper_output_schema(value: dict[str, Any]) -> None: + standard = {"verdict", "summary", "changed_paths", "tests", "unresolved_issues"} + required = value.get("required", []) + properties = value.get("properties", {}) + if not standard.issubset(required) or not standard.issubset(properties): + raise GearboxError("helper output schema must require the standard compact fields") + verdict = properties["verdict"] + if not isinstance(verdict, dict) or set(verdict.get("enum", [])) != { + "PASS", "BLOCKED", "FAIL", + }: + raise GearboxError("helper verdict must be exactly PASS, BLOCKED, or FAIL") + + +def validate_request(value: object) -> tuple[dict[str, Any], dict[str, Any], str]: + if not isinstance(value, dict): + raise GearboxError("request must be an object") + fields = { + "schema", "task", "task_type", "requested_gear", "allowed_context", + "output_contract", "authority", "budget", + } + require_keys(value, fields, fields, "request") + if value["schema"] != REQUEST_SCHEMA: + raise GearboxError("unsupported request schema") + policy, policy_hash = load_policy(value["authority"]) + requested_gear = value["requested_gear"] + if not isinstance(requested_gear, str) or requested_gear not in policy["gears"]: + raise GearboxError("requested gear is not authorized") + gear = policy["gears"][requested_gear] + task_type = value["task_type"] + if not isinstance(task_type, str) or task_type not in gear["task_types"]: + raise GearboxError("task_type is not authorized for the requested gear") + context = value["allowed_context"] + if not isinstance(context, dict): + raise GearboxError("allowed_context must be an object") + require_keys(context, {"repository", "selections", "writable_paths"}, {"repository", "selections", "writable_paths"}, "allowed_context") + repository = repository_identity(context["repository"]) + selections_raw = context["selections"] + writable_raw = context["writable_paths"] + if not isinstance(selections_raw, list) or len(selections_raw) > 100: + raise GearboxError("context selections are invalid") + if not isinstance(writable_raw, list) or len(writable_raw) > 100: + raise GearboxError("writable paths are invalid") + selections = [validate_selection(item) for item in selections_raw] + writable = [validate_relative_path(item) for item in writable_raw] + if len({item["path"] for item in selections}) != len(selections): + raise GearboxError("each context path must have one selection") + if len(set(writable)) != len(writable): + raise GearboxError("writable paths must be unique") + by_path = {item["path"]: item for item in selections} + if any(path not in by_path for path in writable): + raise GearboxError("writable paths require an explicit selection") + for item in selections: + source = repository / item["path"] + if item["kind"] == "new": + if item["path"] not in writable or source.exists(): + raise GearboxError("new context must be writable and absent") + elif not source.exists(): + raise GearboxError(f"selected context source is missing: {item['path']}") + for path in writable: + if by_path[path]["kind"] not in {"file", "new"}: + raise GearboxError("partial context selections cannot be writable") + output = value["output_contract"] + if not isinstance(output, dict): + raise GearboxError("output_contract must be an object") + require_keys(output, {"schema"}, {"schema"}, "output_contract") + output_schema = validate_output_schema(output["schema"]) + budget = value["budget"] + budget_fields = { + "timeout_seconds", "max_raw_bytes", "max_return_bytes", "max_context_bytes", + "max_commands", "max_provider_sessions", + } + if not isinstance(budget, dict): + raise GearboxError("budget must be an object") + require_keys(budget, budget_fields, budget_fields, "budget") + bounds = { + "timeout_seconds": (1, 7200), + "max_raw_bytes": (2048, 64 * 1024 * 1024), + "max_return_bytes": (2048, 64 * 1024), + "max_context_bytes": (0, 2 * 1024 * 1024), + "max_commands": (0, 50), + "max_provider_sessions": (0, 1), + } + for field, (minimum, maximum) in bounds.items(): + item = budget.get(field) + if not isinstance(item, int) or isinstance(item, bool) or not minimum <= item <= maximum: + raise GearboxError(f"budget.{field} is outside its bound") + if budget["max_provider_sessions"] != gear.get("max_provider_sessions", 0): + raise GearboxError("provider-session budget does not match the authorized gear") + if gear["kind"] == "deterministic": + task = value["task"] + if not isinstance(task, dict) or set(task) != {"argv"} or task["argv"] != gear["argv_exact"]: + raise GearboxError("deterministic task argv does not exactly match policy") + if selections or writable or budget["max_context_bytes"] != 0: + raise GearboxError("deterministic gear cannot receive helper context") + if budget["max_commands"] != 1: + raise GearboxError("deterministic gear requires a one-command budget") + else: + if not isinstance(value["task"], str) or not 1 <= len(value["task"]) <= 20_000: + raise GearboxError("helper task must be a bounded string") + if not selections or budget["max_context_bytes"] < 1 or budget["max_commands"] < 1: + raise GearboxError("helper gear requires bounded context and command budgets") + validate_helper_output_schema(output_schema) + normalized = { + "schema": REQUEST_SCHEMA, + "task": value["task"], + "task_type": task_type, + "requested_gear": requested_gear, + "gear": gear, + "allowed_context": { + "repository": str(repository), + "selections": sorted(selections, key=lambda item: item["path"]), + "writable_paths": sorted(writable), + }, + "output_contract": {"schema": output_schema}, + "authority": { + "authority_id": policy["authority_id"], + "policy_sha256": policy_hash, + }, + "budget": dict(budget), + } + return normalized, policy, policy_hash + + +def read_source(repository: Path, relative: str) -> bytes: + source = repository / relative + try: + resolved = source.resolve(strict=True) + except OSError as exc: + raise GearboxError(f"selected context source is unavailable: {relative}") from exc + if resolved != source.absolute() or not resolved.is_relative_to(repository): + raise GearboxError(f"context source crosses a symlink or repository boundary: {relative}") + flags = os.O_RDONLY | os.O_CLOEXEC + if hasattr(os, "O_NOFOLLOW"): + flags |= os.O_NOFOLLOW + descriptor = os.open(source, flags) + try: + before = os.fstat(descriptor) + if not stat.S_ISREG(before.st_mode) or before.st_nlink != 1: + raise GearboxError(f"context file identity is unsafe: {relative}") + chunks: list[bytes] = [] + while chunk := os.read(descriptor, 1024 * 1024): + chunks.append(chunk) + after = os.fstat(descriptor) + identity = (before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns) + if identity != (after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns): + raise GearboxError(f"context source drifted while read: {relative}") + value = b"".join(chunks) + if len(value) != before.st_size: + raise GearboxError(f"context source size drifted: {relative}") + return value + finally: + os.close(descriptor) + + +def python_symbol_ranges(text: str, requested: list[str]) -> list[dict[str, Any]]: + try: + tree = ast.parse(text) + except SyntaxError as exc: + raise GearboxError("Python symbol selection requires parseable source") from exc + found: dict[str, list[tuple[int, int]]] = {} + + class Visitor(ast.NodeVisitor): + def __init__(self) -> None: + self.stack: list[str] = [] + + def record(self, node: ast.AST, name: str) -> None: + qualified = ".".join([*self.stack, name]) + decorators = getattr(node, "decorator_list", []) + start = min([node.lineno] + [item.lineno for item in decorators]) + end = getattr(node, "end_lineno", None) + if not isinstance(end, int): + raise GearboxError(f"symbol has no deterministic end: {qualified}") + found.setdefault(qualified, []).append((start, end)) + + def _visit_named(self, node: ast.AST, name: str) -> None: + self.record(node, name) + self.stack.append(name) + self.generic_visit(node) + self.stack.pop() + + def visit_ClassDef(self, node: ast.ClassDef) -> None: + self._visit_named(node, node.name) + + def visit_FunctionDef(self, node: ast.FunctionDef) -> None: + self._visit_named(node, node.name) + + def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None: + self._visit_named(node, node.name) + + Visitor().visit(tree) + selected: list[dict[str, Any]] = [] + for name in requested: + matches = found.get(name, []) + if len(matches) != 1: + raise GearboxError(f"symbol is ambiguous or missing: {name} ({len(matches)} matches)") + start, end = matches[0] + selected.append({"symbol": name, "start_line": start, "end_line": end}) + selected.sort(key=lambda item: (item["start_line"], item["end_line"], item["symbol"])) + previous_end = 0 + for item in selected: + if item["start_line"] <= previous_end: + raise GearboxError("selected symbols overlap") + previous_end = item["end_line"] + return selected + + +def select_bytes(source: bytes, selection: dict[str, Any]) -> tuple[bytes, int, list[dict[str, Any]]]: + kind = selection["kind"] + if kind == "file": + return source, len(source), [] + try: + text = source.decode("utf-8") + except UnicodeDecodeError as exc: + raise GearboxError("partial context selection requires UTF-8") from exc + lines = text.splitlines(keepends=True) + if kind == "lines": + resolved = [dict(item) for item in selection["ranges"]] + if any(item["end_line"] > len(lines) for item in resolved): + raise GearboxError("line selection exceeds source") + elif kind == "symbols": + if Path(selection["path"]).suffix != ".py": + raise GearboxError("symbol selection currently supports Python only") + resolved = python_symbol_ranges(text, selection["symbols"]) + else: + raise GearboxError(f"unsupported selection kind: {kind}") + chunks: list[bytes] = [] + retained = 0 + for item in resolved: + start = item["start_line"] + end = item["end_line"] + body = "".join(lines[start - 1 : end]).encode("utf-8") + label = f" symbol={item['symbol']}" if "symbol" in item else "" + chunks.append(f"<<>>\n".encode()) + chunks.append(body) + chunks.append(b"\n<<>>\n") + retained += len(body) + return b"".join(chunks), retained, resolved + + +@dataclass +class StagedContext: + workspace: Path + entries: list[dict[str, Any]] + packet_bytes: int + source_bytes: int + manifest_sha256: str + + +def stage_context(run_dir: Path, request: dict[str, Any]) -> StagedContext: + repository = Path(request["allowed_context"]["repository"]) + workspace = run_dir / "workspace" + workspace.mkdir(mode=0o700) + entries: list[dict[str, Any]] = [] + packet_total = 0 + source_total = 0 + try: + for selection in request["allowed_context"]["selections"]: + relative = selection["path"] + target = workspace / relative + target.parent.mkdir(parents=True, exist_ok=True, mode=0o700) + if selection["kind"] == "new": + packet = b"" + source_hash = None + source_size = 0 + resolved: list[dict[str, Any]] = [] + else: + source = read_source(repository, relative) + source_hash = sha256_bytes(source) + if source_hash != selection["source_sha256"]: + raise GearboxError(f"context source hash drifted: {relative}") + packet, _, resolved = select_bytes(source, selection) + source_size = len(source) + atomic_write(target, packet) + entry = { + "path": relative, + "kind": selection["kind"], + "source_sha256": source_hash, + "source_bytes": source_size, + "packet_sha256": sha256_bytes(packet), + "packet_bytes": len(packet), + "resolved_ranges": resolved, + "writable": relative in request["allowed_context"]["writable_paths"], + } + entries.append(entry) + packet_total += len(packet) + source_total += source_size + if packet_total > request["budget"]["max_context_bytes"]: + raise GearboxError("context exceeds the hard byte ceiling") + manifest = { + "schema": "opsle.gearbox.context-manifest.v1", + "entries": entries, + "source_bytes": source_total, + "packet_bytes": packet_total, + "complete": True, + } + encoded = canonical_json(manifest) + atomic_write(workspace / "GEARBOX_CONTEXT.json", encoded) + return StagedContext(workspace, entries, packet_total, source_total, sha256_bytes(encoded)) + except Exception: + shutil.rmtree(workspace, ignore_errors=True) + raise + + +def revalidate_sources(request: dict[str, Any]) -> None: + repository = Path(request["allowed_context"]["repository"]) + for selection in request["allowed_context"]["selections"]: + if selection["kind"] == "new": + continue + actual = sha256_bytes(read_source(repository, selection["path"])) + if actual != selection["source_sha256"]: + raise GearboxError(f"context source drifted after staging: {selection['path']}") + + +def workspace_changes(staged: StagedContext) -> list[str]: + manifest_path = staged.workspace / "GEARBOX_CONTEXT.json" + if ( + not manifest_path.is_file() + or manifest_path.is_symlink() + or sha256_file(manifest_path) != staged.manifest_sha256 + ): + raise GearboxError("helper changed the context manifest") + expected = {entry["path"]: entry for entry in staged.entries} + actual = { + path.relative_to(staged.workspace).as_posix() + for path in staged.workspace.rglob("*") + if path.is_file() and path.name != "GEARBOX_CONTEXT.json" + } + unexpected = sorted(actual - set(expected)) + if unexpected: + raise GearboxError(f"helper created unauthorized paths: {', '.join(unexpected)}") + changed: list[str] = [] + for relative, entry in expected.items(): + path = staged.workspace / relative + if not path.is_file() or path.is_symlink(): + raise GearboxError(f"helper removed or replaced context path: {relative}") + current_hash = sha256_file(path) + if current_hash != entry["packet_sha256"]: + if not entry["writable"]: + raise GearboxError(f"helper changed read-only context: {relative}") + changed.append(relative) + return changed + + +def validate_json_value(value: Any, schema: dict[str, Any], path: str = "$") -> None: + expected = schema.get("type") + types = { + "object": dict, + "array": list, + "string": str, + "integer": int, + "number": (int, float), + "boolean": bool, + "null": type(None), + } + if expected in types and (not isinstance(value, types[expected]) or expected in {"integer", "number"} and isinstance(value, bool)): + raise GearboxError(f"helper final {path} has wrong type") + if "enum" in schema and value not in schema["enum"]: + raise GearboxError(f"helper final {path} is outside its enum") + if isinstance(value, dict): + required = schema.get("required", []) + missing = [key for key in required if key not in value] + if missing: + raise GearboxError(f"helper final {path} is missing: {', '.join(missing)}") + properties = schema.get("properties", {}) + if schema.get("additionalProperties") is False: + unknown = set(value) - set(properties) + if unknown: + raise GearboxError(f"helper final {path} has unknown fields") + for key, item in value.items(): + if key in properties: + validate_json_value(item, properties[key], f"{path}.{key}") + if isinstance(value, list): + if isinstance(schema.get("maxItems"), int) and len(value) > schema["maxItems"]: + raise GearboxError(f"helper final {path} exceeds maxItems") + item_schema = schema.get("items") + if isinstance(item_schema, dict): + for index, item in enumerate(value): + validate_json_value(item, item_schema, f"{path}[{index}]") + if isinstance(value, str) and isinstance(schema.get("maxLength"), int) and len(value) > schema["maxLength"]: + raise GearboxError(f"helper final {path} exceeds maxLength") + + +def artifact(path: Path, run_dir: Path, kind: str) -> dict[str, Any]: + return { + "kind": kind, + "locator": path.relative_to(run_dir.parent.parent).as_posix(), + "bytes": path.stat().st_size, + "sha256": sha256_file(path), + } + + +def terminate_group(process: subprocess.Popen[Any]) -> None: + try: + os.killpg(process.pid, signal.SIGTERM) + except ProcessLookupError: + return + try: + process.wait(timeout=2) + except subprocess.TimeoutExpired: + try: + os.killpg(process.pid, signal.SIGKILL) + except ProcessLookupError: + pass + process.wait(timeout=5) + + +class GearboxRunner: + def __init__( + self, + state_root: Path, + *, + helper_transport: HelperTransport | None = None, + mechanism_revision: str | None = None, + ) -> None: + self.state_root = state_root.resolve() + self.helper_transport = helper_transport + self.mechanism_revision = mechanism_revision + self.state_root.mkdir(parents=True, exist_ok=True, mode=0o700) + os.chmod(self.state_root, 0o700) + (self.state_root / "runs").mkdir(exist_ok=True, mode=0o700) + + def run(self, value: object) -> dict[str, Any]: + request, _policy, policy_hash = validate_request(value) + fingerprint = sha256_bytes(canonical_json(request)) + run_id = "g" + fingerprint[:24] + run_dir = self.state_root / "runs" / run_id + result_path = run_dir / "result.json" + if result_path.exists(): + return read_object(result_path) + if run_dir.exists(): + raise GearboxError("incomplete run exists; refusing duplicate execution") + try: + run_dir.mkdir(mode=0o700) + except FileExistsError as exc: + raise GearboxError("concurrent run exists; refusing duplicate execution") from exc + (run_dir / "raw").mkdir(mode=0o700) + atomic_write(run_dir / "request.json", canonical_json(request)) + atomic_write(run_dir / "started.json", canonical_json({"run_id": run_id, "attempt": 1})) + try: + if request["gear"]["kind"] == "deterministic": + partial = self._run_deterministic(run_dir, request) + else: + partial = self._run_helper(run_dir, request) + except GearboxError as exc: + raw_artifacts = [ + artifact(path, run_dir, f"RAW_{path.name.upper().replace('.', '_')}") + for path in sorted((run_dir / "raw").glob("*")) + if path.is_file() + ] + partial = { + "status": "failed", + "summary": str(exc), + "exit_code": None, + "changed_paths": [], + "provider_sessions": 0, + "commands": 0, + "terminated": True, + "wait_mode": "not_started_or_failed_closed", + "artifacts": raw_artifacts, + "escalation": {"required": True, "reason": "GEARBOX_FAIL_CLOSED"}, + } + result = self._finalize(run_dir, run_id, request, partial) + encoded = canonical_json(result) + if len(encoded) > request["budget"]["max_return_bytes"]: + raise GearboxError("compact result exceeds max_return_bytes") + receipt = self._value_receipt(result, len(encoded), policy_hash) + atomic_write(run_dir / "value-receipt.json", canonical_json(receipt)) + atomic_write(result_path, encoded) + return result + + def _run_deterministic(self, run_dir: Path, request: dict[str, Any]) -> dict[str, Any]: + stdout_path = run_dir / "raw/stdout.raw" + stderr_path = run_dir / "raw/stderr.raw" + repository = Path(request["allowed_context"]["repository"]) + executable = Path(request["gear"]["resolved_executable"]) + if sha256_file(executable) != request["gear"]["executable_sha256"]: + raise GearboxError("deterministic executable drifted before execution") + argv = [str(executable), *request["task"]["argv"][1:]] + with stdout_path.open("xb") as stdout, stderr_path.open("xb") as stderr: + process = subprocess.Popen( + argv, + cwd=repository, + stdin=subprocess.DEVNULL, + stdout=stdout, + stderr=stderr, + close_fds=True, + start_new_session=True, + ) + timed_out = False + try: + exit_code = process.wait(timeout=request["budget"]["timeout_seconds"]) + except subprocess.TimeoutExpired: + timed_out = True + terminate_group(process) + exit_code = process.returncode + raw_bytes = stdout_path.stat().st_size + stderr_path.stat().st_size + raw_limited = raw_bytes > request["budget"]["max_raw_bytes"] + status = "completed" if exit_code == 0 and not timed_out and not raw_limited else "blocked" + if timed_out or raw_limited: + status = "failed" + reason = None + if timed_out: + reason = "TIMEOUT" + elif raw_limited: + reason = "RAW_EVIDENCE_LIMIT" + elif exit_code != 0: + reason = "NONZERO_EXIT" + return { + "status": status, + "summary": "deterministic command completed" if status == "completed" else "deterministic command requires escalation", + "exit_code": exit_code, + "changed_paths": [], + "provider_sessions": 0, + "commands": 1, + "terminated": process.poll() is not None, + "wait_mode": "blocking_subprocess_wait", + "artifacts": [ + artifact(stdout_path, run_dir, "RAW_STDOUT"), + artifact(stderr_path, run_dir, "RAW_STDERR"), + ], + "escalation": {"required": reason is not None, "reason": reason}, + } + + def _run_helper(self, run_dir: Path, request: dict[str, Any]) -> dict[str, Any]: + if self.helper_transport is None: + raise GearboxError("helper transport is unavailable; no fallback is permitted") + staged = stage_context(run_dir, request) + revalidate_sources(request) + transport_request = { + "schema": "opsle.gearbox.helper-request.v1", + "task": request["task"], + "task_type": request["task_type"], + "gear": request["requested_gear"], + "model": request["gear"]["model"], + "effort": request["gear"]["effort"], + "transport_id": request["gear"]["transport_id"], + "context_manifest_sha256": staged.manifest_sha256, + "output_contract": request["output_contract"], + "budget": request["budget"], + } + try: + transport = self.helper_transport.execute( + request=transport_request, + workspace=staged.workspace, + timeout_seconds=request["budget"]["timeout_seconds"], + ) + except Exception as exc: # noqa: BLE001 - unknown transport failure must fail closed + return { + "status": "uncertain", + "summary": f"helper transport failed with unknown execution state: {type(exc).__name__}", + "exit_code": None, + "changed_paths": [], + "provider_sessions": None, + "commands": None, + "terminated": False, + "wait_mode": "blocking_external_transport", + "artifacts": [], + "escalation": {"required": True, "reason": "TRANSPORT_STATE_UNKNOWN"}, + "provider_sessions_verified": False, + } + def failed_after_transport(message: str) -> dict[str, Any]: + observed_sessions = ( + transport.get("provider_sessions") + if isinstance(transport, dict) + and isinstance(transport.get("provider_sessions"), int) + and not isinstance(transport.get("provider_sessions"), bool) + and transport.get("provider_sessions") >= 0 + else None + ) + observed_commands = ( + transport.get("commands") + if isinstance(transport, dict) + and isinstance(transport.get("commands"), int) + and not isinstance(transport.get("commands"), bool) + and transport.get("commands") >= 0 + else None + ) + raw_artifacts = [ + artifact(path, run_dir, f"RAW_{path.name.upper().replace('.', '_')}") + for path in sorted((run_dir / "raw").glob("*")) + if path.is_file() + ] + return { + "status": "failed", + "summary": message, + "exit_code": None, + "changed_paths": [], + "provider_sessions": observed_sessions, + "commands": observed_commands, + "terminated": ( + transport.get("terminated") is True + if isinstance(transport, dict) + else False + ), + "wait_mode": "blocking_external_transport", + "artifacts": raw_artifacts, + "escalation": {"required": True, "reason": "HELPER_FAIL_CLOSED"}, + "provider_sessions_verified": observed_sessions is not None, + } + required = { + "final", "stdout", "stderr", "provider_sessions", "commands", + "terminated", "model", "effort", "transport_id", + } + if not isinstance(transport, dict) or set(transport) != required: + return failed_after_transport("helper transport result is malformed") + if not isinstance(transport["stdout"], bytes) or not isinstance(transport["stderr"], bytes): + return failed_after_transport("helper transport raw evidence must be bytes") + stdout_path = run_dir / "raw/helper-stdout.raw" + stderr_path = run_dir / "raw/helper-stderr.raw" + atomic_write(stdout_path, transport["stdout"]) + atomic_write(stderr_path, transport["stderr"]) + raw_bytes = len(transport["stdout"]) + len(transport["stderr"]) + if raw_bytes > request["budget"]["max_raw_bytes"]: + return failed_after_transport("helper exceeded raw evidence budget") + expected_gear = request["gear"] + if ( + transport["model"] != expected_gear["model"] + or transport["effort"] != expected_gear["effort"] + or transport["transport_id"] != expected_gear["transport_id"] + ): + return failed_after_transport("helper transport, model, or effort drifted") + if not isinstance(transport["provider_sessions"], int) or not 0 <= transport["provider_sessions"] <= request["budget"]["max_provider_sessions"]: + return failed_after_transport("helper exceeded provider-session budget") + if not isinstance(transport["commands"], int) or not 0 <= transport["commands"] <= request["budget"]["max_commands"]: + return failed_after_transport("helper exceeded command budget") + if transport["terminated"] is not True: + return failed_after_transport("helper termination is unverified") + final = transport["final"] + final_path = run_dir / "raw/helper-final.json" + try: + atomic_write(final_path, canonical_json(final)) + validate_json_value(final, request["output_contract"]["schema"]) + changed = workspace_changes(staged) + except (GearboxError, TypeError, ValueError) as exc: + return failed_after_transport(str(exc)) + if sorted(final.get("changed_paths", [])) != changed: + return failed_after_transport( + "helper changed-path claim does not match the staged workspace" + ) + verdict = final.get("verdict") if isinstance(final, dict) else None + status = {"PASS": "completed", "BLOCKED": "blocked", "FAIL": "failed"}.get(verdict, "uncertain") + return { + "status": status, + "summary": final.get("summary", "helper returned") if isinstance(final, dict) else "helper returned", + "exit_code": None, + "changed_paths": changed, + "provider_sessions": transport["provider_sessions"], + "commands": transport["commands"], + "terminated": True, + "wait_mode": "blocking_external_transport", + "artifacts": [ + artifact(stdout_path, run_dir, "RAW_HELPER_STDOUT"), + artifact(stderr_path, run_dir, "RAW_HELPER_STDERR"), + artifact(final_path, run_dir, "RAW_HELPER_FINAL"), + ], + "escalation": { + "required": status != "completed", + "reason": None if status == "completed" else "HELPER_TERMINAL_RESULT", + }, + "context": { + "source_bytes": staged.source_bytes, + "packet_bytes": staged.packet_bytes, + "manifest_sha256": staged.manifest_sha256, + }, + "provider_sessions_verified": True, + } + + def _finalize( + self, + run_dir: Path, + run_id: str, + request: dict[str, Any], + partial: dict[str, Any], + ) -> dict[str, Any]: + result = { + "schema": RESULT_SCHEMA, + "run_id": run_id, + "request_sha256": sha256_file(run_dir / "request.json"), + "authority_id": request["authority"]["authority_id"], + "policy_sha256": request["authority"]["policy_sha256"], + "gear": request["requested_gear"], + "gear_kind": request["gear"]["kind"], + "status": partial["status"], + "summary": partial["summary"][:1200], + "exit_code": partial["exit_code"], + "changed_paths": partial["changed_paths"], + "artifacts": partial["artifacts"], + "metrics": { + "execution_attempts": 1, + "fallback_attempts": 0, + "provider_sessions": partial["provider_sessions"], + "commands": partial["commands"], + "helper_terminated": partial["terminated"], + "primary_wait_mode": partial["wait_mode"], + "raw_evidence_bytes": sum(item["bytes"] for item in partial["artifacts"]), + "provider_sessions_verified": partial.get("provider_sessions_verified", True), + }, + "escalation": partial["escalation"], + "context": partial.get("context"), + "value_receipt": f"runs/{run_id}/value-receipt.json", + } + result["result_sha256"] = sha256_bytes(canonical_json(result)) + return result + + def _value_receipt( + self, result: dict[str, Any], compact_bytes: int, policy_hash: str + ) -> dict[str, Any]: + def measurement( + identity: str, + result_value: Any, + unit: str, + quality: str, + *, + operator: bool, + safe_sum: bool, + direction: str = "NEUTRAL", + ) -> dict[str, Any]: + return { + "id": identity, + "baseline": None, + "result": result_value, + "delta": None, + "unit": unit, + "direction": direction, + "class": quality, + "source_verification": "VERIFIED" if quality == "EXACT" else "OBSERVED", + "evidence_refs": ["compact_result"], + "operator_display": operator, + "aggregation": {"safe": safe_sum, "method": "SUM" if safe_sum else None}, + "derivation": None, + "limitations": [], + } + + metrics = result["metrics"] + provider_sessions = metrics["provider_sessions"] + provider_quality = "EXACT" if metrics["provider_sessions_verified"] else "OBSERVED" + return { + "schema": VALUE_SCHEMA, + "mechanism": { + "id": "opsle.gearbox", + "name": "Agent Gearbox", + "version": VERSION, + "revision": self.mechanism_revision, + }, + "operation": { + "id": result["run_id"], + "name": "bounded-gear-execution", + "configuration_id": f"sha256:{policy_hash}", + "policy_id": result["authority_id"], + }, + "run": {"id": result["run_id"]}, + "measurements": [ + measurement("execution_attempts", 1, "count", "EXACT", operator=False, safe_sum=True), + measurement("fallback_attempts", 0, "count", "EXACT", operator=False, safe_sum=True), + measurement( + "provider_sessions", + provider_sessions, + "count", + provider_quality, + operator=True, + safe_sum=provider_quality == "EXACT" and isinstance(provider_sessions, int), + ), + measurement("raw_evidence_bytes", metrics["raw_evidence_bytes"], "byte", "EXACT", operator=False, safe_sum=True), + measurement("model_visible_result_bytes", compact_bytes, "byte", "EXACT", operator=True, safe_sum=True), + measurement("terminal_status", result["status"], "state", "OBSERVED", operator=True, safe_sum=False, direction="PROTECTION_SIGNAL"), + measurement("primary_wait_mode", metrics["primary_wait_mode"], "state", "OBSERVED", operator=False, safe_sum=False), + ], + "evidence": [ + { + "id": "compact_result", + "kind": "CONTENT_HASH", + "locator": f"sha256:{result['result_sha256']}", + "trust": "VERIFIED", + } + ], + "limitations": [ + "No token, cost, latency, correctness, or causal savings claim is made.", + "A zero provider-session count is observed execution telemetry, not proof that a provider session would otherwise have occurred.", + "Helper isolation and provider accounting depend on the separately supplied transport.", + "Context Firewall reduction is an external integration and was not performed by this core run.", + ], + } + + +def operator_indicator(result: dict[str, Any], receipt: dict[str, Any]) -> str: + measurements = {item["id"]: item["result"] for item in receipt["measurements"]} + return ( + f"[Gearbox] {result['gear_kind']} {result['status']} | " + f"provider_sessions={measurements['provider_sessions']} | " + f"raw={measurements['raw_evidence_bytes']} B | " + f"return={measurements['model_visible_result_bytes']} B" + ) diff --git a/tests/test_core.py b/tests/test_core.py new file mode 100644 index 0000000..ea92b31 --- /dev/null +++ b/tests/test_core.py @@ -0,0 +1,369 @@ +from __future__ import annotations + +import json +import os +import shutil +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +from opsle_gearbox.core import ( + POLICY_SCHEMA, + REQUEST_SCHEMA, + GearboxError, + GearboxRunner, + canonical_json, + sha256_file, + validate_request, +) + +OUTPUT_SCHEMA = { + "type": "object", + "properties": { + "verdict": {"type": "string", "enum": ["PASS", "BLOCKED", "FAIL"]}, + "summary": {"type": "string", "maxLength": 1200}, + "changed_paths": {"type": "array", "items": {"type": "string"}, "maxItems": 40}, + "tests": {"type": "array", "items": {"type": "string"}, "maxItems": 30}, + "unresolved_issues": {"type": "array", "items": {"type": "string"}, "maxItems": 30}, + }, + "required": ["verdict", "summary", "changed_paths", "tests", "unresolved_issues"], + "additionalProperties": False, +} + + +class FakeTransport: + def __init__(self, **overrides): + self.calls = 0 + self.last_request = None + self.overrides = overrides + + def execute(self, *, request, workspace, timeout_seconds): + self.calls += 1 + self.last_request = request + if self.overrides.get("edit"): + (workspace / "editable.txt").write_text("changed\n", encoding="utf-8") + value = { + "final": { + "verdict": "PASS", + "summary": "bounded helper completed", + "changed_paths": ["editable.txt"] if self.overrides.get("edit") else [], + "tests": [], + "unresolved_issues": [], + }, + "stdout": b"private helper transcript\n", + "stderr": b"", + "provider_sessions": 0, + "commands": 1, + "terminated": True, + "model": "fixture-model", + "effort": "low", + "transport_id": "fixture-transport/v1", + } + value.update({key: item for key, item in self.overrides.items() if key != "edit"}) + return value + + +class GearboxTests(unittest.TestCase): + def setUp(self): + self.temporary = tempfile.TemporaryDirectory() + self.base = Path(self.temporary.name) + self.repo = self.base / "repo" + self.repo.mkdir() + subprocess.run(["git", "init", "-b", "main"], cwd=self.repo, check=True, capture_output=True) + subprocess.run(["git", "config", "user.email", "fixture@example.invalid"], cwd=self.repo, check=True) + subprocess.run(["git", "config", "user.name", "Fixture"], cwd=self.repo, check=True) + (self.repo / "README.md").write_text("fixture\n", encoding="utf-8") + subprocess.run(["git", "add", "README.md"], cwd=self.repo, check=True) + subprocess.run(["git", "commit", "-m", "fixture"], cwd=self.repo, check=True, capture_output=True) + self.policy = self.base / "policy.json" + self.policy.write_bytes(canonical_json({ + "schema": POLICY_SCHEMA, + "authority_id": "fixture-policy/v1", + "gears": { + "git-status": { + "kind": "deterministic", + "task_types": ["git"], + "argv_exact": ["git", "status", "--short", "--branch"], + "executable_path": str(Path(shutil.which("git"))), + "executable_sha256": sha256_file(Path(shutil.which("git"))), + }, + "always-fail": { + "kind": "deterministic", + "task_types": ["fixture"], + "argv_exact": ["false"], + "executable_path": str(Path(shutil.which("false"))), + "executable_sha256": sha256_file(Path(shutil.which("false"))), + }, + "fixture-helper": { + "kind": "helper", + "task_types": ["interpretation", "implementation"], + "model": "fixture-model", + "effort": "low", + "max_provider_sessions": 0, + "transport_id": "fixture-transport/v1", + }, + }, + })) + + def tearDown(self): + self.temporary.cleanup() + + def authority(self): + return {"policy_path": str(self.policy), "policy_sha256": sha256_file(self.policy)} + + def deterministic(self): + return { + "schema": REQUEST_SCHEMA, + "task": {"argv": ["git", "status", "--short", "--branch"]}, + "task_type": "git", + "requested_gear": "git-status", + "allowed_context": {"repository": str(self.repo), "selections": [], "writable_paths": []}, + "output_contract": {"schema": OUTPUT_SCHEMA}, + "authority": self.authority(), + "budget": { + "timeout_seconds": 10, + "max_raw_bytes": 65536, + "max_return_bytes": 8192, + "max_context_bytes": 0, + "max_commands": 1, + "max_provider_sessions": 0, + }, + } + + def helper(self, *, writable=False): + source = self.repo / ("editable.txt" if writable else "sample.py") + source.write_text("original\n" if writable else "def alpha():\n return 1\n\ndef omega():\n return 2\n", encoding="utf-8") + selection = { + "path": source.name, + "kind": "file" if writable else "symbols", + "source_sha256": sha256_file(source), + } + if not writable: + selection["symbols"] = ["alpha"] + return { + "schema": REQUEST_SCHEMA, + "task": "Inspect the admitted context and return one compact result.", + "task_type": "implementation" if writable else "interpretation", + "requested_gear": "fixture-helper", + "allowed_context": { + "repository": str(self.repo), + "selections": [selection], + "writable_paths": [source.name] if writable else [], + }, + "output_contract": {"schema": OUTPUT_SCHEMA}, + "authority": self.authority(), + "budget": { + "timeout_seconds": 10, + "max_raw_bytes": 65536, + "max_return_bytes": 8192, + "max_context_bytes": 65536, + "max_commands": 2, + "max_provider_sessions": 0, + }, + } + + def test_deterministic_execution_is_compact_provider_free_and_idempotent(self): + runner = GearboxRunner(self.base / "state", mechanism_revision="fixture-revision") + first = runner.run(self.deterministic()) + second = runner.run(self.deterministic()) + self.assertEqual(first, second) + self.assertEqual(first["status"], "completed") + self.assertEqual(first["metrics"]["provider_sessions"], 0) + self.assertEqual(first["metrics"]["execution_attempts"], 1) + self.assertEqual(first["metrics"]["fallback_attempts"], 0) + self.assertEqual(first["metrics"]["primary_wait_mode"], "blocking_subprocess_wait") + self.assertNotIn("## main", json.dumps(first)) + run = self.base / "state" / "runs" / first["run_id"] + self.assertTrue((run / "raw/stdout.raw").is_file()) + receipt = json.loads((run / "value-receipt.json").read_text()) + self.assertEqual(receipt["schema"], "opsle.value-receipt.v1") + + def test_policy_hash_drift_fails_before_execution(self): + request = self.deterministic() + self.policy.write_text("{}\n", encoding="utf-8") + with self.assertRaisesRegex(GearboxError, "hash drifted"): + validate_request(request) + + def test_nonzero_deterministic_result_escalates_without_fallback(self): + request = self.deterministic() + request["task"] = {"argv": ["false"]} + request["task_type"] = "fixture" + request["requested_gear"] = "always-fail" + result = GearboxRunner(self.base / "state").run(request) + self.assertEqual(result["status"], "blocked") + self.assertEqual(result["exit_code"], 1) + self.assertEqual(result["metrics"]["fallback_attempts"], 0) + self.assertEqual(result["escalation"]["reason"], "NONZERO_EXIT") + + def test_deterministic_argv_must_match_exactly(self): + request = self.deterministic() + request["task"]["argv"].append("--ignored") + with self.assertRaisesRegex(GearboxError, "exactly match"): + validate_request(request) + + def test_deterministic_command_budget_is_exactly_one(self): + request = self.deterministic() + request["budget"]["max_commands"] = 0 + with self.assertRaisesRegex(GearboxError, "one-command budget"): + validate_request(request) + + def test_unknown_request_field_fails_closed(self): + request = self.deterministic() + request["fallback"] = True + with self.assertRaisesRegex(GearboxError, "unknown fields"): + validate_request(request) + + def test_helper_requires_an_explicit_transport_and_never_falls_back(self): + result = GearboxRunner(self.base / "state").run(self.helper()) + self.assertEqual(result["status"], "failed") + self.assertEqual(result["metrics"]["execution_attempts"], 1) + self.assertEqual(result["metrics"]["fallback_attempts"], 0) + self.assertIn("transport is unavailable", result["summary"]) + + def test_symbol_context_is_content_addressed_and_private(self): + transport = FakeTransport() + result = GearboxRunner(self.base / "state", helper_transport=transport).run(self.helper()) + self.assertEqual(result["status"], "completed") + self.assertEqual(transport.calls, 1) + self.assertNotIn("allowed_context", transport.last_request) + self.assertNotIn(str(self.repo), json.dumps(transport.last_request)) + workspace = self.base / "state" / "runs" / result["run_id"] / "workspace" + packet = (workspace / "sample.py").read_text() + self.assertIn("symbol=alpha", packet) + self.assertNotIn("omega", packet) + self.assertEqual(result["context"]["manifest_sha256"], sha256_file(workspace / "GEARBOX_CONTEXT.json")) + + def test_writable_helper_changes_staged_copy_only(self): + transport = FakeTransport(edit=True) + request = self.helper(writable=True) + result = GearboxRunner(self.base / "state", helper_transport=transport).run(request) + self.assertEqual(result["status"], "completed") + self.assertEqual(result["changed_paths"], ["editable.txt"]) + self.assertEqual((self.repo / "editable.txt").read_text(), "original\n") + + def test_helper_model_effort_and_command_budgets_fail_closed(self): + drift = GearboxRunner(self.base / "model-state", helper_transport=FakeTransport(model="wrong")) + self.assertEqual(drift.run(self.helper())["status"], "failed") + over = GearboxRunner(self.base / "command-state", helper_transport=FakeTransport(commands=3)) + result = over.run(self.helper()) + self.assertEqual(result["status"], "failed") + self.assertEqual(result["metrics"]["fallback_attempts"], 0) + + def test_helper_transport_identity_and_changed_path_claim_are_bound(self): + identity = GearboxRunner( + self.base / "identity-state", + helper_transport=FakeTransport(transport_id="wrong-transport"), + ).run(self.helper()) + self.assertEqual(identity["status"], "failed") + self.assertIn("transport, model, or effort drifted", identity["summary"]) + + final = { + "verdict": "PASS", + "summary": "incorrect mutation claim", + "changed_paths": [], + "tests": [], + "unresolved_issues": [], + } + claim = GearboxRunner( + self.base / "claim-state", + helper_transport=FakeTransport(edit=True, final=final), + ).run(self.helper(writable=True)) + self.assertEqual(claim["status"], "failed") + self.assertIn("changed-path claim", claim["summary"]) + + def test_transport_exception_preserves_unknown_provider_state(self): + class BrokenTransport: + def execute(self, **_kwargs): + raise RuntimeError("fixture transport loss") + + result = GearboxRunner( + self.base / "state", helper_transport=BrokenTransport() + ).run(self.helper()) + self.assertEqual(result["status"], "uncertain") + self.assertIsNone(result["metrics"]["provider_sessions"]) + receipt = json.loads( + (self.base / "state" / "runs" / result["run_id"] / "value-receipt.json").read_text() + ) + measurement = next( + item for item in receipt["measurements"] if item["id"] == "provider_sessions" + ) + self.assertEqual(measurement["class"], "OBSERVED") + self.assertFalse(measurement["aggregation"]["safe"]) + + def test_helper_must_terminate(self): + result = GearboxRunner( + self.base / "state", helper_transport=FakeTransport(terminated=False) + ).run(self.helper()) + self.assertEqual(result["status"], "failed") + self.assertIn("termination is unverified", result["summary"]) + + def test_sensitive_and_symlink_context_is_rejected(self): + request = self.helper() + request["allowed_context"]["selections"][0]["path"] = ".env" + with self.assertRaisesRegex(GearboxError, "sensitive"): + validate_request(request) + target = self.repo / "target.py" + target.write_text("def exact():\n return 1\n", encoding="utf-8") + os.symlink("target.py", self.repo / "linked.py") + request = self.helper() + request["allowed_context"]["selections"] = [{ + "path": "linked.py", "kind": "file", "source_sha256": sha256_file(target), + }] + result = GearboxRunner(self.base / "state", helper_transport=FakeTransport()).run(request) + self.assertEqual(result["status"], "failed") + self.assertIn("symlink", result["summary"]) + + def test_context_hash_drift_rejects_before_transport(self): + request = self.helper() + request["allowed_context"]["selections"][0]["source_sha256"] = "0" * 64 + transport = FakeTransport() + result = GearboxRunner(self.base / "state", helper_transport=transport).run(request) + self.assertEqual(result["status"], "failed") + self.assertEqual(transport.calls, 0) + + def test_context_ceiling_rejects_without_transport(self): + request = self.helper() + request["budget"]["max_context_bytes"] = 1 + transport = FakeTransport() + result = GearboxRunner(self.base / "state", helper_transport=transport).run(request) + self.assertEqual(result["status"], "failed") + self.assertEqual(transport.calls, 0) + + def test_malformed_helper_final_fails_closed(self): + transport = FakeTransport(final={"verdict": "PASS"}) + result = GearboxRunner(self.base / "state", helper_transport=transport).run(self.helper()) + self.assertEqual(result["status"], "failed") + self.assertIn("is missing", result["summary"]) + + def test_helper_output_contract_requires_the_compact_fields(self): + request = self.helper() + request["output_contract"]["schema"] = { + "type": "object", "properties": {}, "required": [], + } + with self.assertRaisesRegex(GearboxError, "standard compact fields"): + validate_request(request) + + def test_cli_keeps_operator_indicator_off_stdout(self): + request_path = self.base / "request.json" + request_path.write_bytes(canonical_json(self.deterministic())) + environment = dict(os.environ) + environment["PYTHONPATH"] = str(Path(__file__).parents[1] / "src") + completed = subprocess.run( + [ + sys.executable, "-m", "opsle_gearbox.cli", "--request", str(request_path), + "--state", str(self.base / "cli-state"), + ], + check=True, + capture_output=True, + env=environment, + ) + payload = json.loads(completed.stdout) + self.assertEqual(payload["status"], "completed") + self.assertNotIn(b"[Gearbox]", completed.stdout) + self.assertIn(b"[Gearbox] deterministic completed", completed.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/dogfood b/tools/dogfood new file mode 100755 index 0000000..bbbfdcb --- /dev/null +++ b/tools/dogfood @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +"""Run the provider-free release fixture and optionally copy public evidence.""" + +from __future__ import annotations + +import argparse +import shutil +import sys +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "src")) + +from opsle_gearbox.core import ( + REQUEST_SCHEMA, + GearboxRunner, + canonical_json, + operator_indicator, + read_object, + sha256_file, +) + + +def request(policy: Path) -> dict: + return { + "schema": REQUEST_SCHEMA, + "task": {"argv": ["release-fixture"]}, + "task_type": "fixture", + "requested_gear": "release-fixture", + "allowed_context": { + "repository": str(ROOT), + "selections": [], + "writable_paths": [], + }, + "output_contract": { + "schema": {"type": "object", "properties": {}, "required": []}, + }, + "authority": { + "policy_path": str(policy), + "policy_sha256": sha256_file(policy), + }, + "budget": { + "timeout_seconds": 30, + "max_raw_bytes": 65536, + "max_return_bytes": 8192, + "max_context_bytes": 0, + "max_commands": 1, + "max_provider_sessions": 0, + }, + } + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--state", type=Path) + parser.add_argument("--evidence-dir", type=Path) + parser.add_argument("--mechanism-revision") + args = parser.parse_args() + policy = ROOT / "examples/release-policy.json" + temporary = tempfile.TemporaryDirectory() if args.state is None else None + state = args.state or Path(temporary.name) + value = request(policy) + runner = GearboxRunner( + state, + mechanism_revision=args.mechanism_revision, + ) + result = runner.run(value) + run_dir = state / "runs" / result["run_id"] + receipt = read_object(run_dir / "value-receipt.json") + print(operator_indicator(result, receipt), file=sys.stderr) + sys.stdout.buffer.write(canonical_json(result)) + if args.evidence_dir: + args.evidence_dir.mkdir(parents=True, exist_ok=True) + (args.evidence_dir / "result.json").write_bytes(canonical_json(result)) + shutil.copyfile(run_dir / "value-receipt.json", args.evidence_dir / "value-receipt.json") + public_run_dir = args.evidence_dir / "runs" / result["run_id"] + public_raw_dir = public_run_dir / "raw" + public_raw_dir.mkdir(parents=True, exist_ok=True) + shutil.copyfile( + run_dir / "value-receipt.json", + public_run_dir / "value-receipt.json", + ) + for raw_name in ("stdout.raw", "stderr.raw"): + shutil.copyfile( + run_dir / "raw" / raw_name, + public_raw_dir / raw_name, + ) + raw = read_object(run_dir / "value-receipt.json") + verification = { + "schema": "opsle.gearbox.release-evidence.v1", + "mechanism_revision": args.mechanism_revision, + "policy_sha256": sha256_file(policy), + "request_sha256": result["request_sha256"], + "result_sha256": sha256_file(args.evidence_dir / "result.json"), + "value_receipt_sha256": sha256_file(args.evidence_dir / "value-receipt.json"), + "raw_artifacts": [ + { + "locator": artifact["locator"], + "sha256": artifact["sha256"], + "bytes": artifact["bytes"], + } + for artifact in result["artifacts"] + ], + "status": result["status"], + "provider_sessions": result["metrics"]["provider_sessions"], + "fallback_attempts": result["metrics"]["fallback_attempts"], + "value_measurement_count": len(raw["measurements"]), + "test_command": "tools/verify", + } + (args.evidence_dir / "verification.json").write_bytes(canonical_json(verification)) + return 0 if result["status"] == "completed" else 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/release-fixture b/tools/release-fixture new file mode 100755 index 0000000..cfe686d --- /dev/null +++ b/tools/release-fixture @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 +"""Provider-free deterministic Gearbox release fixture.""" + +import json + +print(json.dumps({ + "fixture": "opsle-gearbox-release-v1", + "status": "PASS", +}, separators=(",", ":"), sort_keys=True)) diff --git a/tools/verify b/tools/verify new file mode 100755 index 0000000..c0efce9 --- /dev/null +++ b/tools/verify @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euo pipefail + +root=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) +cd "$root" + +PYTHONPATH=src python3 -m unittest \ + discover -s tests -p 'test_*.py' +python3 -m compileall -q src tests +git diff --check