diff --git a/.github/scripts/pitot_e2e_report.py b/.github/scripts/pitot_e2e_report.py index 389cfb7..df482c2 100644 --- a/.github/scripts/pitot_e2e_report.py +++ b/.github/scripts/pitot_e2e_report.py @@ -20,6 +20,7 @@ ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT / "scripts")) from pitot_adapter_supervisor import ContractError, validate_manifest +from installation_identity import validate_identity_evidence MANIFEST = json.loads((ROOT / "adapter-verification.json").read_text(encoding="utf-8")) @@ -126,6 +127,7 @@ def validate_result( "version", "executable", "executable_sha256", + "installation", "installer", "runtime", }: @@ -137,9 +139,12 @@ def validate_result( ): raise ValueError("CLI receipt does not match supervised manifest") if not all( - isinstance(item, str) and item and "\0" not in item for item in cli.values() + isinstance(item, str) and item and "\0" not in item + for key, item in cli.items() + if key != "installation" ): raise ValueError("invalid CLI receipt values") + validate_identity_evidence(agent, platform, cli["installation"]) cells = [ cell for cell in ENDPOINT_PROVENANCE["cells"] @@ -150,6 +155,8 @@ def validate_result( if len(cells) != 1: raise ValueError("missing supervised platform provenance") fixture = cells[0] + if cli["installation"] != fixture.get("installation"): + raise ValueError("installed-code identity does not match binary capture") if cli["executable_sha256"] != fixture["executable_sha256"]: raise ValueError("CLI executable digest does not match binary capture") if value["protocol"] != fixture["dialect"] or not re.fullmatch( @@ -169,6 +176,7 @@ def validate_result( "request": fixture["request"], "response": fixture["response"], "executable_sha256": fixture["executable_sha256"], + "installation": fixture["installation"], } if value["endpoint"] != expected_endpoint: raise ValueError( diff --git a/.github/workflows/pitot-e2e-agent.yml b/.github/workflows/pitot-e2e-agent.yml index 2557f40..8aa13bb 100644 --- a/.github/workflows/pitot-e2e-agent.yml +++ b/.github/workflows/pitot-e2e-agent.yml @@ -95,6 +95,14 @@ jobs: ${{ runner.temp }}/pitot-e2e/deepseek-native-session.zstd if-no-files-found: error retention-days: 14 + - name: Upload safe installation diagnostic + if: failure() + uses: actions/upload-artifact@v4 + with: + name: pitot-install-probe-${{ inputs.agent }}-${{ inputs.platform }} + path: ${{ runner.temp }}/pitot-e2e/install.probe.json + if-no-files-found: ignore + retention-days: 14 - name: Upload endpoint provenance candidate if: always() && inputs.capture uses: actions/upload-artifact@v4 diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 7a7d5a4..fbfd3d6 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -165,5 +165,7 @@ jobs: - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} + - name: Verify native installation identity + run: python -m unittest discover -s tests -p test_installation_identity.py - name: Build and verify installed SDK artifacts run: python scripts/check_sdk_packages.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 28767e0..3f50e12 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -121,25 +121,24 @@ name/export errors (E9, F821, F822, F823); formatting remains a separate gate. ## Refreshing agent endpoint provenance -The installer selects its own release location or npm global launcher rather -than rediscovering the agent on PATH. It records the raw version output only -after the complete product banner matches the literal catalog pin and the -entrypoint bytes stay unchanged across the version probe. An unexpected banner -is a verification failure that needs investigation; mentioning the pin is -insufficient. - -Before creating runtime or fixture effects, the driver checks the receipt's -schema, selected agent/platform/runtime/installer, owned absolute path, current -file digest and freshly observed version output. It checks the file digest -again immediately before and after the verification session. A failed or -timed-out version probe, malformed receipt or changed file cannot produce a -successful verification result. - -These are observations of the entrypoint and its self-reported version on a -trusted runner. They do not authenticate a publisher, freeze the file against -concurrent changes, or cover all programs and package files delegated to by an -npm or Windows launcher. Historical endpoint captures retain their original -scope; this check does not upgrade their identity evidence. +Codex, Qwen and DeepSeek install into fresh, owned npm prefixes. Their receipts +bind the package tree (names, types, bytes and platform metadata), the selected +Node binary and version, and the package's JavaScript entrypoint. Version probes +and verification sessions invoke that absolute Node/JavaScript pair. Qwen's +managed-update pin keeps execution in the recorded package; a seeded external +update proves the selection test can detect an unpinned launch. + +The driver validates the receipt before creating session fixtures and rechecks +installation identity around execution. A changed package or Node binary, +malformed receipt, unexpected version banner, or failed probe rejects admission. +Bounded failed-probe streams remain local; public diagnostics contain only +failure reasons, byte counts and digests. Other agents retain entrypoint-only +identity checks. + +These are observations on a trusted runner, not publisher authentication or an +atomic filesystem snapshot. POSIX mode bits and Windows readonly attributes are +recorded; Windows ACLs, operating-system libraries and changes between checks +are outside the guarantee. Historical captures retain their original scope. Protocol documentation is not a verification authority. When a pinned agent version changes, dispatch the existing **Pitot E2E** workflow with @@ -169,7 +168,8 @@ the selected current attempt; a partial rerun cannot borrow earlier cells. Local JSON files cannot authorize a capture refresh. Acquisition or validation failure preserves the accepted file; success replaces it atomically. -The schema4 record keeps wire cells and their hash meaning, and adds the +The schema5 record adds portable installation identity to the wire cells and +retains the `capture-direct/v1` producer profile to the source run, job/check-run IDs and artifact digests. Its sorted file hashes cover the capture orchestration, proxies, witness and canary helpers, catalog-declared integrations, agent diff --git a/cmd/pitot/deepseek_acp.go b/cmd/pitot/deepseek_acp.go index e84bed8..e089aa1 100644 --- a/cmd/pitot/deepseek_acp.go +++ b/cmd/pitot/deepseek_acp.go @@ -59,6 +59,11 @@ func runDeepSeekACP(ctx context.Context, args []string, stdout, stderr io.Writer options.Prompt = value case "--exec": options.Executable = value + case "--entrypoint": + if len(options.Arguments) != 0 { + return errors.New("pitot: duplicate DeepSeek entrypoint") + } + options.Arguments = []string{value} case "--cwd": options.Cwd = value case "--model": diff --git a/cmd/pitot/deepseek_acp_test.go b/cmd/pitot/deepseek_acp_test.go new file mode 100644 index 0000000..7016bab --- /dev/null +++ b/cmd/pitot/deepseek_acp_test.go @@ -0,0 +1,43 @@ +package main + +import ( + "context" + "io" + "os" + "path/filepath" + "runtime" + "strings" + "testing" +) + +func TestDeepSeekExplicitNodeEntrypoint(t *testing.T) { + root := t.TempDir() + name := "node" + if runtime.GOOS == "windows" { + name += ".exe" + } + node := filepath.Join(root, name) + if err := os.WriteFile(node, []byte("owned runtime placeholder"), 0o700); err != nil { + t.Fatal(err) + } + entry := filepath.Join(root, "package with spaces", "工具.js") + program, arguments, err := deepSeekCommand(node, []string{entry}) + if err != nil || program != node || len(arguments) != 1 || arguments[0] != entry { + t.Fatalf("command = %q %q, error = %v", program, arguments, err) + } + for _, args := range [][]string{{"relative.js"}, {filepath.Join(root, "entry.txt")}, {entry, "--extra"}} { + if _, _, err := deepSeekCommand(node, args); err == nil { + t.Fatalf("accepted unsupported entrypoint arguments %q", args) + } + } + if _, _, err := deepSeekCommand(filepath.Join(root, "dsh"), []string{entry}); err == nil { + t.Fatal("accepted a JS entrypoint with a non-Node executable") + } +} + +func TestDeepSeekRejectsDuplicateEntrypointBeforeLaunch(t *testing.T) { + err := runDeepSeekACP(context.Background(), []string{"--runtime", "r", "--prompt", "p", "--entrypoint", "first.js", "--entrypoint", "second.js"}, io.Discard, io.Discard) + if err == nil || !strings.Contains(err.Error(), "duplicate DeepSeek entrypoint") { + t.Fatalf("duplicate entrypoint error = %v", err) + } +} diff --git a/cmd/pitot/main.go b/cmd/pitot/main.go index f538297..87595d5 100644 --- a/cmd/pitot/main.go +++ b/cmd/pitot/main.go @@ -366,7 +366,7 @@ usage: pitot init --host HOST [--force] pitot dev --host HOST -- AGENT [ARGS...] pitot acp devin --runtime PATH --prompt TEXT [--exec PATH] [--model MODEL] [--agent-type TYPE] [--cwd PATH] - pitot acp deepseek --runtime PATH --prompt TEXT [--exec PATH] [--cwd PATH] [--model MODEL] [--base-url URL --api-key-env NAME] [--session-dir PATH] + pitot acp deepseek --runtime PATH --prompt TEXT [--exec PATH] [--entrypoint JS_PATH] [--cwd PATH] [--model MODEL] [--base-url URL --api-key-env NAME] [--session-dir PATH] pitot doctor [--host HOST] [--fix] pitot run [--config PATH] [--strict] --runtime PATH pitot hook HOST [--runtime PATH] diff --git a/scripts/pitot_adapter_supervisor.py b/scripts/pitot_adapter_supervisor.py index 895ba48..064fe86 100644 --- a/scripts/pitot_adapter_supervisor.py +++ b/scripts/pitot_adapter_supervisor.py @@ -13,6 +13,9 @@ ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "tests")) +from installation_identity import validate_identity_evidence + MANIFEST = Path("adapter-verification.json") ENDPOINT_PROVENANCE = Path("tests/endpoint-provenance.json") README = Path("README.md") @@ -209,14 +212,14 @@ def validate_endpoint_provenance(root: Path, manifest: dict[str, object]) -> Non raise ContractError(f"cannot load {ENDPOINT_PROVENANCE}: {error}") from error validate_endpoint_value(value, manifest) if ( - value["schema_version"] in {3, 4} + value["schema_version"] in {3, 4, 5} and value["source"]["catalog_sha256"] != hashlib.sha256((root / MANIFEST).read_bytes()).hexdigest() ): raise ContractError( "endpoint capture source catalog digest differs from current manifest" ) - if value["schema_version"] == 4: + if value["schema_version"] in {4, 5}: from pitot_capture_source import validate_current_producers try: @@ -233,7 +236,7 @@ def validate_endpoint_value(value: object, manifest: dict[str, object]) -> None: "capture_policy", "cells", } - if isinstance(value, dict) and value.get("schema_version") in {3, 4}: + if isinstance(value, dict) and value.get("schema_version") in {3, 4, 5}: required.add("source") if not isinstance(value, dict) or set(value) != required: raise ContractError( @@ -241,7 +244,7 @@ def validate_endpoint_value(value: object, manifest: dict[str, object]) -> None: ) if ( type(value["schema_version"]) is not int - or value["schema_version"] not in {2, 3, 4} + or value["schema_version"] not in {2, 3, 4, 5} or value["capture_policy"] != "pinned_real_cli_capture" ): raise ContractError("endpoint provenance must require pinned real-CLI capture") @@ -274,6 +277,8 @@ def validate_endpoint_value(value: object, manifest: dict[str, object]) -> None: "provenance", "capture_sha256", } + if value["schema_version"] == 5: + required.add("installation") for cell in cells: agent_id, platform = cell["agent"], cell["platform"] if set(cell) != required: @@ -281,6 +286,11 @@ def validate_endpoint_value(value: object, manifest: dict[str, object]) -> None: f"endpoint fixture {agent_id}/{platform} has an invalid schema" ) agent = declared[agent_id] + if value["schema_version"] == 5: + try: + validate_identity_evidence(agent_id, platform, cell["installation"]) + except ValueError as error: + raise ContractError(str(error)) from error if ( cell["version"] != agent["version"] or cell["runtime"] != agent["runtime"][platform] @@ -379,7 +389,7 @@ def validate_endpoint_value(value: object, manifest: dict[str, object]) -> None: raise ContractError( f"endpoint fixture {agent_id}/{platform} has a fabricated capture digest" ) - if value["schema_version"] in {3, 4}: + if value["schema_version"] in {3, 4, 5}: from pitot_capture_source import validate_source_index try: @@ -387,7 +397,7 @@ def validate_endpoint_value(value: object, manifest: dict[str, object]) -> None: value["source"], cells, manifest, - producer_binding=value["schema_version"] == 4, + producer_binding=value["schema_version"] in {4, 5}, ) except (ValueError, KeyError, TypeError) as error: raise ContractError(f"invalid endpoint capture source: {error}") from error diff --git a/scripts/pitot_capture_source.py b/scripts/pitot_capture_source.py index 5b05150..f360819 100644 --- a/scripts/pitot_capture_source.py +++ b/scripts/pitot_capture_source.py @@ -36,6 +36,10 @@ "tests/e2e_unified_runner.sh", "tests/install_real_agent.py", "tests/agent_identity.py", + "tests/installation_identity.py", + "cmd/pitot/deepseek_acp.go", + "internal/deepseekacp/version.go", + "internal/deepseekacp/client.go", "scripts/pitot_capture_source.py", "scripts/pitot_adapter_supervisor.py", ".github/scripts/pitot_e2e_report.py", @@ -788,7 +792,7 @@ def collect( "local producer changed during collection", ) value = { - "schema_version": 4, + "schema_version": 5, "capture_policy": "pinned_real_cli_capture", "cells": cells, "source": { diff --git a/tests/agent_identity.py b/tests/agent_identity.py index 65b82fd..0a20791 100644 --- a/tests/agent_identity.py +++ b/tests/agent_identity.py @@ -14,8 +14,20 @@ import sys import tempfile +from installation_identity import ( + PRIORITY_AGENTS, + VersionProbeError, + installation_command, + installation_environment, + installation_prefix, + package_entrypoint, + verify_installation, +) + VERSION_TIMEOUT = 30 MAX_VERSION_BYTES = 65536 + + RECEIPT_FIELDS = { "schema_version", "agent", @@ -140,13 +152,11 @@ def owned_entrypoint(agent: dict[str, object], platform: str, runtime: str) -> s raise RuntimeError("unsupported agent runtime") kind = agent["installer"]["kind"] executable = str(agent["executable"]) - if agent["id"] == "deepseek": + if agent["id"] in PRIORITY_AGENTS: return str( - Path.home() - / ".local/share/pitot-deepseek" - / str(agent["version"]) + installation_prefix(agent) / "node_modules/.bin" - / ("dsh.cmd" if platform == "windows" else "dsh") + / (f"{executable}.cmd" if platform == "windows" else executable) ) if kind == "npm": result = subprocess.run( @@ -345,13 +355,23 @@ def cursor_context(home: Path, data: Path) -> dict[str, str]: } -def probe_version(agent: dict[str, object], path: str, runtime: str) -> tuple[str, str]: +def probe_version( + agent: dict[str, object], + path: str, + runtime: str, + installation: dict[str, object] | None = None, +) -> tuple[str, str]: if agent["id"] == "kimi": require_kimi_ready(path, runtime) elif agent["id"] == "cursor": require_cursor_ready(path, runtime) before = entrypoint_digest(path, runtime) - command = [path, "--version"] + if installation is not None: + verify_installation(installation) + command = [ + *(installation_command(installation) if installation is not None else [path]), + "--version", + ] if host_controls_wsl(runtime): command = ["wsl.exe", "--distribution", "Ubuntu", "--", *command] with ExitStack() as cleanup: @@ -418,20 +438,73 @@ def probe_version(agent: dict[str, object], path: str, runtime: str) -> tuple[st } ) options["env"] = environment - result = subprocess.run( - command, - check=True, - stdin=subprocess.DEVNULL, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - timeout=VERSION_TIMEOUT, - **options, - ) + if installation is not None: + options["env"] = installation_environment( + str(agent["id"]), installation, options.get("env", dict(os.environ)) + ) + probe_error = None + result = None + try: + result = subprocess.run( + command, + check=True, + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + timeout=VERSION_TIMEOUT, + **options, + ) + except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as error: + if installation is None: + raise + probe_error = VersionProbeError( + "timed-out" + if isinstance(error, subprocess.TimeoutExpired) + else "nonzero-exit", + error.stdout, + error.stderr, + getattr(error, "returncode", None), + ) + finally: + if installation is not None: + try: + verify_installation(installation) + except RuntimeError: + if probe_error is None: + if result is None: + raise + probe_error = VersionProbeError( + "identity-recheck-failed", + result.stdout, + result.stderr, + result.returncode, + ) + probe_error.diagnostic["identity_recheck"] = "failed" + if probe_error is not None: + raise probe_error from None if len(result.stdout) + len(result.stderr) > MAX_VERSION_BYTES: + if installation is not None: + raise VersionProbeError( + "output-limit", result.stdout, result.stderr, result.returncode + ) raise RuntimeError("agent version output exceeds limit") - output = result.stdout.decode("utf-8", errors="strict") - error = result.stderr.decode("utf-8", errors="strict") + try: + output = result.stdout.decode("utf-8", errors="strict") + error = result.stderr.decode("utf-8", errors="strict") + except UnicodeDecodeError: + if installation is not None: + raise VersionProbeError( + "invalid-encoding", result.stdout, result.stderr, result.returncode + ) from None + raise if not stderr_matches(agent, error) or not version_matches(agent, output): + if installation is not None: + raise VersionProbeError( + "unexpected-banner-or-stderr", + result.stdout, + result.stderr, + result.returncode, + ) raise RuntimeError( "agent did not report the exact supervised version; " f"stdout={output[:4096]!r}; stderr={error[:4096]!r}" @@ -445,6 +518,8 @@ def probe_version(agent: dict[str, object], path: str, runtime: str) -> tuple[st def verify_live_entrypoint(receipt: dict[str, object]) -> None: + if receipt.get("agent") in PRIORITY_AGENTS: + verify_installation(receipt.get("installation")) if receipt.get("agent") == "kimi": require_kimi_ready(receipt["executable"], receipt["runtime"]) elif receipt.get("agent") == "cursor": @@ -460,9 +535,13 @@ def inspect_receipt( receipt: object, agent: dict[str, object], platform: str ) -> dict[str, object]: """Admit the selected cell before the driver creates runtime or fixture effects.""" - if not isinstance(receipt, dict) or set(receipt) != RECEIPT_FIELDS: + priority = agent["id"] in PRIORITY_AGENTS + fields = RECEIPT_FIELDS | ({"installation"} if priority else set()) + if not isinstance(receipt, dict) or set(receipt) != fields: raise RuntimeError("invalid installation receipt fields") - if type(receipt["schema_version"]) is not int or receipt["schema_version"] != 1: + if type(receipt["schema_version"]) is not int or receipt["schema_version"] != ( + 2 if priority else 1 + ): raise RuntimeError("unsupported installation receipt schema") for field in RECEIPT_FIELDS - {"schema_version"}: if ( @@ -486,8 +565,18 @@ def inspect_receipt( raise RuntimeError("installation receipt does not select the owned entrypoint") if not version_matches(agent, receipt["version_output"]): raise RuntimeError("installation receipt has an unexpected version banner") + if priority: + verify_installation(receipt["installation"]) + if receipt["installation"]["prefix"] != str( + installation_prefix(agent) + ) or receipt["installation"]["entrypoint"] != str( + package_entrypoint(agent, installation_prefix(agent)) + ): + raise RuntimeError("receipt does not select the owned package entrypoint") verify_live_entrypoint(receipt) - output, digest = probe_version(agent, receipt["executable"], receipt["runtime"]) + output, digest = probe_version( + agent, receipt["executable"], receipt["runtime"], receipt.get("installation") + ) if digest != receipt["executable_sha256"] or output != receipt["version_output"]: raise RuntimeError("agent identity changed since installation receipt") return receipt diff --git a/tests/endpoint-provenance.json b/tests/endpoint-provenance.json index 5f5b5dc..ad639a1 100644 --- a/tests/endpoint-provenance.json +++ b/tests/endpoint-provenance.json @@ -3,9 +3,12 @@ "cells": [ { "agent": "claude", - "capture_sha256": "6eb5f870d6d9eec0fd8bdfce34693ce86e057feb07df5aa9a4af3f3bd706ea8c", + "capture_sha256": "5708547a3442f7789838ecf0a8840e6d71e9f38f0e0406ea8f0f6f22b1898a1a", "dialect": "anthropic_messages", "executable_sha256": "c9b5341637becbd423ddffc5b254afb645682a3868cb708bbc6cc0e7bb419937", + "installation": { + "scope": "entrypoint/v1" + }, "platform": "macos", "provenance": "pinned_real_cli_capture", "request": { @@ -45,9 +48,12 @@ }, { "agent": "claude", - "capture_sha256": "21fa65e59854f6623ddc764b14463d23d1bf87527e4a4173501a0dc354a7dce9", + "capture_sha256": "adcd14006488d4df783429269c2c1a4cfc107045503a5980da0dca6a9422a787", "dialect": "anthropic_messages", "executable_sha256": "24972e3bc859fab2b46ed4c1e51f7d6130f06d3bd550811a114640de3370d0de", + "installation": { + "scope": "entrypoint/v1" + }, "platform": "ubuntu", "provenance": "pinned_real_cli_capture", "request": { @@ -87,9 +93,12 @@ }, { "agent": "claude", - "capture_sha256": "de00faa86bcc17a3858475a91044c6a28b24a1af11650bb7758044643de2aa1f", + "capture_sha256": "53f68de22f5190e01a339b2960dbb38b2bd649a4004b0e4e5f28cd3d08bf9925", "dialect": "anthropic_messages", "executable_sha256": "7999fba95dbffe167d9e0a043f29057979a0518ebe89b60c4fcfc6401ea8c424", + "installation": { + "scope": "entrypoint/v1" + }, "platform": "windows", "provenance": "pinned_real_cli_capture", "request": { @@ -129,9 +138,19 @@ }, { "agent": "codex", - "capture_sha256": "5e39c8858cfe781d5febb081b393afd0c385a73c33bf6609ca7d7b5a4749852e", + "capture_sha256": "d55f571d3db096ba3333ce57423f3af12804d62b82df2a3e5606f9f69c4b01b8", "dialect": "openai_responses", "executable_sha256": "61b0194f3bb6534439c8d26a3ed57d0805f84b884588b761795323eeb92fcf70", + "installation": { + "entrypoint": "node_modules/@openai/codex/bin/codex.js", + "metadata": "posix-mode", + "node_sha256": "18e387c90ab8a8400183e8bdd396376e1e875b91b4c874b894dcade7b35bf572", + "node_version": "v22.23.2", + "scope": "owned-prefix-and-node/v1", + "strategy": "absolute-node-js/v1", + "tree_profile": "owned-installation/v1", + "tree_sha256": "e2917469ec723fa1af7fb2642ea2b8747d26d366df6923aff89f4567f1db7691" + }, "platform": "macos", "provenance": "pinned_real_cli_capture", "request": { @@ -179,9 +198,19 @@ }, { "agent": "codex", - "capture_sha256": "a45c25b9a94213356abc092f187117258099981423758f74e27cf6398efea157", + "capture_sha256": "60977d4a8c9cb5d3f2ee339756837d0262587072583ed1ca7a6148728889289d", "dialect": "openai_responses", "executable_sha256": "61b0194f3bb6534439c8d26a3ed57d0805f84b884588b761795323eeb92fcf70", + "installation": { + "entrypoint": "node_modules/@openai/codex/bin/codex.js", + "metadata": "posix-mode", + "node_sha256": "fde6a4bf8d0562f7751d1a2d6cb9b417c4cfe107bbcb0aa3e9a24e125e348f48", + "node_version": "v22.23.3", + "scope": "owned-prefix-and-node/v1", + "strategy": "absolute-node-js/v1", + "tree_profile": "owned-installation/v1", + "tree_sha256": "cd43f720b7506f18b2d569decc568f2d1cf59196502aa98d8086a66babb149d5" + }, "platform": "ubuntu", "provenance": "pinned_real_cli_capture", "request": { @@ -229,9 +258,19 @@ }, { "agent": "codex", - "capture_sha256": "1f90c8f26662b2532208cf6abe8bc263e22cadc0a05fd966dc5cfe13652afb5c", + "capture_sha256": "4944bae99385b50a7d5821b108398d10dbe07e6324168b22f5a353d42e50f57d", "dialect": "openai_responses", - "executable_sha256": "c54db6755e710c39703f7c37512f9e35ed41042d8080558d2b84b8d2694323c3", + "executable_sha256": "2b3dd1940b0bedfcb54528a42694a6a85fdac196ea877115419a25381ed1005f", + "installation": { + "entrypoint": "node_modules/@openai/codex/bin/codex.js", + "metadata": "windows-readonly", + "node_sha256": "9c9245166b4a8e182e0b797da9c20136117ff24368eaff1fec8343a123c8db0e", + "node_version": "v22.23.3", + "scope": "owned-prefix-and-node/v1", + "strategy": "absolute-node-js/v1", + "tree_profile": "owned-installation/v1", + "tree_sha256": "45ac15287dcb86e2675ca90af0aba0604d63fdf9920f1eb49b2d40f454d42158" + }, "platform": "windows", "provenance": "pinned_real_cli_capture", "request": { @@ -279,9 +318,12 @@ }, { "agent": "copilot", - "capture_sha256": "c922e1edecb25cf7909b1fdb0bf8144a7c05ea841ad2315f5fc7656873571dd8", + "capture_sha256": "d03816631d68c5cc22e18102e6d06bc01e3a435f583648ef7b3e871bcb31b847", "dialect": "openai_chat", "executable_sha256": "0ea824a86be5757533fdb092eff7050871bd7a711a46babde0ffe0e44ac5ad88", + "installation": { + "scope": "entrypoint/v1" + }, "platform": "macos", "provenance": "pinned_real_cli_capture", "request": { @@ -333,9 +375,12 @@ }, { "agent": "copilot", - "capture_sha256": "d17ae1033aafd6c8c7eb5ee2cf2cb134a661ecc37944169574d60806038fa103", + "capture_sha256": "587960da7ac17da700faeae5d9fa3a4dff9ce729fdaffb01d4cd52b9fdd0f9ef", "dialect": "openai_chat", "executable_sha256": "0ea824a86be5757533fdb092eff7050871bd7a711a46babde0ffe0e44ac5ad88", + "installation": { + "scope": "entrypoint/v1" + }, "platform": "ubuntu", "provenance": "pinned_real_cli_capture", "request": { @@ -387,9 +432,12 @@ }, { "agent": "copilot", - "capture_sha256": "01594240b7ce34526bf7ccbc15bcf9f2ba4b2fff552d6e949c501720df7a0868", + "capture_sha256": "e7a4c28bad0899a9774f4bcdc264c8d453fd80afe6a03304a490d0bd457aa086", "dialect": "openai_chat", "executable_sha256": "959e8ea2c63474956adc1de9c884df9018a686a9c5463e8366b438161c01c8f8", + "installation": { + "scope": "entrypoint/v1" + }, "platform": "windows", "provenance": "pinned_real_cli_capture", "request": { @@ -441,9 +489,12 @@ }, { "agent": "cursor", - "capture_sha256": "45b8810fda9834e0a492a9926dffb9be57ee1338d513286ef17d3abe005751b2", + "capture_sha256": "f6005b969def3bb81ec2aabcedae6f32e662aee91bb60b68a08bb72efb5fc49b", "dialect": "cursor_connect_proto", "executable_sha256": "2ccc9a8e167797641448b5e5c936f006ba137a2555f117f38c5eb76a5238a233", + "installation": { + "scope": "entrypoint/v1" + }, "platform": "macos", "provenance": "pinned_real_cli_capture", "request": { @@ -470,9 +521,12 @@ }, { "agent": "cursor", - "capture_sha256": "e10c16ce205909adef763903de2d0ac84eabb2e1b496347992ad3c3360213908", + "capture_sha256": "286e0e249de8e190f8b45153b5c28eb5eedfcc3104b05b751370063dad5df502", "dialect": "cursor_connect_proto", "executable_sha256": "2ccc9a8e167797641448b5e5c936f006ba137a2555f117f38c5eb76a5238a233", + "installation": { + "scope": "entrypoint/v1" + }, "platform": "ubuntu", "provenance": "pinned_real_cli_capture", "request": { @@ -499,9 +553,12 @@ }, { "agent": "cursor", - "capture_sha256": "b8af04ff98e36d1f956dd00742a51aed5325e2fef51e867edca544ae8da00555", + "capture_sha256": "efeaa573c8a48652a6a8a4ee3e664075565712cf131cd1fc58a30e46f2552027", "dialect": "cursor_connect_proto", "executable_sha256": "299eaddf3327768e7fcd64f29ad01747c740f7e316931575fb3cee617c92ccfc", + "installation": { + "scope": "entrypoint/v1" + }, "platform": "windows", "provenance": "pinned_real_cli_capture", "request": { @@ -528,9 +585,19 @@ }, { "agent": "deepseek", - "capture_sha256": "9f36db049a9f7ac3a58356a9aa17e1bc3727e1a41badd1e2e3b3af30491f672c", + "capture_sha256": "672d94b5bc6d05fc7009b6e69bd7ba552dae90ea718c5dc4f157baa704b5b32a", "dialect": "openai_chat", "executable_sha256": "1a03dee18683483ff6a1b27b2a1e650230da6bcca9a0f55f7d81b3308c3f307f", + "installation": { + "entrypoint": "node_modules/@deepseek-ai/dsh/lib/bin.js", + "metadata": "posix-mode", + "node_sha256": "18e387c90ab8a8400183e8bdd396376e1e875b91b4c874b894dcade7b35bf572", + "node_version": "v22.23.2", + "scope": "owned-prefix-and-node/v1", + "strategy": "absolute-node-js/v1", + "tree_profile": "owned-installation/v1", + "tree_sha256": "02fcab246905871dc7b5b00383f2432c0fe0d19b2a95d01b918a7032e6132744" + }, "platform": "macos", "provenance": "pinned_real_cli_capture", "request": { @@ -590,9 +657,19 @@ }, { "agent": "deepseek", - "capture_sha256": "5c08fb7a1644f87521c9321c99b6deca046ba0c46993d7d747f51215c587f4b2", + "capture_sha256": "146c456ea93ec95cb4ca9e2e291a318660bf7d47114b5f018e78eff796fad9cd", "dialect": "openai_chat", "executable_sha256": "1a03dee18683483ff6a1b27b2a1e650230da6bcca9a0f55f7d81b3308c3f307f", + "installation": { + "entrypoint": "node_modules/@deepseek-ai/dsh/lib/bin.js", + "metadata": "posix-mode", + "node_sha256": "fde6a4bf8d0562f7751d1a2d6cb9b417c4cfe107bbcb0aa3e9a24e125e348f48", + "node_version": "v22.23.3", + "scope": "owned-prefix-and-node/v1", + "strategy": "absolute-node-js/v1", + "tree_profile": "owned-installation/v1", + "tree_sha256": "09fe25af4c5ec36d5fb67da730b3662e4a0c502eb38ea15fb4b047997b13ba35" + }, "platform": "ubuntu", "provenance": "pinned_real_cli_capture", "request": { @@ -652,9 +729,19 @@ }, { "agent": "deepseek", - "capture_sha256": "5b26b5c0c269b5fd0392a298d3e238da8ea197f382698c5e3df71ab81101f4be", + "capture_sha256": "9c2d987b5a4cbdfd704907bcb67923099bc3da40c1638510da4d7fba563da9a4", "dialect": "openai_chat", "executable_sha256": "5fb91401a66fc3873a920474696eaf03044158acfa13bc149a5b08d7441f493d", + "installation": { + "entrypoint": "node_modules/@deepseek-ai/dsh/lib/bin.js", + "metadata": "windows-readonly", + "node_sha256": "9c9245166b4a8e182e0b797da9c20136117ff24368eaff1fec8343a123c8db0e", + "node_version": "v22.23.3", + "scope": "owned-prefix-and-node/v1", + "strategy": "absolute-node-js/v1", + "tree_profile": "owned-installation/v1", + "tree_sha256": "5f44719f42255b472aa025d048c639157a96b90d8206f51fe7304e0b8e48c789" + }, "platform": "windows", "provenance": "pinned_real_cli_capture", "request": { @@ -714,9 +801,12 @@ }, { "agent": "devin", - "capture_sha256": "b162effcbc4be932b051e18ae73139dbaa9d4611e7cb1bdf795fd79a1ce24fed", + "capture_sha256": "20cb48009cffb16f0c37a6c73fdba0ee4bf1a038e38d8db21265eb2802f3d238", "dialect": "devin_connect_proto", "executable_sha256": "7ef3859e68d4eabc0115e51898fcd4eab1edde753c27a472349ef551180b38ff", + "installation": { + "scope": "entrypoint/v1" + }, "platform": "macos", "provenance": "pinned_real_cli_capture", "request": { @@ -743,9 +833,12 @@ }, { "agent": "devin", - "capture_sha256": "a87dc314284a5502d80d34de2b532667198652a256bcf85e22211e4bd502bf8f", + "capture_sha256": "4436b85b9122e967a08ac2d956a41dfe0244f581ec3f27c285ee91f33cf7b0c6", "dialect": "devin_connect_proto", "executable_sha256": "5a8649d9c4e6dcb7a0b223b6043a0af9256dc0d44d7c980b7d47caec275443ab", + "installation": { + "scope": "entrypoint/v1" + }, "platform": "ubuntu", "provenance": "pinned_real_cli_capture", "request": { @@ -772,9 +865,12 @@ }, { "agent": "devin", - "capture_sha256": "8b1dbc6de5b80be42494d1b0066082b7e6c99bea96043bae9960409c0dc776d2", + "capture_sha256": "03837ff7295608b1f14dd6011ca3fc49a0d7204f9bc83748b6374b5e184d9537", "dialect": "devin_connect_proto", "executable_sha256": "bd2704d32669887a15164725d043e2d3aa18c529920ebe341fb6b70eabac2d30", + "installation": { + "scope": "entrypoint/v1" + }, "platform": "windows", "provenance": "pinned_real_cli_capture", "request": { @@ -801,9 +897,12 @@ }, { "agent": "gemini", - "capture_sha256": "0b9396676f937c13f30aa871d2874b3f438e1c91b801dcd16264529de39d4762", + "capture_sha256": "1d30d5dbb4ab3a6f2c23605492e4d7912960eaf7d1225748b61eba669625588e", "dialect": "gemini_generate_content", "executable_sha256": "5aee9ecb65b0b821e36a12f6e9e837301636de1a849a5a8bcc9c2b2620bbd72e", + "installation": { + "scope": "entrypoint/v1" + }, "platform": "macos", "provenance": "pinned_real_cli_capture", "request": { @@ -851,9 +950,12 @@ }, { "agent": "gemini", - "capture_sha256": "6cd688f58722a757df69a82543f4c988a5930479012e63baf936a887fa93419d", + "capture_sha256": "ba385c092e030a6efddab5d3a07abce7d84c23995b282e3c691d4f2d14868e06", "dialect": "gemini_generate_content", "executable_sha256": "5aee9ecb65b0b821e36a12f6e9e837301636de1a849a5a8bcc9c2b2620bbd72e", + "installation": { + "scope": "entrypoint/v1" + }, "platform": "ubuntu", "provenance": "pinned_real_cli_capture", "request": { @@ -901,9 +1003,12 @@ }, { "agent": "gemini", - "capture_sha256": "1f6d00cc96e00f3cb972f8e1089b13935ffd149e6b8aa67911848068f77d668b", + "capture_sha256": "fae8bc7c58bb6d0bc7b63e6861104efa96e82375726259134ec7a46e11f39e0d", "dialect": "gemini_generate_content", "executable_sha256": "ed3d5b269acefc5dffb342df0a214b2351794244c520c124d3556bc5ea6064c5", + "installation": { + "scope": "entrypoint/v1" + }, "platform": "windows", "provenance": "pinned_real_cli_capture", "request": { @@ -951,9 +1056,12 @@ }, { "agent": "kimi", - "capture_sha256": "4ca011a5faccac9d135d75220d2738d19a9df347a0b975546c560afff1e0915b", + "capture_sha256": "7e8e7a839318423e8817ac5238c794de45da9dc6391c3aa034e4d073900ac50d", "dialect": "openai_chat", "executable_sha256": "4bab6f96c2c289368b7f05a04f66737c53a59ce740f932d8f149240584758efb", + "installation": { + "scope": "entrypoint/v1" + }, "platform": "macos", "provenance": "pinned_real_cli_capture", "request": { @@ -1014,9 +1122,12 @@ }, { "agent": "kimi", - "capture_sha256": "a3c2d0a3c96e99de4f74ad40970e3fb082250efab11cbaeb80b8ecca357c7de4", + "capture_sha256": "635636fc781e5e3834179e102386496cb15c1f995a297077504e13a57a14f73e", "dialect": "openai_chat", "executable_sha256": "66f47536e40b02bb1d577cdd324768f73d39b8b9472e0ca140e73d8e225cd7de", + "installation": { + "scope": "entrypoint/v1" + }, "platform": "ubuntu", "provenance": "pinned_real_cli_capture", "request": { @@ -1077,9 +1188,12 @@ }, { "agent": "kimi", - "capture_sha256": "08c897a74d0946a64a448578de24169eb65736289fd1eb7222274de539c14c24", + "capture_sha256": "bbf937c664af66ff6badeb1650593fe60ecb7ed92d0601837f124fd95d596616", "dialect": "openai_chat", "executable_sha256": "28096756410623f0413607b10872dc16fe8c037c87b005821030daba569a32d4", + "installation": { + "scope": "entrypoint/v1" + }, "platform": "windows", "provenance": "pinned_real_cli_capture", "request": { @@ -1140,9 +1254,12 @@ }, { "agent": "opencode", - "capture_sha256": "0197c1c312d6532e4a8d05dce595bc6a86e3a393ba74b3f8c6a0a1459a1aaf5c", + "capture_sha256": "40c1b102d236d40cfb9f4002c58289e23540caed58ebda405bacf22f316dcf76", "dialect": "openai_responses", "executable_sha256": "7fa5c8659e57cae76b1da4f45edbc583aef93aae917246a0b3abb1ac1b3fb93c", + "installation": { + "scope": "entrypoint/v1" + }, "platform": "macos", "provenance": "pinned_real_cli_capture", "request": { @@ -1191,9 +1308,12 @@ }, { "agent": "opencode", - "capture_sha256": "f9a06f9015188a22a73e7d1717185489dca98151e57448533e53ad05e03810bb", + "capture_sha256": "97ae1278b1c7aaeb080cf4a1ba29e7be3329bcef9d74a69edb8cb401f67d1ac2", "dialect": "openai_responses", "executable_sha256": "e26aa101cfdc40f0e680baa609ff18f131bbc6b91c60fdc11b2b2e2cba7281a4", + "installation": { + "scope": "entrypoint/v1" + }, "platform": "ubuntu", "provenance": "pinned_real_cli_capture", "request": { @@ -1242,9 +1362,12 @@ }, { "agent": "opencode", - "capture_sha256": "31ce073b2034222781b9a4519bae2ffd62b72ffe167aa70bc136aae29d55cfe6", + "capture_sha256": "525d48117f8840f28ecfc871960dad8b4cd8b4302d3027a9f9a1dfb69a338c1c", "dialect": "openai_responses", "executable_sha256": "8f315b35eb8a1fb363665166126a8770bbd19312079b2cde976f5c308b68145b", + "installation": { + "scope": "entrypoint/v1" + }, "platform": "windows", "provenance": "pinned_real_cli_capture", "request": { @@ -1293,9 +1416,12 @@ }, { "agent": "pi", - "capture_sha256": "f90ded0ea820aaf27a2e025956367f2d4f1c1271a4c8c0797f3ad0975e70f55e", + "capture_sha256": "3d50939aa99a10227ba51830a73526ecee0c1b3a3b17e5d1b96279b2d8f5cfc4", "dialect": "openai_chat", "executable_sha256": "e79626f2dd6f94aa45d30f3fa63cd84319a6eefcd150b353cfaf274366926774", + "installation": { + "scope": "entrypoint/v1" + }, "platform": "macos", "provenance": "pinned_real_cli_capture", "request": { @@ -1335,9 +1461,12 @@ }, { "agent": "pi", - "capture_sha256": "e0316503ed761604e429919e7593590bc1b2f329b13b0c604864ae2a88a3ec87", + "capture_sha256": "0208904ba375b4f1402e643bb7bc1610933f8773a51c9b499f275c4b26ef9933", "dialect": "openai_chat", "executable_sha256": "e79626f2dd6f94aa45d30f3fa63cd84319a6eefcd150b353cfaf274366926774", + "installation": { + "scope": "entrypoint/v1" + }, "platform": "ubuntu", "provenance": "pinned_real_cli_capture", "request": { @@ -1377,9 +1506,12 @@ }, { "agent": "pi", - "capture_sha256": "453d9e32581184d83cb26ba6b529db15f08662fd17fea6b1cc75f8b49a34c7e4", + "capture_sha256": "e3764c13ab1be4a05d45e0c79b133e6009a3c0141cefd8b757ced474c74e5cd4", "dialect": "openai_chat", "executable_sha256": "35ec1b2b20461eadbbaaf10e209e0ffe9a0f8cc2f948ae4670023ea85caff296", + "installation": { + "scope": "entrypoint/v1" + }, "platform": "windows", "provenance": "pinned_real_cli_capture", "request": { @@ -1419,9 +1551,19 @@ }, { "agent": "qwen", - "capture_sha256": "bf800127313bc7c5e43bb483ee6d8cfee8ca53c2057a150ac5e1c939bb7d1336", + "capture_sha256": "49dce0e5ac25dff70a306bcae73e396a7ca735cb8473594f53ac2d2b04ad743e", "dialect": "openai_chat", "executable_sha256": "06c2be3fcb1b451931d3e100acc7edd3ff748c74189f6fcc70300b5cd758f8e6", + "installation": { + "entrypoint": "node_modules/@qwen-code/qwen-code/cli-entry.js", + "metadata": "posix-mode", + "node_sha256": "18e387c90ab8a8400183e8bdd396376e1e875b91b4c874b894dcade7b35bf572", + "node_version": "v22.23.2", + "scope": "owned-prefix-and-node/v1", + "strategy": "absolute-node-js/v1", + "tree_profile": "owned-installation/v1", + "tree_sha256": "2e2bba01ba1d27f41130580c54808727530e7d33b7cb690e27da321ba57576e0" + }, "platform": "macos", "provenance": "pinned_real_cli_capture", "request": { @@ -1466,9 +1608,19 @@ }, { "agent": "qwen", - "capture_sha256": "0a9133db580f0861d1fe583fbb0fd81196581af4d4141ab793015a1e8ac516de", + "capture_sha256": "5de080822b9a3934105b2b16d6ec3213e528a22f948fefbc07d8fb317b4797cc", "dialect": "openai_chat", "executable_sha256": "06c2be3fcb1b451931d3e100acc7edd3ff748c74189f6fcc70300b5cd758f8e6", + "installation": { + "entrypoint": "node_modules/@qwen-code/qwen-code/cli-entry.js", + "metadata": "posix-mode", + "node_sha256": "fde6a4bf8d0562f7751d1a2d6cb9b417c4cfe107bbcb0aa3e9a24e125e348f48", + "node_version": "v22.23.3", + "scope": "owned-prefix-and-node/v1", + "strategy": "absolute-node-js/v1", + "tree_profile": "owned-installation/v1", + "tree_sha256": "2827221e31866712326ccbc915ae8931c947332a73525212b8c1e103221d39f4" + }, "platform": "ubuntu", "provenance": "pinned_real_cli_capture", "request": { @@ -1513,9 +1665,19 @@ }, { "agent": "qwen", - "capture_sha256": "71a9c5e9463a5ea1b7050053286f4fa11484c2c30c7b509ddac86c16527a1e13", + "capture_sha256": "0da935bc455440126660cfc9329fd955e92dea24dda67f290e8035875a6187ab", "dialect": "openai_chat", - "executable_sha256": "a5c19d0c03467074c1ff755ec604422223e1019fa19f8a9bf2f81dd84f32e070", + "executable_sha256": "a06c37d20d3cdbb1e42317900db3ee0cfc083eba4403e26f749efbf1ae67aeb4", + "installation": { + "entrypoint": "node_modules/@qwen-code/qwen-code/cli-entry.js", + "metadata": "windows-readonly", + "node_sha256": "9c9245166b4a8e182e0b797da9c20136117ff24368eaff1fec8343a123c8db0e", + "node_version": "v22.23.3", + "scope": "owned-prefix-and-node/v1", + "strategy": "absolute-node-js/v1", + "tree_profile": "owned-installation/v1", + "tree_sha256": "3cd3fca039bf974f0664643e528205c77d809c418bb56d09728046db64ecb931" + }, "platform": "windows", "provenance": "pinned_real_cli_capture", "request": { @@ -1559,536 +1721,536 @@ "version": "0.25.1" } ], - "schema_version": 4, + "schema_version": 5, "source": { "artifacts": [ { "agent": "claude", - "archive_sha256": "99078aa5e0efd9ec2823027516915704056157d8f7aac2faba2656ad4b0ea7cc", - "artifact_id": 11792147264, - "capture_sha256": "6eb5f870d6d9eec0fd8bdfce34693ce86e057feb07df5aa9a4af3f3bd706ea8c", - "check_run_id": 115154313535, + "archive_sha256": "a545cdb4caafa1cea220867eae1ec417eafc1e988b4798748231add32c1068eb", + "artifact_id": 11812742248, + "capture_sha256": "5708547a3442f7789838ecf0a8840e6d71e9f38f0e0406ea8f0f6f22b1898a1a", + "check_run_id": 115292781228, "conclusion": "success", - "job_id": 115154313535, + "job_id": 115292781228, "platform": "macos", "runner": "macos-latest", - "runner_id": 1000035622, + "runner_id": 1000036069, "runner_labels": [ "macos-latest" ], - "runner_name": "GitHub Actions 1000035622" + "runner_name": "GitHub Actions 1000036069" }, { "agent": "claude", - "archive_sha256": "ae2626154f81705dc335f8d245deb189fe346a0e9a8f531eb5a9b33ffa90de6e", - "artifact_id": 11792501139, - "capture_sha256": "21fa65e59854f6623ddc764b14463d23d1bf87527e4a4173501a0dc354a7dce9", - "check_run_id": 115154312953, + "archive_sha256": "cbcc5865db0da8cf2fef8bc2261d17bb33e5030cc27ace2f289ac76f4f47a07f", + "artifact_id": 11811972315, + "capture_sha256": "adcd14006488d4df783429269c2c1a4cfc107045503a5980da0dca6a9422a787", + "check_run_id": 115292780984, "conclusion": "success", - "job_id": 115154312953, + "job_id": 115292780984, "platform": "ubuntu", "runner": "ubuntu-latest", - "runner_id": 1000035597, + "runner_id": 1000036054, "runner_labels": [ "ubuntu-latest" ], - "runner_name": "GitHub Actions 1000035597" + "runner_name": "GitHub Actions 1000036054" }, { "agent": "claude", - "archive_sha256": "77c85d56b9514f00b0c3925d6e8aa3dd83ca23db2c263aab69c36391b07deff4", - "artifact_id": 11791834756, - "capture_sha256": "de00faa86bcc17a3858475a91044c6a28b24a1af11650bb7758044643de2aa1f", - "check_run_id": 115154313210, + "archive_sha256": "86bd9da92917d33eef708bc4a4b692a9c36baf9136044617f707b53762b3f51b", + "artifact_id": 11812577010, + "capture_sha256": "53f68de22f5190e01a339b2960dbb38b2bd649a4004b0e4e5f28cd3d08bf9925", + "check_run_id": 115292780643, "conclusion": "success", - "job_id": 115154313210, + "job_id": 115292780643, "platform": "windows", "runner": "windows-latest", - "runner_id": 1000035606, + "runner_id": 1000036050, "runner_labels": [ "windows-latest" ], - "runner_name": "GitHub Actions 1000035606" + "runner_name": "GitHub Actions 1000036050" }, { "agent": "codex", - "archive_sha256": "ff29d6cf96195d3ce3a9e81061aedef0d66611420ecd390d0d2ecc0e2d8a32ce", - "artifact_id": 11792436724, - "capture_sha256": "5e39c8858cfe781d5febb081b393afd0c385a73c33bf6609ca7d7b5a4749852e", - "check_run_id": 115154313328, + "archive_sha256": "9cde37ea39bd3be6b0bb586dc674d46f51b09102b3be9190e2fdadd45a51e2ed", + "artifact_id": 11812462013, + "capture_sha256": "d55f571d3db096ba3333ce57423f3af12804d62b82df2a3e5606f9f69c4b01b8", + "check_run_id": 115292780725, "conclusion": "success", - "job_id": 115154313328, + "job_id": 115292780725, "platform": "macos", "runner": "macos-latest", - "runner_id": 1000035616, + "runner_id": 1000036046, "runner_labels": [ "macos-latest" ], - "runner_name": "GitHub Actions 1000035616" + "runner_name": "GitHub Actions 1000036046" }, { "agent": "codex", - "archive_sha256": "b49b5d4aeb5b51da8757cbdb2024b8f80880d8660a378bf2968591743045ad6a", - "artifact_id": 11792775343, - "capture_sha256": "a45c25b9a94213356abc092f187117258099981423758f74e27cf6398efea157", - "check_run_id": 115154313073, + "archive_sha256": "c0f869ceb64dbef40dee325d3a6f2ebfc15dfc9a99760a3a89dbf9b23ef8b85e", + "artifact_id": 11811807666, + "capture_sha256": "60977d4a8c9cb5d3f2ee339756837d0262587072583ed1ca7a6148728889289d", + "check_run_id": 115292782173, "conclusion": "success", - "job_id": 115154313073, + "job_id": 115292782173, "platform": "ubuntu", "runner": "ubuntu-latest", - "runner_id": 1000035605, + "runner_id": 1000036063, "runner_labels": [ "ubuntu-latest" ], - "runner_name": "GitHub Actions 1000035605" + "runner_name": "GitHub Actions 1000036063" }, { "agent": "codex", - "archive_sha256": "39b2b66f9c35ad5bb60323fbbec480b24235f4f0db9df2bdd647b16c01e74531", - "artifact_id": 11791957270, - "capture_sha256": "1f90c8f26662b2532208cf6abe8bc263e22cadc0a05fd966dc5cfe13652afb5c", - "check_run_id": 115154313309, + "archive_sha256": "903456eb85469765aba8dd4c361d48013906d15e7381f9e697d6d107def8eda9", + "artifact_id": 11811637996, + "capture_sha256": "4944bae99385b50a7d5821b108398d10dbe07e6324168b22f5a353d42e50f57d", + "check_run_id": 115292782255, "conclusion": "success", - "job_id": 115154313309, + "job_id": 115292782255, "platform": "windows", "runner": "windows-latest", - "runner_id": 1000035602, + "runner_id": 1000036061, "runner_labels": [ "windows-latest" ], - "runner_name": "GitHub Actions 1000035602" + "runner_name": "GitHub Actions 1000036061" }, { "agent": "copilot", - "archive_sha256": "844083dd8187b667360c41e86b3ed4222cbdfbb7bc41209e5458955486a9450b", - "artifact_id": 11792321923, - "capture_sha256": "c922e1edecb25cf7909b1fdb0bf8144a7c05ea841ad2315f5fc7656873571dd8", - "check_run_id": 115154313050, + "archive_sha256": "6d3e9751e68a012b7073cdb02bb704a9540e9f297abb550f9d260aa1bdc6710e", + "artifact_id": 11812547700, + "capture_sha256": "d03816631d68c5cc22e18102e6d06bc01e3a435f583648ef7b3e871bcb31b847", + "check_run_id": 115292781537, "conclusion": "success", - "job_id": 115154313050, + "job_id": 115292781537, "platform": "macos", "runner": "macos-latest", - "runner_id": 1000035618, + "runner_id": 1000036071, "runner_labels": [ "macos-latest" ], - "runner_name": "GitHub Actions 1000035618" + "runner_name": "GitHub Actions 1000036071" }, { "agent": "copilot", - "archive_sha256": "883a7cfc1b2273af5699b0debaf196fc0944be552131cc0bfcb01dcf14e4ca2d", - "artifact_id": 11791794931, - "capture_sha256": "d17ae1033aafd6c8c7eb5ee2cf2cb134a661ecc37944169574d60806038fa103", - "check_run_id": 115154313533, + "archive_sha256": "217c998edb3bc4fb458556bc48074af1a94d54e85b028d74a737b7022f414480", + "artifact_id": 11811412818, + "capture_sha256": "587960da7ac17da700faeae5d9fa3a4dff9ce729fdaffb01d4cd52b9fdd0f9ef", + "check_run_id": 115292782050, "conclusion": "success", - "job_id": 115154313533, + "job_id": 115292782050, "platform": "ubuntu", "runner": "ubuntu-latest", - "runner_id": 1000035617, + "runner_id": 1000036060, "runner_labels": [ "ubuntu-latest" ], - "runner_name": "GitHub Actions 1000035617" + "runner_name": "GitHub Actions 1000036060" }, { "agent": "copilot", - "archive_sha256": "880b9b7cfbb01f4dedf61156012f70924ddb6d16e5dffb7906162d5af28026a4", - "artifact_id": 11791904023, - "capture_sha256": "01594240b7ce34526bf7ccbc15bcf9f2ba4b2fff552d6e949c501720df7a0868", - "check_run_id": 115154313227, + "archive_sha256": "bdd7495ddbfbe6d722806a2a5e35f03197fe2bd46851c7405e465a0f0f84b416", + "artifact_id": 11811537947, + "capture_sha256": "e7a4c28bad0899a9774f4bcdc264c8d453fd80afe6a03304a490d0bd457aa086", + "check_run_id": 115292782294, "conclusion": "success", - "job_id": 115154313227, + "job_id": 115292782294, "platform": "windows", "runner": "windows-latest", - "runner_id": 1000035607, + "runner_id": 1000036064, "runner_labels": [ "windows-latest" ], - "runner_name": "GitHub Actions 1000035607" + "runner_name": "GitHub Actions 1000036064" }, { "agent": "cursor", - "archive_sha256": "61a28e8b4cafe56dca6d91410c1b6053623b661aab4bebd17c0de3d23cb234ff", - "artifact_id": 11792925010, - "capture_sha256": "45b8810fda9834e0a492a9926dffb9be57ee1338d513286ef17d3abe005751b2", - "check_run_id": 115154313127, + "archive_sha256": "b476ba1834de160af338f4d36fbb398c78e8678cd7c8eb7118345745c49b585a", + "artifact_id": 11813015097, + "capture_sha256": "f6005b969def3bb81ec2aabcedae6f32e662aee91bb60b68a08bb72efb5fc49b", + "check_run_id": 115292782464, "conclusion": "success", - "job_id": 115154313127, + "job_id": 115292782464, "platform": "macos", "runner": "macos-latest", - "runner_id": 1000035604, + "runner_id": 1000036074, "runner_labels": [ "macos-latest" ], - "runner_name": "GitHub Actions 1000035604" + "runner_name": "GitHub Actions 1000036074" }, { "agent": "cursor", - "archive_sha256": "0a54f7495df21747c13569e401ad85ef4ea7bd3161d5182bfa15855879051ccb", - "artifact_id": 11792097041, - "capture_sha256": "e10c16ce205909adef763903de2d0ac84eabb2e1b496347992ad3c3360213908", - "check_run_id": 115154313343, + "archive_sha256": "304ca1991479174501ca9164122d069b69e557d6c6befbc9593fda990846f478", + "artifact_id": 11811572775, + "capture_sha256": "286e0e249de8e190f8b45153b5c28eb5eedfcc3104b05b751370063dad5df502", + "check_run_id": 115292780654, "conclusion": "success", - "job_id": 115154313343, + "job_id": 115292780654, "platform": "ubuntu", "runner": "ubuntu-latest", - "runner_id": 1000035615, + "runner_id": 1000036043, "runner_labels": [ "ubuntu-latest" ], - "runner_name": "GitHub Actions 1000035615" + "runner_name": "GitHub Actions 1000036043" }, { "agent": "cursor", - "archive_sha256": "75f353f07826a8d810a91998b50283a3ba49f3e94c20f76c4b12d7cfdb3c2d38", - "artifact_id": 11792655871, - "capture_sha256": "b8af04ff98e36d1f956dd00742a51aed5325e2fef51e867edca544ae8da00555", - "check_run_id": 115154313344, + "archive_sha256": "c9e3cfc8e027962001c0979e3569a8fbab79e6adee6fa0a83a5b37ce30d12144", + "artifact_id": 11811907724, + "capture_sha256": "efeaa573c8a48652a6a8a4ee3e664075565712cf131cd1fc58a30e46f2552027", + "check_run_id": 115292782224, "conclusion": "success", - "job_id": 115154313344, + "job_id": 115292782224, "platform": "windows", "runner": "windows-latest", - "runner_id": 1000035611, + "runner_id": 1000036059, "runner_labels": [ "windows-latest" ], - "runner_name": "GitHub Actions 1000035611" + "runner_name": "GitHub Actions 1000036059" }, { "agent": "deepseek", - "archive_sha256": "8e5e5161dd3da7460de1f156b8de66c9aa087a400feaf1b39131008583e47753", - "artifact_id": 11792072030, - "capture_sha256": "9f36db049a9f7ac3a58356a9aa17e1bc3727e1a41badd1e2e3b3af30491f672c", - "check_run_id": 115154312914, + "archive_sha256": "c7a5d1f5076a7b9469168fb2ba6c5c3b86d77361aa6fac4ba3251ce9f5ffe647", + "artifact_id": 11812047735, + "capture_sha256": "672d94b5bc6d05fc7009b6e69bd7ba552dae90ea718c5dc4f157baa704b5b32a", + "check_run_id": 115292780892, "conclusion": "success", - "job_id": 115154312914, + "job_id": 115292780892, "platform": "macos", "runner": "macos-latest", - "runner_id": 1000035591, + "runner_id": 1000036048, "runner_labels": [ "macos-latest" ], - "runner_name": "GitHub Actions 1000035591" + "runner_name": "GitHub Actions 1000036048" }, { "agent": "deepseek", - "archive_sha256": "b8038ce4ec09814b84f9d35de92209e753abe211c57d4386174eda163d5b48a1", - "artifact_id": 11791724994, - "capture_sha256": "5c08fb7a1644f87521c9321c99b6deca046ba0c46993d7d747f51215c587f4b2", - "check_run_id": 115154312928, + "archive_sha256": "86c56ddc0b5e52cf3992f76cddc59cc6965d53c2d030b2bbff039df5ac4dde0f", + "artifact_id": 11812237167, + "capture_sha256": "146c456ea93ec95cb4ca9e2e291a318660bf7d47114b5f018e78eff796fad9cd", + "check_run_id": 115292781030, "conclusion": "success", - "job_id": 115154312928, + "job_id": 115292781030, "platform": "ubuntu", "runner": "ubuntu-latest", - "runner_id": 1000035594, + "runner_id": 1000036053, "runner_labels": [ "ubuntu-latest" ], - "runner_name": "GitHub Actions 1000035594" + "runner_name": "GitHub Actions 1000036053" }, { "agent": "deepseek", - "archive_sha256": "fd3ff0cdcef9f567686f00dfdeb2428201bddd2069cba3fa1cd1e0e6f700362c", - "artifact_id": 11792396723, - "capture_sha256": "5b26b5c0c269b5fd0392a298d3e238da8ea197f382698c5e3df71ab81101f4be", - "check_run_id": 115154313313, + "archive_sha256": "043463071f8562ad0759b4aeca1b0eebdaf990bbfa8fff4f1cba36e352e5ed34", + "artifact_id": 11813016365, + "capture_sha256": "9c2d987b5a4cbdfd704907bcb67923099bc3da40c1638510da4d7fba563da9a4", + "check_run_id": 115292781265, "conclusion": "success", - "job_id": 115154313313, + "job_id": 115292781265, "platform": "windows", "runner": "windows-latest", - "runner_id": 1000035608, + "runner_id": 1000036055, "runner_labels": [ "windows-latest" ], - "runner_name": "GitHub Actions 1000035608" + "runner_name": "GitHub Actions 1000036055" }, { "agent": "devin", - "archive_sha256": "d4ad1a6c5e29047d8d2f752dee7b66b2b0ef19cfedcd0a659d24218bd6c1bf46", - "artifact_id": 11792770665, - "capture_sha256": "b162effcbc4be932b051e18ae73139dbaa9d4611e7cb1bdf795fd79a1ce24fed", - "check_run_id": 115154312880, + "archive_sha256": "71828fc15be82f16d5450444190524e8ff61fbb7360a4b9d0d5e99ad83198961", + "artifact_id": 11812412169, + "capture_sha256": "20cb48009cffb16f0c37a6c73fdba0ee4bf1a038e38d8db21265eb2802f3d238", + "check_run_id": 115292780794, "conclusion": "success", - "job_id": 115154312880, + "job_id": 115292780794, "platform": "macos", "runner": "macos-latest", - "runner_id": 1000035590, + "runner_id": 1000036047, "runner_labels": [ "macos-latest" ], - "runner_name": "GitHub Actions 1000035590" + "runner_name": "GitHub Actions 1000036047" }, { "agent": "devin", - "archive_sha256": "595213ce04c8c92573e950cbbfa677021160c5994da48ff3f4b537ecb8639421", - "artifact_id": 11792491351, - "capture_sha256": "a87dc314284a5502d80d34de2b532667198652a256bcf85e22211e4bd502bf8f", - "check_run_id": 115154313384, + "archive_sha256": "06ce910359d5941c6d1a0f95829aa2927f80c6e946f02d2344574ada9c54ad86", + "artifact_id": 11811372914, + "capture_sha256": "4436b85b9122e967a08ac2d956a41dfe0244f581ec3f27c285ee91f33cf7b0c6", + "check_run_id": 115292780787, "conclusion": "success", - "job_id": 115154313384, + "job_id": 115292780787, "platform": "ubuntu", "runner": "ubuntu-latest", - "runner_id": 1000035613, + "runner_id": 1000036045, "runner_labels": [ "ubuntu-latest" ], - "runner_name": "GitHub Actions 1000035613" + "runner_name": "GitHub Actions 1000036045" }, { "agent": "devin", - "archive_sha256": "9228ba6c849aa317e7c8c6a9dbd87d1c9086de99829277e2064005754af7a9ab", - "artifact_id": 11792231818, - "capture_sha256": "8b1dbc6de5b80be42494d1b0066082b7e6c99bea96043bae9960409c0dc776d2", - "check_run_id": 115154313361, + "archive_sha256": "477f05b160fc862088b63b1ac00090fc53788ea4b1eaf5920ae4cf2828224284", + "artifact_id": 11812292068, + "capture_sha256": "03837ff7295608b1f14dd6011ca3fc49a0d7204f9bc83748b6374b5e184d9537", + "check_run_id": 115292781515, "conclusion": "success", - "job_id": 115154313361, + "job_id": 115292781515, "platform": "windows", "runner": "windows-latest", - "runner_id": 1000035610, + "runner_id": 1000036056, "runner_labels": [ "windows-latest" ], - "runner_name": "GitHub Actions 1000035610" + "runner_name": "GitHub Actions 1000036056" }, { "agent": "gemini", - "archive_sha256": "c241f4201c6dcadd407a9ca43a855c9b831e2852b467b56ce83d7e1cdf88287f", - "artifact_id": 11792212075, - "capture_sha256": "0b9396676f937c13f30aa871d2874b3f438e1c91b801dcd16264529de39d4762", - "check_run_id": 115154312987, + "archive_sha256": "7345ec1bcca756acc591f9b90d966d7c535654b7c08e48c5b027352a742f62e9", + "artifact_id": 11812447369, + "capture_sha256": "1d30d5dbb4ab3a6f2c23605492e4d7912960eaf7d1225748b61eba669625588e", + "check_run_id": 115292781493, "conclusion": "success", - "job_id": 115154312987, + "job_id": 115292781493, "platform": "macos", "runner": "macos-latest", - "runner_id": 1000035619, + "runner_id": 1000036062, "runner_labels": [ "macos-latest" ], - "runner_name": "GitHub Actions 1000035619" + "runner_name": "GitHub Actions 1000036062" }, { "agent": "gemini", - "archive_sha256": "d39255bd2e6224cd5790bf98a88443dae471b6e4881019b1798d999849912cfa", - "artifact_id": 11792436433, - "capture_sha256": "6cd688f58722a757df69a82543f4c988a5930479012e63baf936a887fa93419d", - "check_run_id": 115154313006, + "archive_sha256": "ff8d8dbcf5dc3a8a000b3c5ccaa273d76a33fbfd6874fa14d4785f776e6aa439", + "artifact_id": 11811717700, + "capture_sha256": "ba385c092e030a6efddab5d3a07abce7d84c23995b282e3c691d4f2d14868e06", + "check_run_id": 115292780430, "conclusion": "success", - "job_id": 115154313006, + "job_id": 115292780430, "platform": "ubuntu", "runner": "ubuntu-latest", - "runner_id": 1000035595, + "runner_id": 1000036042, "runner_labels": [ "ubuntu-latest" ], - "runner_name": "GitHub Actions 1000035595" + "runner_name": "GitHub Actions 1000036042" }, { "agent": "gemini", - "archive_sha256": "390da5c4cf1058eebc4cba05efc0c2ce3f1f63cbaa1361863a99b5edc76ce92e", - "artifact_id": 11792171887, - "capture_sha256": "1f6d00cc96e00f3cb972f8e1089b13935ffd149e6b8aa67911848068f77d668b", - "check_run_id": 115154312995, + "archive_sha256": "b8274c75fcd744a14eac9a7750313d4f5e6a151986f7948b999c26ab8a6c45b1", + "artifact_id": 11812022776, + "capture_sha256": "fae8bc7c58bb6d0bc7b63e6861104efa96e82375726259134ec7a46e11f39e0d", + "check_run_id": 115292781975, "conclusion": "success", - "job_id": 115154312995, + "job_id": 115292781975, "platform": "windows", "runner": "windows-latest", - "runner_id": 1000035600, + "runner_id": 1000036058, "runner_labels": [ "windows-latest" ], - "runner_name": "GitHub Actions 1000035600" + "runner_name": "GitHub Actions 1000036058" }, { "agent": "kimi", - "archive_sha256": "ecd7ce0e51010e6eaac01e6e27b9abcef5c29b5582b91bcec2ed806446282c69", - "artifact_id": 11791887979, - "capture_sha256": "4ca011a5faccac9d135d75220d2738d19a9df347a0b975546c560afff1e0915b", - "check_run_id": 115154312990, + "archive_sha256": "07a76d2393663a92fbb449e8e1944e93375534a68092cc863d296ea667cacf8e", + "artifact_id": 11813295072, + "capture_sha256": "7e8e7a839318423e8817ac5238c794de45da9dc6391c3aa034e4d073900ac50d", + "check_run_id": 115292782208, "conclusion": "success", - "job_id": 115154312990, + "job_id": 115292782208, "platform": "macos", "runner": "macos-latest", - "runner_id": 1000035609, + "runner_id": 1000036073, "runner_labels": [ "macos-latest" ], - "runner_name": "GitHub Actions 1000035609" + "runner_name": "GitHub Actions 1000036073" }, { "agent": "kimi", - "archive_sha256": "35d3a1057d4ba2c17cce2f7a72dfb77e724461ebc96d0cac5e9646cd0ddf561d", - "artifact_id": 11792111827, - "capture_sha256": "a3c2d0a3c96e99de4f74ad40970e3fb082250efab11cbaeb80b8ecca357c7de4", - "check_run_id": 115154313296, + "archive_sha256": "a38c02ede97296e600affe53ccd268dc739921863ab0e989f2a9a40526c3b813", + "artifact_id": 11812307014, + "capture_sha256": "635636fc781e5e3834179e102386496cb15c1f995a297077504e13a57a14f73e", + "check_run_id": 115292780876, "conclusion": "success", - "job_id": 115154313296, + "job_id": 115292780876, "platform": "ubuntu", "runner": "ubuntu-latest", - "runner_id": 1000035599, + "runner_id": 1000036044, "runner_labels": [ "ubuntu-latest" ], - "runner_name": "GitHub Actions 1000035599" + "runner_name": "GitHub Actions 1000036044" }, { "agent": "kimi", - "archive_sha256": "02f1fa5914b5400495a7dc62594659978b27723941f78d2c1d89e26d248a87ea", - "artifact_id": 11792476422, - "capture_sha256": "08c897a74d0946a64a448578de24169eb65736289fd1eb7222274de539c14c24", - "check_run_id": 115154313179, + "archive_sha256": "b78736584918c2bd3c42c07aca7632cc9d707b5e31c4ded98d327cf56ddac9fa", + "artifact_id": 11812950067, + "capture_sha256": "bbf937c664af66ff6badeb1650593fe60ecb7ed92d0601837f124fd95d596616", + "check_run_id": 115292782416, "conclusion": "success", - "job_id": 115154313179, + "job_id": 115292782416, "platform": "windows", "runner": "windows-latest", - "runner_id": 1000035601, + "runner_id": 1000036067, "runner_labels": [ "windows-latest" ], - "runner_name": "GitHub Actions 1000035601" + "runner_name": "GitHub Actions 1000036067" }, { "agent": "opencode", - "archive_sha256": "fcc4ee1b4d5444d4b1596d17224bbeedc73d482cb15ace19af53cc32c2bbf07f", - "artifact_id": 11792945193, - "capture_sha256": "0197c1c312d6532e4a8d05dce595bc6a86e3a393ba74b3f8c6a0a1459a1aaf5c", - "check_run_id": 115154313149, + "archive_sha256": "b281f4251275363d75ae7148d8b6752289a7fa88f91fe2783111bd5a0965d38a", + "artifact_id": 11812820104, + "capture_sha256": "40c1b102d236d40cfb9f4002c58289e23540caed58ebda405bacf22f316dcf76", + "check_run_id": 115292781569, "conclusion": "success", - "job_id": 115154313149, + "job_id": 115292781569, "platform": "macos", "runner": "macos-latest", - "runner_id": 1000035596, + "runner_id": 1000036070, "runner_labels": [ "macos-latest" ], - "runner_name": "GitHub Actions 1000035596" + "runner_name": "GitHub Actions 1000036070" }, { "agent": "opencode", - "archive_sha256": "6bca3009d2975eeb175cf865e252a2840e0a8a0a491c668a6b68446950bd0497", - "artifact_id": 11792256627, - "capture_sha256": "f9a06f9015188a22a73e7d1717185489dca98151e57448533e53ad05e03810bb", - "check_run_id": 115154313103, + "archive_sha256": "b2c1d7aa1978f753bfee75ecba23708b561efd15120f64ea747f3587e3d51ebe", + "artifact_id": 11812471915, + "capture_sha256": "97ae1278b1c7aaeb080cf4a1ba29e7be3329bcef9d74a69edb8cb401f67d1ac2", + "check_run_id": 115292781309, "conclusion": "success", - "job_id": 115154313103, + "job_id": 115292781309, "platform": "ubuntu", "runner": "ubuntu-latest", - "runner_id": 1000035598, + "runner_id": 1000036051, "runner_labels": [ "ubuntu-latest" ], - "runner_name": "GitHub Actions 1000035598" + "runner_name": "GitHub Actions 1000036051" }, { "agent": "opencode", - "archive_sha256": "8071e6b46bc74f36a9377b8a0ce97f4125a7aab9440818b5e3706cfc87795d01", - "artifact_id": 11792416724, - "capture_sha256": "31ce073b2034222781b9a4519bae2ffd62b72ffe167aa70bc136aae29d55cfe6", - "check_run_id": 115154313014, + "archive_sha256": "b5756f67832d5d9a7174f6b2fd218e9a7be63e8a5fa7d0e0ca913f64354d6c5d", + "artifact_id": 11812682149, + "capture_sha256": "525d48117f8840f28ecfc871960dad8b4cd8b4302d3027a9f9a1dfb69a338c1c", + "check_run_id": 115292782313, "conclusion": "success", - "job_id": 115154313014, + "job_id": 115292782313, "platform": "windows", "runner": "windows-latest", - "runner_id": 1000035593, + "runner_id": 1000036065, "runner_labels": [ "windows-latest" ], - "runner_name": "GitHub Actions 1000035593" + "runner_name": "GitHub Actions 1000036065" }, { "agent": "pi", - "archive_sha256": "d527908d906d82e67afccff8b6ce104863c02fef534a63cab5d53fcd13e79177", - "artifact_id": 11792496890, - "capture_sha256": "f90ded0ea820aaf27a2e025956367f2d4f1c1271a4c8c0797f3ad0975e70f55e", - "check_run_id": 115154313406, + "archive_sha256": "cfbf846c535fe38dcdf380834adc378982da947fc53aa3d8c98e67fe246d7137", + "artifact_id": 11811898251, + "capture_sha256": "3d50939aa99a10227ba51830a73526ecee0c1b3a3b17e5d1b96279b2d8f5cfc4", + "check_run_id": 115292781996, "conclusion": "success", - "job_id": 115154313406, + "job_id": 115292781996, "platform": "macos", "runner": "macos-latest", - "runner_id": 1000035621, + "runner_id": 1000036072, "runner_labels": [ "macos-latest" ], - "runner_name": "GitHub Actions 1000035621" + "runner_name": "GitHub Actions 1000036072" }, { "agent": "pi", - "archive_sha256": "af07bc6616a6f1350f97f416ddcec45690ed7582d03881c6163786dc1b5487a3", - "artifact_id": 11792416584, - "capture_sha256": "e0316503ed761604e429919e7593590bc1b2f329b13b0c604864ae2a88a3ec87", - "check_run_id": 115154312885, + "archive_sha256": "c522310f0175c50af336c632a266efec626acc009889753bf387551583b1c47f", + "artifact_id": 11812247007, + "capture_sha256": "0208904ba375b4f1402e643bb7bc1610933f8773a51c9b499f275c4b26ef9933", + "check_run_id": 115292781412, "conclusion": "success", - "job_id": 115154312885, + "job_id": 115292781412, "platform": "ubuntu", "runner": "ubuntu-latest", - "runner_id": 1000035592, + "runner_id": 1000036052, "runner_labels": [ "ubuntu-latest" ], - "runner_name": "GitHub Actions 1000035592" + "runner_name": "GitHub Actions 1000036052" }, { "agent": "pi", - "archive_sha256": "81a04b9fe18071ac01443fe99355e06f1fcffd3a91988547b1d088be28aa0ef1", - "artifact_id": 11791942579, - "capture_sha256": "453d9e32581184d83cb26ba6b529db15f08662fd17fea6b1cc75f8b49a34c7e4", - "check_run_id": 115154313261, + "archive_sha256": "3c415e11009d2b551eda82c5d2db42629066b318690dacf431573f9682f757a4", + "artifact_id": 11812307572, + "capture_sha256": "e3764c13ab1be4a05d45e0c79b133e6009a3c0141cefd8b757ced474c74e5cd4", + "check_run_id": 115292782532, "conclusion": "success", - "job_id": 115154313261, + "job_id": 115292782532, "platform": "windows", "runner": "windows-latest", - "runner_id": 1000035603, + "runner_id": 1000036066, "runner_labels": [ "windows-latest" ], - "runner_name": "GitHub Actions 1000035603" + "runner_name": "GitHub Actions 1000036066" }, { "agent": "qwen", - "archive_sha256": "4fff40c9f1e0b771abaa9044476d9c659d0babc1d5665431544d200bb22de364", - "artifact_id": 11792840787, - "capture_sha256": "bf800127313bc7c5e43bb483ee6d8cfee8ca53c2057a150ac5e1c939bb7d1336", - "check_run_id": 115154313379, + "archive_sha256": "d8de8fbe5608c1db7393e156ceb9fc3327ac52741d1fce3b42437be894911b8a", + "artifact_id": 11811977723, + "capture_sha256": "49dce0e5ac25dff70a306bcae73e396a7ca735cb8473594f53ac2d2b04ad743e", + "check_run_id": 115292780925, "conclusion": "success", - "job_id": 115154313379, + "job_id": 115292780925, "platform": "macos", "runner": "macos-latest", - "runner_id": 1000035620, + "runner_id": 1000036049, "runner_labels": [ "macos-latest" ], - "runner_name": "GitHub Actions 1000035620" + "runner_name": "GitHub Actions 1000036049" }, { "agent": "qwen", - "archive_sha256": "ddb49477c3281c85a428cff75b8280f1d2cba6e125654083ce4ae5b7bfe27004", - "artifact_id": 11791879347, - "capture_sha256": "0a9133db580f0861d1fe583fbb0fd81196581af4d4141ab793015a1e8ac516de", - "check_run_id": 115154313331, + "archive_sha256": "e468eec9ddb7dfefdf90ffd17e2702094e8870bf32d1a5f0d9e928f35ac00bd6", + "artifact_id": 11812786020, + "capture_sha256": "5de080822b9a3934105b2b16d6ec3213e528a22f948fefbc07d8fb317b4797cc", + "check_run_id": 115292782686, "conclusion": "success", - "job_id": 115154313331, + "job_id": 115292782686, "platform": "ubuntu", "runner": "ubuntu-latest", - "runner_id": 1000035612, + "runner_id": 1000036068, "runner_labels": [ "ubuntu-latest" ], - "runner_name": "GitHub Actions 1000035612" + "runner_name": "GitHub Actions 1000036068" }, { "agent": "qwen", - "archive_sha256": "10a6974c80cdde83f6e472e784de8772a5e9ad426c87a9bc3b73533559d95d7a", - "artifact_id": 11792481536, - "capture_sha256": "71a9c5e9463a5ea1b7050053286f4fa11484c2c30c7b509ddac86c16527a1e13", - "check_run_id": 115154313567, + "archive_sha256": "f5856b2a343b9e0217970af0ffb1564c26f62b08da1e9702d1bf328a4bc88d48", + "artifact_id": 11812716972, + "capture_sha256": "0da935bc455440126660cfc9329fd955e92dea24dda67f290e8035875a6187ab", + "check_run_id": 115292781557, "conclusion": "success", - "job_id": 115154313567, + "job_id": 115292781557, "platform": "windows", "runner": "windows-latest", - "runner_id": 1000035614, + "runner_id": 1000036057, "runner_labels": [ "windows-latest" ], - "runner_name": "GitHub Actions 1000035614" + "runner_name": "GitHub Actions 1000036057" } ], "catalog_sha256": "f92926d7a643aed5dfb151ba8af1ff570f1a8e46a3587a91e2ae4498868abe82", @@ -2099,11 +2261,11 @@ }, { "path": ".github/scripts/pitot_e2e_report.py", - "sha256": "8ef5d1b62184b9e20df5a200382b9e053aa4b511729998356798eafa5504ba0a" + "sha256": "63cd940aa8d98f6cbc5a68227bc9fb8f8b9454d73199d398c4c36b6b301eafbe" }, { "path": ".github/workflows/pitot-e2e-agent.yml", - "sha256": "87ade7b93ca7b767ff9e6193c64234ad16b6e08e6396d52c1c2b4da8cc3d76bf" + "sha256": "ba825bffba6cba886c912601ea29e23c08c56244483bcc0154cf06d651bf6f3c" }, { "path": ".github/workflows/pitot-e2e.yml", @@ -2113,6 +2275,10 @@ "path": "adapter-verification.json", "sha256": "f92926d7a643aed5dfb151ba8af1ff570f1a8e46a3587a91e2ae4498868abe82" }, + { + "path": "cmd/pitot/deepseek_acp.go", + "sha256": "2342016a16b76a369ef6db9e3cf8872a86cdaf4f37a6d57b2d3ca9bbc5fbb580" + }, { "path": "integrations/codex/PreToolUse.ps1", "sha256": "a032bdefcaa6bdabc240ea5a91b19caa9804e976697ae67cbbb9c3a407fa4450" @@ -2161,21 +2327,29 @@ "path": "integrations/qwen/PreToolUse.cjs", "sha256": "e9bf00bbbee5c15f01ea203d0992b34f8754b75a9c9889eecb16baeb10fcce6c" }, + { + "path": "internal/deepseekacp/client.go", + "sha256": "6e328c6f0f66b6cf851a6150dfc77d55c0d313af9205fb4d8c6e0c46c56d4695" + }, + { + "path": "internal/deepseekacp/version.go", + "sha256": "76d4fa9195c9caaa07ea28f21a6cb61eeed5ac833c22868d3d32f4b5e91e0ef9" + }, { "path": "internal/testrole/main.go", "sha256": "6d657eb85d8ddaff0ae5a3da281aa7aa0a4bdda860d179e99508110033765787" }, { "path": "scripts/pitot_adapter_supervisor.py", - "sha256": "ee6fa1e8b96d84aa5bd34a5ce9d758855bd651c462e6b284a575689a543c0c99" + "sha256": "b0235e463d0d38a344c8aafa7be439f6aba1916c1b97db10126df43400f84b90" }, { "path": "scripts/pitot_capture_source.py", - "sha256": "cacc0ebd58f8f5e1e6df07b6fa71d1a98a3283f0bb50ccff78319572729c4854" + "sha256": "2a7afd1ff0062dea968bfb4a4344ee786ebe502180b152af690a0ad262af3f67" }, { "path": "tests/agent_identity.py", - "sha256": "4eb4112bf7af725d67cadc819c64538c8a8290997e53a7e6f69ef44187b9d823" + "sha256": "1799b33f36b7527fd95d616a4aa997ec27b552702408bd20bf758fe7c1624727" }, { "path": "tests/cursor_control_proxy.mjs", @@ -2239,7 +2413,11 @@ }, { "path": "tests/install_real_agent.py", - "sha256": "0b8da97ac0210e18a9ddc409a90d46ed26083b97fa7884e284e563e7d974f0b3" + "sha256": "5f3474219f36f7a68988fb51912476586f90a31c9a4671f6336e1637549c8252" + }, + { + "path": "tests/installation_identity.py", + "sha256": "8a6b32d3f304561e1f7194edb534ad60bf845459de745509cc5a934f7ff83624" }, { "path": "tests/model_control_proxy.py", @@ -2247,11 +2425,11 @@ }, { "path": "tests/real_agent_driver.py", - "sha256": "7c75f03630f4f84883f014de00921ded8e2be4ad194ff990db335d675cd10c3e" + "sha256": "4629601ef65127c5618ab4807be67d3459c7f2beb5e4df1a374f4ed67a9c320a" }, { "path": "tests/run_e2e_report.py", - "sha256": "b994fcca93932de23ad9ece10399adcbb62ab75deb3563865726d7b1c1ec157d" + "sha256": "6f75f96c294258c2ff8944640ee8485ddcd72828ca0cef284213c23da78e5b41" }, { "path": "tests/witness/main.go", @@ -2262,8 +2440,8 @@ "repository": "operatorstack/pitot", "repository_id": 1305805198, "run_attempt": 1, - "run_id": 38354544512, - "source_sha": "8f9db79ff096f8890e36dc2b6c227ae04f3baab4", + "run_id": 38396428636, + "source_sha": "6c1635114587cdcbafaa07cbf03024d0d9c61637", "workflow_id": 381486275, "workflow_path": ".github/workflows/pitot-e2e.yml" } diff --git a/tests/install_real_agent.py b/tests/install_real_agent.py index 2e097c2..7ec28cd 100644 --- a/tests/install_real_agent.py +++ b/tests/install_real_agent.py @@ -18,7 +18,17 @@ import urllib.request import zipfile -from agent_identity import owned_entrypoint, probe_version +from installation_identity import persist_probe_error +from agent_identity import owned_entrypoint, probe_version, VersionProbeError +from installation_identity import ( + PRIORITY_AGENTS, + installation_prefix, + observe_installation, + node_environment, + node_probe, + node_output, + prepare_installation, +) LAB = Path(__file__).resolve().parent.parent @@ -151,29 +161,73 @@ def install(agent: dict[str, object], platform: str, runtime: str) -> str: run([*wsl, "chmod", "0755", f"{release}/cursor-agent"]) run([*wsl, "ln", "-sfn", f"{release}/cursor-agent", "/usr/local/bin/agent"]) return f"{release}/cursor-agent" - if kind == "npm" and agent["id"] == "deepseek": - prefix = Path.home() / ".local/share/pitot-deepseek" / version - run( - [ - npm_executable(), - "install", - "--prefix", - str(prefix), - "--ignore-scripts", - "--no-audit", - "--no-fund", - "--save-exact", - f"{package}@{version}", - ] - ) - lock = json.loads((prefix / "package-lock.json").read_text(encoding="utf-8")) - selected = lock["packages"]["node_modules/@deepseek-ai/dsh"] - if ( - selected["version"] != version - or selected["integrity"] - != "sha512-EAJ3gPNcVt/uv8X19PMm9NkVhWgT7xXNMk0UKCVm+IQ5rpSQOcsMUa0HWlnYYVybKMsccjcRB21vVVsaXQ6IdA==" - ): - raise RuntimeError("DeepSeek release integrity mismatch") + if kind == "npm" and agent["id"] in PRIORITY_AGENTS: + prefix = installation_prefix(agent) + prepare_installation(prefix) + with tempfile.TemporaryDirectory(prefix="pitot-npm-") as temporary: + owned = Path(temporary) + userconfig, globalconfig = owned / "user.npmrc", owned / "global.npmrc" + userconfig.write_text("", encoding="utf-8") + globalconfig.write_text("", encoding="utf-8") + environment = node_environment( + { + key: value + for key, value in os.environ.items() + if not key.lower().startswith("npm_config_") + } + ) + run( + [ + npm_executable(), + "install", + "--prefix", + str(prefix), + "--userconfig", + str(userconfig), + "--globalconfig", + str(globalconfig), + "--cache", + str(owned / "cache"), + "--ignore-scripts" + if agent["id"] == "deepseek" + else "--ignore-scripts=false", + "--no-audit", + "--no-fund", + "--save-exact", + f"{package}@{version}", + ], + cwd=owned, + env=environment, + ) + if agent["id"] == "qwen": + node = shutil.which("node.exe" if os.name == "nt" else "node") + if node is None: + raise RuntimeError("Node runtime is unavailable") + target = json.loads( + node_output( + node_probe( + [node, "-p", "JSON.stringify([process.platform, process.arch])"] + ) + ) + ) + if ( + not isinstance(target, list) + or len(target) != 2 + or not all(isinstance(item, str) for item in target) + ): + raise RuntimeError("Node did not identify its installation platform") + initialize_qwen_ripgrep(prefix, *target) + if agent["id"] == "deepseek": + lock = json.loads( + (prefix / "package-lock.json").read_text(encoding="utf-8") + ) + selected = lock["packages"]["node_modules/@deepseek-ai/dsh"] + if ( + selected["version"] != version + or selected["integrity"] + != "sha512-EAJ3gPNcVt/uv8X19PMm9NkVhWgT7xXNMk0UKCVm+IQ5rpSQOcsMUa0HWlnYYVybKMsccjcRB21vVVsaXQ6IdA==" + ): + raise RuntimeError("DeepSeek release integrity mismatch") elif kind == "npm": run( [ @@ -341,13 +395,41 @@ def install(agent: dict[str, object], platform: str, runtime: str) -> str: return owned_entrypoint(agent, platform, runtime) +def initialize_qwen_ripgrep( + prefix: Path, node_platform: str, architecture: str +) -> None: + """Finish the pinned CLI's first-use executable setup before observing it.""" + if node_platform == "win32": + return + if node_platform not in {"darwin", "linux"} or architecture not in {"arm64", "x64"}: + raise RuntimeError("unsupported Qwen bundled ripgrep platform") + binary = ( + prefix + / "node_modules/@qwen-code/qwen-code/vendor/ripgrep" + / f"{architecture}-{node_platform}" + / "rg" + ) + info = binary.lstat() + if not stat.S_ISREG(info.st_mode) or not binary.resolve(strict=True).is_relative_to( + prefix.resolve(strict=True) + ): + raise RuntimeError("Qwen bundled ripgrep must be a regular owned file") + # Qwen's first search applies exactly this repair when all execute bits are absent. + if info.st_mode & 0o111 == 0: + binary.chmod(0o755) + + def executable_receipt( agent: dict[str, object], platform: str, runtime: str, path: str | None = None ) -> dict[str, object]: path = path if path is not None else owned_entrypoint(agent, platform, runtime) - output, digest = probe_version(agent, path, runtime) + installation = ( + observe_installation(agent) if agent["id"] in PRIORITY_AGENTS else None + ) + output, digest = probe_version(agent, path, runtime, installation) return { - "schema_version": 1, + **({"installation": installation} if installation is not None else {}), + "schema_version": 2 if installation is not None else 1, "agent": agent["id"], "version": agent["version"], "executable": path, @@ -373,8 +455,12 @@ def main() -> int: runtime = agent["runtime"].get(args.platform) if runtime is None: raise SystemExit(f"unsupported platform: {args.platform}") - path = install(agent, args.platform, runtime) - receipt = executable_receipt(agent, args.platform, runtime, path) + try: + path = install(agent, args.platform, runtime) + receipt = executable_receipt(agent, args.platform, runtime, path) + except VersionProbeError as error: + persist_probe_error(error, args.output) + raise SystemExit(str(error)) from None args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text( json.dumps(receipt, indent=2, sort_keys=True) + "\n", encoding="utf-8" diff --git a/tests/installation_identity.py b/tests/installation_identity.py new file mode 100644 index 0000000..4858d3b --- /dev/null +++ b/tests/installation_identity.py @@ -0,0 +1,514 @@ +"""Observe an owned installation tree; this is not publisher authentication.""" + +from __future__ import annotations + +import hashlib +import json +import os +from pathlib import Path, PurePosixPath +import re +import shutil +import stat +import subprocess + +PRIORITY_AGENTS = {"codex", "qwen", "deepseek"} + + +TREE_PROFILE = "owned-installation/v1" +MAX_TREE_ENTRIES = 100_000 +MAX_TREE_BYTES = 4 * 1024 * 1024 * 1024 + + +MAX_VERSION_BYTES = 65536 + + +class VersionProbeError(RuntimeError): + """Retain bounded streams while keeping arbitrary CLI output out of logs.""" + + def __init__( + self, + reason: str, + stdout: bytes = b"", + stderr: bytes = b"", + exit_code: int | None = None, + ): + self.stdout = (stdout or b"")[:MAX_VERSION_BYTES] + self.stderr = (stderr or b"")[:MAX_VERSION_BYTES] + self.reason = reason + self.exit_code = exit_code + self.diagnostic = { + "reason": reason, + "exit_code": exit_code, + "stdout": self._summary(stdout or b""), + "stderr": self._summary(stderr or b""), + } + super().__init__( + f"agent version probe {reason}; stdout_bytes={len(stdout or b'')}; stderr_bytes={len(stderr or b'')}; exit={exit_code}" + ) + + @staticmethod + def _summary(data: bytes) -> dict[str, object]: + captured = data[:MAX_VERSION_BYTES] + return { + "bytes": len(data), + "captured_bytes": len(captured), + "truncated": len(captured) != len(data), + "sha256": hashlib.sha256(captured).hexdigest(), + } + + +def persist_probe_error(error: VersionProbeError, output: Path) -> None: + output.parent.mkdir(parents=True, exist_ok=True) + for name, data in (("stdout", error.stdout), ("stderr", error.stderr)): + private = output.with_suffix(f".probe-private-{name}") + descriptor = os.open(private, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + with os.fdopen(descriptor, "wb") as stream: + stream.write(data) + output.with_suffix(".probe.json").write_text( + json.dumps(error.diagnostic, indent=2) + "\n", encoding="utf-8" + ) + + +def node_probe(command: list[str]) -> subprocess.CompletedProcess: + try: + return subprocess.run( + command, + check=True, + capture_output=True, + timeout=30, + env=node_environment(dict(os.environ)), + ) + except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as error: + raise VersionProbeError( + "node-timed-out" + if isinstance(error, subprocess.TimeoutExpired) + else "node-nonzero-exit", + error.stdout, + error.stderr, + getattr(error, "returncode", None), + ) from None + + +def node_output(result: subprocess.CompletedProcess) -> str: + if len(result.stdout) + len(result.stderr) > MAX_VERSION_BYTES: + raise VersionProbeError("node-output-limit", result.stdout, result.stderr) + try: + value = result.stdout.decode("utf-8", errors="strict").strip() + except UnicodeDecodeError: + raise VersionProbeError( + "node-invalid-encoding", result.stdout, result.stderr + ) from None + if result.stderr: + raise VersionProbeError("node-unexpected-stderr", result.stdout, result.stderr) + return value + + +def node_entrypoint(node: str, entry: str) -> str: + result = node_probe( + [ + node, + "-e", + "console.log(require('node:fs').realpathSync(process.argv[1]))", + entry, + ] + ) + value = node_output(result) + if result.stderr or not Path(value).is_absolute() or "\0" in value: + raise VersionProbeError("node-invalid-entrypoint", result.stdout, result.stderr) + return value + + +def installation_prefix(agent: dict[str, object]) -> Path: + home = Path.home() + if os.name != "nt": + home = home.resolve(strict=True) + return home / ".local/share/pitot-agents" / str(agent["id"]) / str(agent["version"]) + + +def prepare_installation(prefix: Path) -> None: + """Reject redirected ancestors before npm can create installation effects.""" + for parent in reversed(prefix.parents): + try: + info = parent.lstat() + except FileNotFoundError: + parent.mkdir() + info = parent.lstat() + _reject_reparse(info) + if not stat.S_ISDIR(info.st_mode): + raise RuntimeError("installation parent is not a regular directory") + prefix.mkdir() # Exclusive: a previous or partial installation is not fresh. + + +def package_entrypoint(agent: dict[str, object], prefix: Path) -> Path: + package = prefix / "node_modules" / str(agent["installer"]["package"]) + metadata = json.loads((package / "package.json").read_text(encoding="utf-8")) + if ( + metadata.get("name") != agent["installer"]["package"] + or metadata.get("version") != agent["version"] + ): + raise RuntimeError("installed package does not match the supervised pin") + bins = metadata.get("bin") + entry = bins.get(str(agent["executable"])) if isinstance(bins, dict) else bins + if not isinstance(entry, str) or not entry or "\0" in entry: + raise RuntimeError("installed package has no supported JS entrypoint") + selected = (package / entry).resolve(strict=True) + if ( + not selected.is_relative_to(prefix.resolve(strict=True)) + or selected.suffix != ".js" + or not selected.is_file() + ): + raise RuntimeError("installed JS entrypoint escapes the owned prefix") + return selected + + +def node_environment(environment: dict[str, str]) -> dict[str, str]: + return { + key: value + for key, value in environment.items() + if key.upper() not in {"NODE_OPTIONS", "NODE_PATH", "NODE_COMPILE_CACHE"} + } + + +def file_digest(path: Path) -> str: + if not path.is_absolute() or not path.is_file(): + raise RuntimeError("runtime path must select an absolute regular file") + digest = hashlib.sha256() + with path.open("rb") as stream: + for block in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(block) + return digest.hexdigest() + + +def observe_installation(agent: dict[str, object]) -> dict[str, object]: + prefix = installation_prefix(agent) + tree = tree_digest(prefix) + entry = package_entrypoint(agent, prefix) + selected = shutil.which("node.exe" if os.name == "nt" else "node") + if selected is None: + raise RuntimeError("Node runtime is unavailable") + # Version managers can expose a symlink to a multi-call launcher. Resolving + # that symlink and invoking its target would run the manager, not Node. + located = node_probe([selected, "-p", "process.execPath"]) + runtime_path = node_output(located) + if ( + located.stderr + or not runtime_path + or "\0" in runtime_path + or not Path(runtime_path).is_absolute() + ): + raise VersionProbeError( + "node-invalid-runtime-path", located.stdout, located.stderr + ) + try: + node = Path(runtime_path).resolve(strict=True) + digest = file_digest(node) + except (OSError, RuntimeError): + raise VersionProbeError( + "node-invalid-runtime-path", located.stdout, located.stderr + ) from None + result = node_probe([str(node), "--version"]) + version = node_output(result) + if result.stderr or not re.fullmatch(r"v[0-9]+\.[0-9]+\.[0-9]+", version): + raise VersionProbeError( + "node-invalid-version-banner", result.stdout, result.stderr + ) + value = { + "strategy": "absolute-node-js/v1", + "prefix": str(prefix), + "entrypoint": node_entrypoint(str(node), str(entry)), + "tree": tree, + "node": {"path": str(node), "sha256": digest, "version": version}, + } + verify_installation(value) + return value + + +def verify_installation(value: object) -> None: + if ( + not isinstance(value, dict) + or set(value) != {"strategy", "prefix", "entrypoint", "tree", "node"} + or value["strategy"] != "absolute-node-js/v1" + ): + raise RuntimeError("invalid installed-code identity") + for field in ("prefix", "entrypoint"): + if ( + not isinstance(value[field], str) + or "\0" in value[field] + or not Path(value[field]).is_absolute() + ): + raise RuntimeError("invalid installation path") + node = value["node"] + if ( + not isinstance(node, dict) + or set(node) != {"path", "sha256", "version"} + or not all(isinstance(x, str) and x and "\0" not in x for x in node.values()) + ): + raise RuntimeError("invalid Node identity") + if not re.fullmatch(r"[0-9a-f]{64}", node["sha256"]) or not re.fullmatch( + r"v[0-9]+\.[0-9]+\.[0-9]+", node["version"] + ): + raise RuntimeError("invalid Node identity digest or version") + tree = value["tree"] + if ( + not isinstance(tree, dict) + or set(tree) != {"profile", "metadata", "sha256", "entries", "bytes"} + or tree["profile"] != TREE_PROFILE + or tree["metadata"] != ("windows-readonly" if os.name == "nt" else "posix-mode") + or not isinstance(tree["sha256"], str) + or not re.fullmatch(r"[0-9a-f]{64}", tree["sha256"]) + or type(tree["entries"]) is not int + or not 1 <= tree["entries"] <= MAX_TREE_ENTRIES + or type(tree["bytes"]) is not int + or not 0 <= tree["bytes"] <= MAX_TREE_BYTES + ): + raise RuntimeError("invalid installation tree identity") + if tree_digest(Path(value["prefix"])) != value["tree"]: + raise RuntimeError("installed package tree changed since installation receipt") + entry = Path(value["entrypoint"]) + if ( + not entry.resolve(strict=True).is_relative_to( + Path(value["prefix"]).resolve(strict=True) + ) + or entry.suffix != ".js" + or not entry.is_file() + ): + raise RuntimeError("invalid installed JS entrypoint") + + if file_digest(Path(node["path"])) != node["sha256"]: + raise RuntimeError("Node runtime changed since installation receipt") + result = node_probe([node["path"], "--version"]) + if result.stderr or node_output(result) != node["version"]: + raise VersionProbeError( + "Node version changed since installation receipt", + result.stdout, + result.stderr, + ) + if node_entrypoint(node["path"], value["entrypoint"]) != value["entrypoint"]: + raise RuntimeError("JS entrypoint differs from Node canonical path") + if file_digest(Path(node["path"])) != node["sha256"]: + raise RuntimeError("Node runtime changed during version observation") + + +def installation_command(value: dict[str, object]) -> list[str]: + return [value["node"]["path"], value["entrypoint"]] + + +def identity_evidence(receipt: dict[str, object]) -> dict[str, str]: + """Project observed identity without publishing runner-local paths.""" + if receipt["agent"] not in PRIORITY_AGENTS: + return {"scope": "entrypoint/v1"} + value = receipt["installation"] + return { + "scope": "owned-prefix-and-node/v1", + "strategy": value["strategy"], + "tree_profile": value["tree"]["profile"], + "metadata": value["tree"]["metadata"], + "tree_sha256": value["tree"]["sha256"], + "node_sha256": value["node"]["sha256"], + "node_version": value["node"]["version"], + "entrypoint": Path(value["entrypoint"]) + .relative_to(Path(value["prefix"]).resolve()) + .as_posix(), + } + + +def validate_identity_evidence(agent: str, platform: str, value: object) -> None: + """Validate a portable observation, without executing or reading its paths.""" + if agent not in PRIORITY_AGENTS: + if value != {"scope": "entrypoint/v1"}: + raise ValueError("invalid entrypoint identity scope") + return + if ( + not isinstance(value, dict) + or set(value) + != { + "scope", + "strategy", + "tree_profile", + "metadata", + "tree_sha256", + "node_sha256", + "node_version", + "entrypoint", + } + or not all( + isinstance(item, str) and item and "\0" not in item + for item in value.values() + ) + or value["scope"] != "owned-prefix-and-node/v1" + or value["strategy"] != "absolute-node-js/v1" + or value["tree_profile"] != TREE_PROFILE + or value["metadata"] + != ("windows-readonly" if platform == "windows" else "posix-mode") + or not re.fullmatch(r"[0-9a-f]{64}", value["tree_sha256"]) + or not re.fullmatch(r"[0-9a-f]{64}", value["node_sha256"]) + or not re.fullmatch(r"v[0-9]+\.[0-9]+\.[0-9]+", value["node_version"]) + ): + raise ValueError("invalid installed-code identity evidence") + entry = PurePosixPath(value["entrypoint"]) + if ( + entry.as_posix() != value["entrypoint"] + or entry.is_absolute() + or ".." in entry.parts + or "\\" in value["entrypoint"] + or not entry.parts + or entry.parts[0] != "node_modules" + or entry.suffix != ".js" + ): + raise ValueError("invalid portable package entrypoint") + + +def installation_environment( + agent: str, value: dict[str, object], environment: dict[str, str] +) -> dict[str, str]: + result = node_environment(environment) + if agent == "qwen": + home = result.get("QWEN_HOME") + if not home or not Path(home).is_absolute(): + raise RuntimeError("Qwen execution requires an owned absolute home") + result["QWEN_CODE_MANAGED_NPM_PIN"] = json.dumps( + { + "bootstrap": value["entrypoint"], + "version": None, + "updateRoot": str(Path(home) / "updates/npm"), + } + ) + return result + + +def _unchanged( + before: os.stat_result, after: os.stat_result, *, handle: bool = False +) -> bool: + fields = ["st_dev", "st_ino", "st_size", "st_mtime_ns", "st_ctime_ns"] + # Windows path stat adds execute bits for certain filename extensions; + # descriptor stat cannot apply that rule. Recheck path attributes separately. + if os.name != "nt" or not handle: + fields.extend(("st_mode", "st_file_attributes", "st_reparse_tag")) + return all( + getattr(before, field, None) == getattr(after, field, None) for field in fields + ) + + +def _reject_reparse(info: os.stat_result) -> None: + if getattr(info, "st_file_attributes", 0) & stat.FILE_ATTRIBUTE_REPARSE_POINT: + raise RuntimeError("installation contains a Windows reparse point") + + +def _metadata(info: os.stat_result) -> int: + if os.name == "nt": + # Windows mode bits are synthesized. Bind readonly, not an invented ACL. + return info.st_file_attributes & stat.FILE_ATTRIBUTE_READONLY + return stat.S_IMODE(info.st_mode) + + +def tree_digest(root: Path) -> dict[str, object]: + """Bind names, types, metadata and bytes, without following directory links. + + File links used by npm must resolve within the installation. Directory links + are outside this profile. Before/after observations detect preserved changes; + they do not provide an atomic snapshot or prevent concurrent replacement. + """ + if not root.is_absolute(): + raise RuntimeError("installation root must be absolute") + try: + requested_root = root + initial = root.lstat() + _reject_reparse(initial) + if not stat.S_ISDIR(initial.st_mode): + raise RuntimeError("installation root must be a regular directory") + ancestors: list[tuple[Path, os.stat_result]] = [] + if os.name == "nt": + for parent in root.parents: + info = parent.lstat() + _reject_reparse(info) + ancestors.append((parent, info)) + root = root.resolve(strict=True) + + def check_root() -> None: + if not _unchanged(initial, requested_root.lstat()): + raise RuntimeError("installation root changed during observation") + for parent, before in ancestors: + after = parent.lstat() + _reject_reparse(after) + # Ancestor directory contents may change independently. Retain + # its location and type without claiming ownership of siblings. + if any( + getattr(before, field, None) != getattr(after, field, None) + for field in ("st_dev", "st_ino", "st_mode", "st_reparse_tag") + ): + raise RuntimeError( + "installation ancestor changed during observation" + ) + + check_root() + records: list[list[object]] = [] + observations: list[tuple[Path, os.stat_result]] = [] + total_bytes = 0 + + def visit(path: Path) -> None: + nonlocal total_bytes + if len(records) >= MAX_TREE_ENTRIES: + raise RuntimeError("installation exceeds entry limit") + before = path.lstat() + _reject_reparse(before) + name = path.relative_to(root).as_posix() + name.encode("utf-8", errors="strict") + observations.append((path, before)) + metadata = _metadata(before) + if stat.S_ISDIR(before.st_mode): + records.append([name, "directory", metadata]) + for child in sorted(path.iterdir(), key=lambda item: item.name): + visit(child) + elif stat.S_ISLNK(before.st_mode): + target = os.readlink(path) + target.encode("utf-8", errors="strict") + resolved = path.resolve(strict=True) + if not resolved.is_relative_to(root) or not resolved.is_file(): + raise RuntimeError("installation link must select an internal file") + records.append([name, "symlink", metadata, target]) + elif stat.S_ISREG(before.st_mode): + total_bytes += before.st_size + if total_bytes > MAX_TREE_BYTES: + raise RuntimeError("installation exceeds byte limit") + digest = hashlib.sha256() + count = 0 + with path.open("rb") as stream: + if not _unchanged(before, os.fstat(stream.fileno()), handle=True): + raise RuntimeError("installation changed while opening a file") + for block in iter(lambda: stream.read(1024 * 1024), b""): + count += len(block) + if count > before.st_size: + raise RuntimeError( + "installation file grew during observation" + ) + digest.update(block) + if count != before.st_size or not _unchanged( + before, os.fstat(stream.fileno()), handle=True + ): + raise RuntimeError( + "installation changed during file observation" + ) + records.append([name, "file", metadata, count, digest.hexdigest()]) + else: + raise RuntimeError("installation contains an unsupported file type") + if not _unchanged(before, path.lstat()): + raise RuntimeError("installation changed during tree observation") + + visit(root) + for path, before in observations: + if not _unchanged(before, path.lstat()): + raise RuntimeError("installation changed during tree observation") + check_root() + profile = "windows-readonly" if os.name == "nt" else "posix-mode" + canonical = json.dumps( + [TREE_PROFILE, profile, records], ensure_ascii=True, separators=(",", ":") + ).encode("ascii") + return { + "profile": TREE_PROFILE, + "metadata": profile, + "sha256": hashlib.sha256(canonical).hexdigest(), + "entries": len(records), + "bytes": total_bytes, + } + except (OSError, UnicodeError, ValueError) as error: + raise RuntimeError("cannot observe the complete installation tree") from error diff --git a/tests/real_agent_driver.py b/tests/real_agent_driver.py index d071cc8..0486903 100644 --- a/tests/real_agent_driver.py +++ b/tests/real_agent_driver.py @@ -23,6 +23,14 @@ import time import urllib.parse +from installation_identity import ( + VersionProbeError, + persist_probe_error, + installation_command, + installation_environment, + identity_evidence, +) + from agent_identity import ( agent_environment, cursor_context, @@ -825,6 +833,7 @@ def capture_record( "runtime": installation["runtime"], "version": installation["version"], "executable_sha256": digest, + "installation": identity_evidence(installation), "dialect": dialect, "request": observed, "response": { @@ -862,6 +871,7 @@ def validate_capture_fixture(record: dict[str, object]) -> dict[str, object]: "request": record["request"], "response": record["response"], "executable_sha256": record["executable_sha256"], + "installation": record["installation"], } @@ -1423,6 +1433,7 @@ def validate_receipts( "request": captured["request"], "response": captured["response"], "executable_sha256": captured["executable_sha256"], + "installation": captured["installation"], } else: endpoint_evidence = validate_capture_fixture(captured) @@ -1433,6 +1444,7 @@ def validate_receipts( "version": installation["version"], "executable": installation["executable"], "executable_sha256": installation["executable_sha256"], + "installation": identity_evidence(installation), "installer": installation["installer"], "runtime": installation["runtime"], }, @@ -1507,6 +1519,67 @@ def require_fixture_removed(base: Path) -> None: raise RuntimeError("Cursor temporary fixture cleanup failed") +def qwen_update_canary( + installation: dict[str, object], environment: dict[str, str], base: Path +) -> Path: + """Place a valid managed update outside the pinned install as a negative control.""" + value = installation["installation"] + entry = value["entrypoint"] + observed = subprocess.run( + [ + value["node"]["path"], + "-e", + "console.log(require('node:fs').statSync(process.argv[1]).ctimeMs)", + entry, + ], + check=True, + capture_output=True, + text=True, + timeout=30, + env=environment, + ) + ctime = json.loads(observed.stdout) + root = ( + Path(environment["QWEN_HOME"]) + / "updates/npm" + / hashlib.sha256(entry.encode()).hexdigest()[:16] + ) + package = root / "versions/999.0.0/node_modules/@qwen-code/qwen-code" + package.mkdir(parents=True) + marker = base / "unrecorded-qwen-update-selected" + write_json( + package / "package.json", + {"name": "@qwen-code/qwen-code", "version": "999.0.0", "type": "module"}, + ) + (package / "cli.js").write_text( + "import {writeFileSync} from 'node:fs'; writeFileSync(" + + json.dumps(str(marker)) + + ", 'selected'); process.exit(91);\n", + encoding="utf-8", + ) + write_json( + root / "active.json", + { + "version": "999.0.0", + "bootstrap": entry, + "baseVersion": installation["version"], + "bootstrapCtimeMs": ctime, + }, + ) + unpinned = dict(environment) + unpinned.pop("QWEN_CODE_MANAGED_NPM_PIN", None) + control = subprocess.run( + installation_command(value) + ["--help"], + env=unpinned, + capture_output=True, + timeout=30, + ) + if control.returncode != 91 or not marker.is_file(): + raise RuntimeError("Qwen managed-update positive control did not execute") + marker.unlink() + return marker + + def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--agent", required=True) @@ -1520,6 +1593,14 @@ def main() -> int: parser.add_argument("--response-fault", choices=("none", "text"), default="none") parser.add_argument("--expect-incompatible-response", action="store_true") args = parser.parse_args() + try: + return run_session(args) + except VersionProbeError as error: + persist_probe_error(error, args.evidence) + raise SystemExit(str(error)) from None + + +def run_session(args: argparse.Namespace) -> int: agent = next(item for item in MANIFEST["agents"] if item["id"] == args.agent) installation = inspect_receipt( json.loads(args.installation.read_text(encoding="utf-8")), agent, args.platform @@ -1887,6 +1968,21 @@ def main() -> int: environment.pop("MSYSTEM", None) environment.pop("EXEPATH", None) executable = installation["executable"] + installed_code = installation.get("installation") + launch = ( + installation_command(installed_code) + if installed_code is not None + else [executable] + ) + if installed_code is not None: + environment = installation_environment( + args.agent, installed_code, environment + ) + update_canary = ( + qwen_update_canary(installation, environment, base) + if args.agent == "qwen" + else None + ) if host_controls_wsl: wsl_environment = { **extra_env, @@ -1940,7 +2036,9 @@ def main() -> int: "--prompt", prompt, "--exec", - executable, + launch[0], + "--entrypoint", + launch[1], "--cwd", str(project), "--base-url", @@ -1970,7 +2068,7 @@ def main() -> int: command = ( [executable, "-p", prompt, *flags] if args.agent in {"copilot", "kimi"} - else [executable, *flags, prompt] + else [*launch, *flags, prompt] ) verify_live_entrypoint(installation) try: @@ -2003,6 +2101,10 @@ def main() -> int: ) from error finally: verify_live_entrypoint(installation) + if update_canary is not None and update_canary.exists(): + raise RuntimeError( + "Qwen selected an update outside the recorded installation" + ) # GitHub's Windows Python console defaults to CP-1252 while several # released CLIs emit Unicode status glyphs. Emit UTF-8 bytes so the # reporting layer cannot fail after a successful agent session. diff --git a/tests/run_e2e_report.py b/tests/run_e2e_report.py index 14757eb..7be2d32 100644 --- a/tests/run_e2e_report.py +++ b/tests/run_e2e_report.py @@ -11,6 +11,8 @@ import subprocess import sys +from installation_identity import validate_identity_evidence + LAB = Path(__file__).resolve().parent.parent ROOT = LAB @@ -105,6 +107,7 @@ def load_evidence(path: Path | None, *, agent: str) -> dict[str, object] | None: "request", "response", "executable_sha256", + "installation", } if not isinstance(endpoint, dict) or set(endpoint) != endpoint_required: return None @@ -112,6 +115,16 @@ def load_evidence(path: Path | None, *, agent: str) -> dict[str, object] | None: r"[0-9a-f]{64}", str(endpoint.get("fixture_sha256", "")) ): return None + try: + identity = value.get("cli", {}).get("installation") + metadata = identity.get("metadata") if isinstance(identity, dict) else None + validate_identity_evidence( + agent, "windows" if metadata == "windows-readonly" else "ubuntu", identity + ) + if endpoint["installation"] != identity: + return None + except ValueError: + return None return value diff --git a/tests/test_capture_source.py b/tests/test_capture_source.py index ec362bf..50f6daa 100644 --- a/tests/test_capture_source.py +++ b/tests/test_capture_source.py @@ -212,9 +212,9 @@ def test_complete_set_records_distinct_job_and_check_ids(self): output = Path(directory) / "accepted.json" self.promote(api, output) value = json.loads(output.read_text()) - self.assertEqual(value["schema_version"], 4) + self.assertEqual(value["schema_version"], 5) self.assertEqual(value["source"]["producer_profile"], "capture-direct/v1") - self.assertEqual(len(value["source"]["producer_files"]), 41) + self.assertEqual(len(value["source"]["producer_files"]), 45) self.assertIn( "tests/cursor_transport.mjs", {item["path"] for item in value["source"]["producer_files"]}, @@ -375,6 +375,20 @@ def test_historical_records_do_not_gain_current_producer_claims(self): historical = json.loads((ROOT / supervisor.ENDPOINT_PROVENANCE).read_text()) # Derive only the original schema3 vocabulary, not a new-profile upgrade. historical["schema_version"] = 3 + for cell in historical["cells"]: + cell.pop("installation", None) + core = { + key: value for key, value in cell.items() if key != "capture_sha256" + } + cell["capture_sha256"] = hashlib.sha256( + json.dumps(core, sort_keys=True, separators=(",", ":")).encode() + ).hexdigest() + for artifact in historical["source"]["artifacts"]: + if ( + artifact["agent"] == cell["agent"] + and artifact["platform"] == cell["platform"] + ): + artifact["capture_sha256"] = cell["capture_sha256"] historical["source"].pop("producer_files", None) historical["source"].pop("producer_profile", None) with tempfile.TemporaryDirectory() as directory: diff --git a/tests/test_e2e_reporting.py b/tests/test_e2e_reporting.py index 30e89c4..6ec7396 100644 --- a/tests/test_e2e_reporting.py +++ b/tests/test_e2e_reporting.py @@ -34,6 +34,21 @@ def _load(name: str, path: Path): ) +def _installation(agent, platform): + if agent not in {"codex", "qwen", "deepseek"}: + return {"scope": "entrypoint/v1"} + return { + "scope": "owned-prefix-and-node/v1", + "strategy": "absolute-node-js/v1", + "tree_profile": "owned-installation/v1", + "metadata": "windows-readonly" if platform == "windows" else "posix-mode", + "tree_sha256": "a" * 64, + "node_sha256": "b" * 64, + "node_version": "v22.0.0", + "entrypoint": "node_modules/fixture/cli.js", + } + + def _receipt(agent="claude", platform="ubuntu"): record = next(item for item in runner.MANIFEST["agents"] if item["id"] == agent) dialect = { @@ -70,6 +85,7 @@ def _receipt(agent="claude", platform="ubuntu"): "runtime": record["runtime"][platform], "version": record["version"], "executable_sha256": "d" * 64, + "installation": _installation(agent, platform), "dialect": dialect, "request": request, "response": response, @@ -94,6 +110,7 @@ def _receipt(agent="claude", platform="ubuntu"): "request": request, "response": response, "executable_sha256": fixture["executable_sha256"], + "installation": _installation(agent, platform), } return { "schema_version": 2, @@ -102,6 +119,7 @@ def _receipt(agent="claude", platform="ubuntu"): "version": record["version"], "executable": f"/bin/{record['executable']}", "executable_sha256": fixture["executable_sha256"], + "installation": _installation(agent, platform), "installer": record["installer"]["kind"], "runtime": record["runtime"][platform], }, @@ -669,6 +687,36 @@ def test_rejects_incomplete_or_mismatched_causal_receipts(self): with self.assertRaisesRegex(ValueError, "executable digest"): reporter.validate_result(value, agent="claude", platform="ubuntu") + def test_priority_installed_identity_cannot_be_dropped_or_replaced(self): + for agent in ("codex", "qwen", "deepseek"): + for change in ("missing", "legacy", "node", "tree", "extra", "absolute"): + with self.subTest(agent=agent, change=change): + value = _result(agent=agent) + if agent == "deepseek": + value["controller"]["native_tool_results"] = { + "session_id": "controlled-fixture-session", + "call_ids": ["pitot_tool_allow", "pitot_tool_deny"], + "is_error": [False, True], + "denial_code": "PITOT_DENIED", + "session_sha256": "a" * 64, + } + reporter.validate_result(value, agent=agent, platform="ubuntu") + identity = value["cli"]["installation"] + if change == "missing": + del value["cli"]["installation"] + elif change == "legacy": + value["cli"]["installation"] = {"scope": "entrypoint/v1"} + elif change == "node": + identity["node_sha256"] = "c" * 64 + elif change == "tree": + identity["tree_sha256"] = "c" * 64 + elif change == "extra": + identity["node_path"] = "/private/runner/node" + else: + identity["entrypoint"] = "/private/runner/cli.js" + with self.assertRaises(ValueError): + reporter.validate_result(value, agent=agent, platform="ubuntu") + def test_rejects_untrusted_commit_and_run_url(self): value = _result() with self.assertRaises(ValueError): diff --git a/tests/test_installation_identity.py b/tests/test_installation_identity.py new file mode 100644 index 0000000..27fd769 --- /dev/null +++ b/tests/test_installation_identity.py @@ -0,0 +1,599 @@ +from __future__ import annotations + +import os +import json +import hashlib +import copy +import shutil +from pathlib import Path +import stat +import subprocess +import tempfile +import unittest +from unittest import mock + +import agent_identity +import install_real_agent +from installation_identity import ( + tree_digest, + installation_prefix, + installation_environment, +) + + +class InstallationTreeTests(unittest.TestCase): + @unittest.skipIf(os.name == "nt", "POSIX executable mode initialization") + def test_qwen_ripgrep_initialized_before_snapshot_without_byte_change(self): + with tempfile.TemporaryDirectory() as d: + prefix = Path(d).resolve() + binary = ( + prefix + / "node_modules/@qwen-code/qwen-code/vendor/ripgrep/arm64-darwin/rg" + ) + binary.parent.mkdir(parents=True) + binary.write_bytes(b"owned bundled executable") + binary.chmod(0o644) + before = tree_digest(prefix) + install_real_agent.initialize_qwen_ripgrep(prefix, "darwin", "arm64") + self.assertEqual(stat.S_IMODE(binary.stat().st_mode), 0o755) + self.assertEqual(binary.read_bytes(), b"owned bundled executable") + initialized = tree_digest(prefix) + self.assertNotEqual(before, initialized) + install_real_agent.initialize_qwen_ripgrep(prefix, "darwin", "arm64") + self.assertEqual(tree_digest(prefix), initialized) + binary.chmod(0o744) + install_real_agent.initialize_qwen_ripgrep(prefix, "darwin", "arm64") + self.assertEqual(stat.S_IMODE(binary.stat().st_mode), 0o744) + + def test_qwen_windows_initialization_has_no_filesystem_effect(self): + with tempfile.TemporaryDirectory() as d: + prefix = Path(d).resolve() + before = tree_digest(prefix) + install_real_agent.initialize_qwen_ripgrep(prefix, "win32", "x64") + self.assertEqual(tree_digest(prefix), before) + + def test_executable_extensions_are_stable_regular_files(self): + with tempfile.TemporaryDirectory() as d: + root = Path(d) + for extension in ("exe", "cmd", "bat", "com", "js"): + (root / ("program." + extension)).write_bytes(b"stable bytes") + original = tree_digest(root) + self.assertEqual(original, tree_digest(root)) + + def tree(self, root: Path, reverse: bool = False) -> None: + root.mkdir() + names = ["package.json", "cli.js", "payload.js"] + for name in reversed(names) if reverse else names: + (root / name).write_bytes(name.encode()) + (root / "empty").mkdir() + + def test_order_location_and_mtime_do_not_replace_content_identity(self): + with tempfile.TemporaryDirectory() as directory: + first, second = Path(directory) / "first", Path(directory) / "second" + self.tree(first) + self.tree(second, reverse=True) + original = tree_digest(first) + self.assertEqual(original, tree_digest(second)) + os.utime(first / "cli.js", (1, 1)) + self.assertEqual(original, tree_digest(first)) + + def test_unchanged_launcher_does_not_hide_delegated_tree_changes(self): + for mutation in ("payload", "add", "remove", "rename", "empty", "mode"): + with self.subTest(mutation=mutation), tempfile.TemporaryDirectory() as d: + root = Path(d) / "installed" + self.tree(root) + original = tree_digest(root) + launcher = (root / "cli.js").read_bytes() + payload = root / "payload.js" + if mutation == "payload": + payload.write_bytes(b"changed delegated behavior") + elif mutation == "add": + (root / "new.js").write_bytes(b"new") + elif mutation == "remove": + payload.unlink() + elif mutation == "rename": + payload.rename(root / "renamed.js") + elif mutation == "empty": + (root / "another-empty").mkdir() + else: + payload.chmod(stat.S_IREAD if os.name == "nt" else 0o700) + try: + self.assertEqual(launcher, (root / "cli.js").read_bytes()) + self.assertNotEqual(original["sha256"], tree_digest(root)["sha256"]) + finally: + if payload.exists(): + payload.chmod(0o600) + + @unittest.skipIf(os.name == "nt", "Windows profile refuses reparse points") + def test_internal_npm_file_link_binds_target_spelling_and_bytes(self): + with tempfile.TemporaryDirectory() as d: + root = Path(d) / "installed" + self.tree(root) + link = root / "launcher" + link.symlink_to("cli.js") + original = tree_digest(root) + link.unlink() + link.symlink_to("./cli.js") + self.assertNotEqual(original, tree_digest(root)) + original = tree_digest(root) + (root / "cli.js").write_bytes(b"changed") + self.assertNotEqual(original, tree_digest(root)) + + @unittest.skipIf(os.name == "nt", "POSIX link/special-file cases") + def test_unsupported_links_and_special_files_refuse(self): + for kind in ("escape", "dangling", "cycle", "directory", "fifo", "root"): + with self.subTest(kind=kind), tempfile.TemporaryDirectory() as d: + root = Path(d) / "installed" + self.tree(root) + link = root / "link" + if kind == "escape": + (Path(d) / "outside").write_bytes(b"outside") + link.symlink_to("../outside") + elif kind == "dangling": + link.symlink_to("missing") + elif kind == "cycle": + link.symlink_to("link") + elif kind == "directory": + link.symlink_to("empty", target_is_directory=True) + elif kind == "fifo": + os.mkfifo(link) + else: + alias = Path(d) / "alias" + alias.symlink_to(root, target_is_directory=True) + root = alias + with self.assertRaises(RuntimeError): + tree_digest(root) + + @unittest.skipIf(os.name == "nt", "Uses a POSIX root symlink swap") + def test_persistent_root_replacement_during_resolution_refuses(self): + with tempfile.TemporaryDirectory() as d: + root, other = Path(d) / "installed", Path(d) / "replacement" + self.tree(root) + self.tree(other) + resolve = Path.resolve + replaced = False + + def swap(path, *args, **kwargs): + nonlocal replaced + if path == root and not replaced: + replaced = True + root.rename(Path(d) / "original") + root.symlink_to(other, target_is_directory=True) + return resolve(path, *args, **kwargs) + + with ( + mock.patch.object(Path, "resolve", swap), + self.assertRaises(RuntimeError), + ): + tree_digest(root) + + def test_limits_refuse_incomplete_observations(self): + with tempfile.TemporaryDirectory() as d: + root = Path(d) / "installed" + self.tree(root) + for limit in ("MAX_TREE_ENTRIES", "MAX_TREE_BYTES"): + with ( + self.subTest(limit=limit), + mock.patch("installation_identity." + limit, 1), + self.assertRaises(RuntimeError), + ): + tree_digest(root) + + @unittest.skipUnless(os.name == "nt", "Requires native Windows junctions") + def test_windows_junction_entry_root_and_parent_refuse(self): + with tempfile.TemporaryDirectory() as d: + base = Path(d) + root = base / "installed" + self.tree(root) + alias = base / "alias" + subprocess.run( + ["cmd", "/d", "/c", "mklink", "/J", str(alias), str(root)], + check=True, + capture_output=True, + ) + try: + for selected in (alias, alias / "empty"): + with ( + self.subTest(selected=selected), + self.assertRaises(RuntimeError), + ): + tree_digest(selected) + nested = root / "junction" + subprocess.run( + [ + "cmd", + "/d", + "/c", + "mklink", + "/J", + str(nested), + str(root / "empty"), + ], + check=True, + capture_output=True, + ) + try: + with self.assertRaises(RuntimeError): + tree_digest(root) + finally: + nested.rmdir() + finally: + alias.rmdir() + + +@unittest.skipUnless( + shutil.which("node"), "Requires Node for the shipped-JS command fixture" +) +class InstalledReceiptTests(unittest.TestCase): + def fixture(self, home): + agent = next( + a for a in install_real_agent.MANIFEST["agents"] if a["id"] == "codex" + ) + prefix = installation_prefix(agent) + package = prefix / "node_modules" / agent["installer"]["package"] + package.mkdir(parents=True) + (package / "package.json").write_text( + json.dumps( + { + "name": agent["installer"]["package"], + "version": agent["version"], + "type": "module", + "bin": {"codex": "cli.js"}, + } + ) + ) + (package / "cli.js").write_text( + 'import "./delegate.js"; console.log("codex-cli ' + agent["version"] + '");' + ) + (package / "delegate.js").write_text('export const value = "original";') + launcher = ( + prefix / "node_modules/.bin" / ("codex.cmd" if os.name == "nt" else "codex") + ) + launcher.parent.mkdir() + if os.name == "nt": + launcher.write_text('@echo off\nnode "%~dp0/../@openai/codex/cli.js" %*\n') + else: + launcher.symlink_to("../@openai/codex/cli.js") + platform = ( + "windows" + if os.name == "nt" + else "macos" + if __import__("sys").platform == "darwin" + else "ubuntu" + ) + receipt = install_real_agent.executable_receipt(agent, platform, "native") + return agent, platform, receipt, package + + def test_direct_node_receipt_ignores_path_shadow_and_rejects_delegate_change(self): + with ( + tempfile.TemporaryDirectory() as d, + mock.patch.object(Path, "home", return_value=Path(d)), + ): + agent, platform, receipt, package = self.fixture(Path(d)) + self.assertEqual(receipt["schema_version"], 2) + with mock.patch.dict( + os.environ, + { + "PATH": str(Path(d) / "no-runtime"), + "NODE_OPTIONS": "--require missing-module", + "NODE_PATH": str(Path(d) / "foreign"), + }, + ): + self.assertEqual( + agent_identity.inspect_receipt(receipt, agent, platform), receipt + ) + (package / "delegate.js").write_text('export const value = "changed";') + with self.assertRaisesRegex(RuntimeError, "package tree changed"): + agent_identity.inspect_receipt(receipt, agent, platform) + + def test_runtime_change_fails_before_launch(self): + with ( + tempfile.TemporaryDirectory() as d, + mock.patch.object(Path, "home", return_value=Path(d)), + ): + _, _, receipt, _ = self.fixture(Path(d)) + node = Path(d) / "observed-node" + node.write_bytes(b"original") + receipt["installation"]["node"]["path"] = str(node) + receipt["installation"]["node"]["sha256"] = hashlib.sha256( + b"original" + ).hexdigest() + node.write_bytes(b"changed") + with ( + mock.patch.object( + agent_identity.subprocess, + "run", + side_effect=AssertionError("must not execute"), + ), + self.assertRaisesRegex(RuntimeError, "Node runtime changed"), + ): + agent_identity.verify_live_entrypoint(receipt) + + def test_legacy_priority_receipt_cannot_bypass_tree_identity(self): + with ( + tempfile.TemporaryDirectory() as d, + mock.patch.object(Path, "home", return_value=Path(d)), + ): + agent, platform, receipt, _ = self.fixture(Path(d)) + receipt.pop("installation") + receipt["schema_version"] = 1 + with self.assertRaisesRegex(RuntimeError, "receipt fields"): + agent_identity.inspect_receipt(receipt, agent, platform) + + def test_forged_node_version_and_untyped_tree_counts_refuse(self): + with ( + tempfile.TemporaryDirectory() as d, + mock.patch.object(Path, "home", return_value=Path(d)), + ): + agent, platform, receipt, _ = self.fixture(Path(d)) + bad = copy.deepcopy(receipt) + bad["installation"]["node"]["version"] = "v0.0.0" + with self.assertRaisesRegex(RuntimeError, "Node version changed"): + agent_identity.inspect_receipt(bad, agent, platform) + for value in (True, 1.0): + bad = copy.deepcopy(receipt) + bad["installation"]["tree"]["entries"] = value + with ( + mock.patch.object( + agent_identity.subprocess, + "run", + side_effect=AssertionError("must not execute"), + ), + self.assertRaisesRegex(RuntimeError, "invalid installation tree"), + ): + agent_identity.inspect_receipt(bad, agent, platform) + + @unittest.skipIf(os.name == "nt", "POSIX redirected installer parent") + def test_installer_rejects_redirected_parent_before_npm(self): + with ( + tempfile.TemporaryDirectory() as d, + mock.patch.object(Path, "home", return_value=Path(d)), + ): + agent = next( + a for a in install_real_agent.MANIFEST["agents"] if a["id"] == "codex" + ) + outside = Path(d) / "foreign" + outside.mkdir() + (Path(d) / ".local").symlink_to(outside, target_is_directory=True) + with ( + mock.patch.object( + install_real_agent, + "run", + side_effect=AssertionError("npm must not execute"), + ), + self.assertRaisesRegex(RuntimeError, "parent"), + ): + install_real_agent.install(agent, "macos", "native") + self.assertEqual(list(outside.iterdir()), []) + + @unittest.skipUnless( + os.name == "nt", "Native Windows pre-install junction boundary" + ) + def test_installer_rejects_junction_before_creating_foreign_files(self): + with ( + tempfile.TemporaryDirectory() as d, + mock.patch.object(Path, "home", return_value=Path(d)), + ): + home = Path(d) + outside = home / "foreign" + outside.mkdir() + alias = home / ".local" + subprocess.run( + ["cmd", "/d", "/c", "mklink", "/J", str(alias), str(outside)], + check=True, + capture_output=True, + ) + try: + agent = next( + a + for a in install_real_agent.MANIFEST["agents"] + if a["id"] == "codex" + ) + with ( + mock.patch.object( + install_real_agent, + "run", + side_effect=AssertionError("npm must not execute"), + ), + self.assertRaisesRegex(RuntimeError, "reparse"), + ): + install_real_agent.install(agent, "windows", "native") + self.assertEqual(list(outside.iterdir()), []) + finally: + alias.rmdir() + + def test_failed_probe_retains_streams_without_emitting_raw_secrets(self): + with ( + tempfile.TemporaryDirectory() as d, + mock.patch.object(Path, "home", return_value=Path(d)), + ): + agent, _, receipt, package = self.fixture(Path(d)) + (package / "cli.js").write_text( + 'console.log("stdout marker"); console.error("private-token-marker"); process.exit(7);' + ) + value = receipt["installation"] + value["tree"] = tree_digest(Path(value["prefix"])) + with self.assertRaises(agent_identity.VersionProbeError) as raised: + agent_identity.probe_version( + agent, receipt["executable"], "native", value + ) + error = raised.exception + self.assertEqual(error.exit_code, 7) + self.assertEqual(error.stdout, b"stdout marker\n") + self.assertEqual(error.stderr, b"private-token-marker\n") + self.assertNotIn("private-token-marker", str(error)) + self.assertNotIn("private-token-marker", json.dumps(error.diagnostic)) + + def test_timeout_retains_separate_partial_streams(self): + with ( + tempfile.TemporaryDirectory() as d, + mock.patch.object(Path, "home", return_value=Path(d)), + ): + agent, _, receipt, _ = self.fixture(Path(d)) + run = subprocess.run + + def timeout(command, **kwargs): + if len(command) == 3 and command[-1] == "--version": + raise subprocess.TimeoutExpired( + command, 30, output=b"partial stdout", stderr=b"partial stderr" + ) + return run(command, **kwargs) + + with ( + mock.patch.object( + agent_identity.subprocess, "run", side_effect=timeout + ), + self.assertRaises(agent_identity.VersionProbeError) as raised, + ): + agent_identity.probe_version( + agent, receipt["executable"], "native", receipt["installation"] + ) + self.assertEqual(raised.exception.reason, "timed-out") + self.assertEqual(raised.exception.stdout, b"partial stdout") + self.assertEqual(raised.exception.stderr, b"partial stderr") + oversized = agent_identity.VersionProbeError( + "output-limit", b"x" * 100_000, b"y" * 100_000 + ) + self.assertEqual(len(oversized.stdout), agent_identity.MAX_VERSION_BYTES) + self.assertTrue(oversized.diagnostic["stderr"]["truncated"]) + + def test_nonexistent_reported_node_path_is_private_failure(self): + from installation_identity import observe_installation + + with ( + tempfile.TemporaryDirectory() as d, + mock.patch.object(Path, "home", return_value=Path(d)), + ): + agent, _, _, _ = self.fixture(Path(d)) + missing = str(Path(d) / "private-missing-runtime").encode() + with ( + mock.patch( + "installation_identity.node_probe", + return_value=subprocess.CompletedProcess([], 0, missing, b""), + ), + self.assertRaises(agent_identity.VersionProbeError) as caught, + ): + observe_installation(agent) + self.assertEqual(caught.exception.reason, "node-invalid-runtime-path") + self.assertEqual(caught.exception.stdout, missing) + self.assertNotIn("private-missing-runtime", str(caught.exception)) + + def test_malformed_node_output_retains_bounded_diagnostics(self): + from installation_identity import node_output + + for out, err in ( + (b"\xff", b""), + (b"v1.2.3", b"private"), + (b"x" * 100_000, b""), + ): + with ( + self.subTest(size=len(out)), + self.assertRaises(agent_identity.VersionProbeError) as caught, + ): + node_output(subprocess.CompletedProcess([], 0, out, err)) + self.assertLessEqual( + len(caught.exception.stdout), agent_identity.MAX_VERSION_BYTES + ) + + def test_node_probe_failure_keeps_private_streams(self): + from installation_identity import node_probe, persist_probe_error + + with ( + mock.patch( + "installation_identity.subprocess.run", + side_effect=subprocess.CalledProcessError( + 9, ["node"], output=b"out", stderr=b"secret" + ), + ), + self.assertRaises(agent_identity.VersionProbeError) as caught, + ): + node_probe(["node", "--version"]) + with tempfile.TemporaryDirectory() as d: + output = Path(d) / "receipt.json" + persist_probe_error(caught.exception, output) + self.assertEqual( + output.with_suffix(".probe-private-stderr").read_bytes(), b"secret" + ) + self.assertNotIn("secret", output.with_suffix(".probe.json").read_text()) + + def test_probe_failure_survives_identity_recheck_failure(self): + with ( + tempfile.TemporaryDirectory() as d, + mock.patch.object(Path, "home", return_value=Path(d)), + ): + agent, _, receipt, _ = self.fixture(Path(d)) + with ( + mock.patch.object( + agent_identity, + "verify_installation", + side_effect=[None, RuntimeError("changed")], + ), + mock.patch.object( + agent_identity.subprocess, + "run", + side_effect=subprocess.CalledProcessError( + 7, [], output=b"out", stderr=b"err" + ), + ), + self.assertRaises(agent_identity.VersionProbeError) as caught, + ): + agent_identity.probe_version( + agent, receipt["executable"], "native", receipt["installation"] + ) + self.assertEqual(caught.exception.stderr, b"err") + self.assertEqual(caught.exception.diagnostic["identity_recheck"], "failed") + + def test_completed_probe_streams_survive_failed_identity_recheck(self): + with ( + tempfile.TemporaryDirectory() as d, + mock.patch.object(Path, "home", return_value=Path(d)), + ): + agent, _, receipt, _ = self.fixture(Path(d)) + with ( + mock.patch.object( + agent_identity, + "verify_installation", + side_effect=[None, RuntimeError("changed")], + ), + mock.patch.object( + agent_identity.subprocess, + "run", + return_value=subprocess.CompletedProcess( + [], 0, b"unexpected banner", b"private output" + ), + ), + self.assertRaises(agent_identity.VersionProbeError) as caught, + ): + agent_identity.probe_version( + agent, receipt["executable"], "native", receipt["installation"] + ) + self.assertEqual(caught.exception.reason, "identity-recheck-failed") + self.assertEqual(caught.exception.stdout, b"unexpected banner") + self.assertEqual(caught.exception.stderr, b"private output") + self.assertNotIn("private output", str(caught.exception)) + + def test_qwen_null_pin_is_authored_after_inherited_override_removal(self): + with tempfile.TemporaryDirectory() as d: + home = str(Path(d) / "qwen") + entry = str(Path(d) / "package/cli-entry.js") + original = { + "QWEN_HOME": home, + "QWEN_CODE_MANAGED_NPM_PIN": "foreign", + "NODE_OPTIONS": "foreign", + } + result = installation_environment("qwen", {"entrypoint": entry}, original) + pin = json.loads(result["QWEN_CODE_MANAGED_NPM_PIN"]) + self.assertEqual( + pin, + { + "bootstrap": entry, + "version": None, + "updateRoot": str(Path(home) / "updates/npm"), + }, + ) + self.assertNotIn("NODE_OPTIONS", result) + self.assertEqual(original["NODE_OPTIONS"], "foreign") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_model_control_proxy.py b/tests/test_model_control_proxy.py index 33c3acd..22a2935 100644 --- a/tests/test_model_control_proxy.py +++ b/tests/test_model_control_proxy.py @@ -1083,6 +1083,7 @@ def test_capture_record_binds_detected_dialect_and_executable(self): item for item in driver.MANIFEST["agents"] if item["id"] == "claude" ) installation = { + "agent": agent["id"], "version": agent["version"], "runtime": "native", "executable_sha256": "a" * 64, @@ -1110,6 +1111,7 @@ def test_invented_endpoint_cannot_pass(self): item for item in driver.MANIFEST["agents"] if item["id"] == "cursor" ) installation = { + "agent": agent["id"], "version": agent["version"], "runtime": "native", "executable_sha256": "a" * 64,