From 6005b340a6f6fb3f8683439d6b5fd154e1fd253f Mon Sep 17 00:00:00 2001 From: Deploy Date: Sat, 29 Aug 2026 02:42:08 +0000 Subject: [PATCH 1/3] feat: publish portable Gearbox core --- .github/workflows/ci.yml | 22 + .gitignore | 9 + ARCHITECTURE.md | 63 ++ BENCHMARK.md | 19 + LIMITATIONS.md | 21 + NOTICE | 7 + PROVENANCE.md | 31 + README.md | 122 ++++ SECURITY.md | 43 ++ SPEC.md | 124 ++++ THEORY.md | 54 ++ examples/release-policy.json | 13 + pyproject.toml | 22 + src/opsle_gearbox/__init__.py | 19 + src/opsle_gearbox/cli.py | 54 ++ src/opsle_gearbox/core.py | 1117 +++++++++++++++++++++++++++++++++ tests/test_core.py | 369 +++++++++++ tools/dogfood | 116 ++++ tools/release-fixture | 9 + tools/verify | 10 + 20 files changed, 2244 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 ARCHITECTURE.md create mode 100644 BENCHMARK.md create mode 100644 LIMITATIONS.md create mode 100644 NOTICE create mode 100644 PROVENANCE.md create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 SPEC.md create mode 100644 THEORY.md create mode 100644 examples/release-policy.json create mode 100644 pyproject.toml create mode 100644 src/opsle_gearbox/__init__.py create mode 100644 src/opsle_gearbox/cli.py create mode 100644 src/opsle_gearbox/core.py create mode 100644 tests/test_core.py create mode 100755 tools/dogfood create mode 100755 tools/release-fixture create mode 100755 tools/verify diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..44d30ec --- /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@v4 + - uses: actions/setup-python@v5 + 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/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 From 70b534c25b82bd44e5a6882427126c68fb55edd0 Mon Sep 17 00:00:00 2001 From: Deploy Date: Sat, 29 Aug 2026 02:43:14 +0000 Subject: [PATCH 2/3] evidence: record provider-free release fixture --- evidence/release-001/README.md | 10 ++++++++++ evidence/release-001/result.json | 1 + .../runs/ga587483b4894867066c18309/raw/stderr.raw | 0 .../runs/ga587483b4894867066c18309/raw/stdout.raw | 1 + .../runs/ga587483b4894867066c18309/value-receipt.json | 1 + evidence/release-001/value-receipt.json | 1 + evidence/release-001/verification.json | 1 + 7 files changed, 15 insertions(+) create mode 100644 evidence/release-001/README.md create mode 100644 evidence/release-001/result.json create mode 100644 evidence/release-001/runs/ga587483b4894867066c18309/raw/stderr.raw create mode 100644 evidence/release-001/runs/ga587483b4894867066c18309/raw/stdout.raw create mode 100644 evidence/release-001/runs/ga587483b4894867066c18309/value-receipt.json create mode 100644 evidence/release-001/value-receipt.json create mode 100644 evidence/release-001/verification.json 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"} From 15a8a4c2111674ce77bcaa9183f4b87003a1640d Mon Sep 17 00:00:00 2001 From: Deploy Date: Sat, 29 Aug 2026 02:44:07 +0000 Subject: [PATCH 3/3] ci: use current official action runtimes --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 44d30ec..d853286 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,8 +13,8 @@ jobs: verify: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 with: python-version: "3.12" - run: python -m pip install .