diff --git a/.github/scripts/pitot_e2e_report.py b/.github/scripts/pitot_e2e_report.py
index 9e3247ac2..7551d1f4e 100644
--- a/.github/scripts/pitot_e2e_report.py
+++ b/.github/scripts/pitot_e2e_report.py
@@ -68,7 +68,7 @@ def validate_result(
"nonce",
"receipts",
"runtime",
- "hooks",
+ "boundaries",
"controller",
"consumer",
"canary",
@@ -86,10 +86,10 @@ def validate_result(
raise ValueError("passing result requires real_cli verification")
if value["status"] == "fail" and mode is not None:
raise ValueError("failed result cannot claim a verification mode")
- allowed_evidence = {"binary-observed request, real hook control, projected Consumer, allow/deny canary, and final receipts", "real-agent control evidence contract failed"}
+ allowed_evidence = {"binary-observed request, real action control, projected Consumer, allow/deny canary, and final receipts", "real-agent control evidence contract failed"}
if value["evidence"] not in allowed_evidence:
raise ValueError("invalid evidence summary")
- evidence_fields = ("cli", "protocol", "endpoint", "prompt_hash", "nonce", "receipts", "runtime", "hooks", "controller", "consumer", "canary")
+ evidence_fields = ("cli", "protocol", "endpoint", "prompt_hash", "nonce", "receipts", "runtime", "boundaries", "controller", "consumer", "canary")
if value["status"] == "fail":
if any(value[field] is not None for field in evidence_fields):
raise ValueError("failed result cannot carry passing evidence")
@@ -130,10 +130,11 @@ def validate_result(
}
if not isinstance(value["receipts"], dict) or set(value["receipts"]) != receipt_fields or not all(item is True for item in value["receipts"].values()):
raise ValueError("incomplete causal receipts")
- hooks = value["hooks"]
- if not isinstance(hooks, list) or len(hooks) != 2 or [item.get("pitot_exit") for item in hooks] != [0, 2] or any(item.get("host") != agent or item.get("action_kind") != "shell" or item.get("nonce") != nonce for item in hooks):
- raise ValueError("invalid Pitot allow/deny hook receipts")
- action_ids = [item.get("action_id") for item in hooks]
+ boundaries = value["boundaries"]
+ expected_transport = "acp" if record["integration"] == "acp_client" else "hook"
+ if not isinstance(boundaries, list) or len(boundaries) != 2 or [item.get("decision") for item in boundaries] != ["allow", "deny"] or any(item.get("host") != agent or item.get("transport") != expected_transport or item.get("action_kind") != "shell" or item.get("nonce") != nonce for item in boundaries):
+ raise ValueError("invalid Pitot allow/deny boundary receipts")
+ action_ids = [item.get("action_id") for item in boundaries]
if len(set(action_ids)) != 2 or value["controller"] != {"id": "e2e-shell-controller", "action_ids": action_ids, "outcomes": ["allow", "deny"]}:
raise ValueError("invalid Controller receipts")
if value["consumer"] != {"id": "e2e-audit", "action_ids": action_ids, "projection": "sha256"}:
@@ -237,7 +238,7 @@ def failed_result(agent: str, platform: str, head_sha: str, run_url: str) -> dic
"nonce": None,
"receipts": None,
"runtime": None,
- "hooks": None,
+ "boundaries": None,
"controller": None,
"consumer": None,
"canary": None,
diff --git a/labs/15-pitot/adapter-verification.json b/labs/15-pitot/adapter-verification.json
index 86ac44bfe..648907f91 100644
--- a/labs/15-pitot/adapter-verification.json
+++ b/labs/15-pitot/adapter-verification.json
@@ -1,7 +1,7 @@
{
- "schema_version": 6,
+ "schema_version": 7,
"capabilities": [
- {"id": "hook_control", "matrix": "agent_platform"},
+ {"id": "action_control", "matrix": "agent_platform"},
{"id": "consumer_delivery", "matrix": "agent_platform"},
{"id": "explicit_request", "matrix": "platform"}
],
@@ -14,6 +14,7 @@
{"id": "claude", "label": "Claude", "version": "2.1.217", "executable": "claude", "installer": {"kind": "npm", "package": "@anthropic-ai/claude-code"}, "integration": "native_command_hook", "artifacts": [], "runtime": {"ubuntu": "native", "macos": "native", "windows": "native"}, "driver": "real_agent_driver.py", "required_mode": "real_cli"},
{"id": "cursor", "label": "Cursor", "version": "2026.07.20-8cc9c0b", "executable": "agent", "installer": {"kind": "cursor_release", "package": "https://downloads.cursor.com/lab/2026.07.20-8cc9c0b"}, "integration": "native_command_hook", "artifacts": ["integrations/cursor/beforeShellExecution"], "runtime": {"ubuntu": "native", "macos": "native", "windows": "wsl"}, "driver": "real_agent_driver.py", "required_mode": "real_cli"},
{"id": "codex", "label": "Codex", "version": "0.145.0", "executable": "codex", "installer": {"kind": "npm", "package": "@openai/codex"}, "integration": "native_command_hook", "artifacts": ["integrations/codex/PreToolUse.ps1"], "runtime": {"ubuntu": "native", "macos": "native", "windows": "native"}, "driver": "real_agent_driver.py", "required_mode": "real_cli"},
+ {"id": "devin", "label": "Devin", "version": "3000.3.22", "executable": "devin", "installer": {"kind": "devin_release", "package": "https://static.devin.ai/cli"}, "integration": "acp_client", "artifacts": [], "runtime": {"ubuntu": "native", "macos": "native", "windows": "native"}, "driver": "real_agent_driver.py", "required_mode": "real_cli"},
{"id": "copilot", "label": "GitHub Copilot CLI", "version": "1.0.73", "executable": "copilot", "installer": {"kind": "npm", "package": "@github/copilot"}, "integration": "native_command_hook", "artifacts": ["integrations/copilot/PreToolUse", "integrations/copilot/PreToolUse.ps1"], "runtime": {"ubuntu": "native", "macos": "native", "windows": "native"}, "driver": "real_agent_driver.py", "required_mode": "real_cli"},
{"id": "gemini", "label": "Gemini", "version": "0.51.0", "executable": "gemini", "installer": {"kind": "npm", "package": "@google/gemini-cli"}, "integration": "native_command_hook", "artifacts": ["integrations/gemini/BeforeTool", "integrations/gemini/BeforeTool.ps1"], "runtime": {"ubuntu": "native", "macos": "native", "windows": "native"}, "driver": "real_agent_driver.py", "required_mode": "real_cli"},
{"id": "kimi", "label": "Kimi Code", "version": "0.29.0", "executable": "kimi", "installer": {"kind": "kimi_release", "package": "https://code.kimi.com/kimi-code"}, "integration": "native_command_hook", "artifacts": [], "runtime": {"ubuntu": "native", "macos": "native", "windows": "native"}, "driver": "real_agent_driver.py", "required_mode": "real_cli"},
diff --git a/labs/15-pitot/pitot-distribution/UPSTREAM.json b/labs/15-pitot/pitot-distribution/UPSTREAM.json
index d31a1190a..7f648e9a4 100644
--- a/labs/15-pitot/pitot-distribution/UPSTREAM.json
+++ b/labs/15-pitot/pitot-distribution/UPSTREAM.json
@@ -1,11 +1,11 @@
{
"files": {
".goreleaser.yml": "d375f187624219e9420acd2a40cfae95772ef1fee3abda3e729d6a16ba612032",
- "CONTRIBUTING.md": "23728d8a132d62b8adfb2e5c3eb9d9bfcf8a4d04543765b1e22ad8d55424af8f",
+ "CONTRIBUTING.md": "02c89a5790f2943e5e2466b0d260210238cae306922f0f031adb5f7028b48066",
"LICENSE": "bd17d47aefaa7406616179288058001b6532881a2517254fe668d7e9c4965cfb",
- "README.md": "dc3adbcf60ed946537353c1b873a8270c67dde11d7f71cf4cd361d13544f909e",
- "adapter-verification.json": "f8ad4e206571650f698826a8b66d8c00822be425e8d2de8ae98d98239e575eb4",
- "adapters/adapters.go": "1b46ba131fa3b2c93eed23526330275a3506451ba4bbd4f497e5378dfab2b6a8",
+ "README.md": "27df9b71b14ac474de3d03f2049eabb7d1c2c35514b7d351940683b0651d1984",
+ "adapter-verification.json": "fedd1624e01d19739114203674281296de779097e45fbb9e497047805b7d9250",
+ "adapters/adapters.go": "13cc5d94ea7c4ae50eccfd27d63e0d91b38cf76ee20aa9737a7edc9bd47d13b2",
"assets/pitot-boundary.png": "8a0ddb7d81831d94e14813f50ea4ca8670d77417f339ed2f91f0c653bf52f41d",
"assets/pitot-boundary.svg": "0c3871d70c84748573f231842091deb38a6def2862403ca34e8cc4493b9c9ebf",
"assets/pitot-hero.png": "a73532252b1e66c06273abbf5a4fe6261e98de3133b09e8d550edacfeeab92f8",
@@ -16,20 +16,22 @@
"bridge/bridge.go": "5adfcd3f743cae46e4446a6e030d53464ada97de0261a8588fa2a9fcd62136b8",
"bridge/bridge_test.go": "6dcc6d05f2b39c25955fc0b2d21d3d148dd9d77600fb12799941f86bdb1acb61",
"cmd/generate-schema/main.go": "6e9d0030290d99e36967433f96e38385a122974f899ad9421aac1ef7e50d8fcb",
- "cmd/pitot/doctor_host.go": "3eb21fb3a9b42fa7f2a33ed2ed82ae6de68eb2d084ecef4a9e0dce36e37d8a31",
+ "cmd/pitot/devin_acp.go": "b10962cc21239d0b3955d2521f1f4d940c5de34624d14ce89ef0e40e1fbc1138",
+ "cmd/pitot/devin_acp_test.go": "158a9ee019e8368b9f9434000eea8cbeee98201203213ef83797102549e1dd0a",
+ "cmd/pitot/doctor_host.go": "fa7819448218c3dd92d3393df9a0ac6e5aa70803348898eec10220bc7b3867a4",
"cmd/pitot/doctor_host_test.go": "4e6e327f6cf27cf94a0a608e10eb6790d6c11fcd53e6dfd7370007190749952f",
"cmd/pitot/install.go": "46cab87809566f8da47c4d71b8a87618d06cb0829bdabe1c8f734faf53a9f46c",
"cmd/pitot/kimi_control_test.go": "27b38867d4799636a664e3b1726ef55568f5dfd6a9be11f0ce5eca9931d759a4",
"cmd/pitot/kimi_smoke_test.go": "01cbf18312902cac42ec1f2547d35362c2bc920938acea508ba7c7f0638a9473",
- "cmd/pitot/main.go": "be069ed59ffe5e8cde26e3b88640dd8ae876fa0c266281ab9ac206be3e8944f1",
- "cmd/pitot/main_test.go": "6a97e930a83d15a51713530c092a7e7146c4cb4e4d7eb9f58b97d1577c986c6b",
+ "cmd/pitot/main.go": "9b7851b4e91c3eef08d6dac82939c1842f8e1a5237bf603e5a6ccc51a50561ec",
+ "cmd/pitot/main_test.go": "35c17c8de7caf13d0ed399c0b67903236cd3c64faa1ea134c5445d00887a32a5",
"cmd/pitot/shim.go": "3a7a4be5bd769010ad947de78188dd17c729388dbf74153d4dbd51fba2121ab1",
"cmd/pitot/substrate_test.go": "b4e7d83aa592f7b5bf4ec059ec552abd62473e51a6634a26428448168898d0a3",
"cmd/pitot/upgrade.go": "2ad6f581318c91c2617e1a65409458cd4de6641c940554b232dbdfd65e0fd221",
"cmd/pitot/version.go": "60ddbabcc8985c5137d5b73e3ba1be0f62fbb9db2a6612027e08314dfd82d618",
- "cmd/pitot/wire.go": "56c4d410eab4974952c5d7b7c37ad739341ebf292bb21de6143011c0bb1b1f48",
+ "cmd/pitot/wire.go": "e9181ef4e7133b35e263db69baac4be6ee33d6ee1ea36ab9749a2c630e5e2259",
"cmd/pitot/wire_test.go": "66a3c3302780145ebf12da3df1390f61a8d9b82f30b03f6332dd958799e70f7f",
- "cmd/pitot/workbench.go": "aa0a270829cdaa2bc7bf84d39a88f703088a9dc1c55057f7a2fd369ba98c9597",
+ "cmd/pitot/workbench.go": "614ec10f70542c5bbf65583e8ee94bb236e9757ad73f1d5cb46cdca08511182d",
"cmd/pitot/workbench_build_test.go": "8d5c5c35e8cbd59e21cfdd7e206d6b5b769892ab27a99fe5d87b13b1a31d0714",
"cmd/pitot/workbench_contract_test.go": "5e465f3d3f8b93ffaacfb4738279d369b13514bf7613b8611952f3cd26896586",
"cmd/pitot/workbench_dev_test.go": "abffe81e1a25f086d7f3c2f1c32986ee93618bbfc87220bdfda840ec6c6466bf",
@@ -43,6 +45,8 @@
"conformance/fixtures/negative.jsonl": "503ea76988df595d96ebf695f991b8ea6c892be4a578522dff4ddb0d39b647e4",
"conformance/fixtures/positive.jsonl": "881efdf58b66ee7d03171c6b4410bf1bce9e1b8db5c8bf969d0e8ec467420c3f",
"doc.go": "a8abdafac969b1bf4372c8bb023aa51125dc073f03218f4ab9913dfc5ffa877d",
+ "docs/devin-adapter-research.md": "687993d733b3f57a3be539fb468e12a79f851d3039958369b97bd68d375ad7e1",
+ "docs/evidence/devin-acp-3000.3.22.json": "4b4f0a386b56e586eeef5a95226a9a15420b2550893d3ba7a777714fdb7705c7",
"e2e/e2e_coverage_test.go": "09a738ca70749be94c462ce9974cdac9a7be0b24b9dc2b20ec0738d4686ebfbf",
"e2e/e2e_hook_test.go": "5e184dc8907b6e36daeab90bbbb1654fa5336312866412031805a13ba535d1f8",
"examples/doc.go": "58f3f9eb7d272d7b6eecdb05f43e1613d5e3ef92d15d97c5440bd4b6990c26f9",
@@ -63,12 +67,14 @@
"integrations/pi/pitot.ts": "ed2d60d5ab6e33e115cfa058e4f96095100e93a061567d0af31249aa756bab3e",
"integrations/qwen/PreToolUse": "95c358620f2f882bb8680e6aa9639f3b36a8567a3fd60cf14a6cdf3b3fbcf78b",
"integrations/qwen/PreToolUse.cjs": "e9bf00bbbee5c15f01ea203d0992b34f8754b75a9c9889eecb16baeb10fcce6c",
+ "internal/devinacp/client.go": "e2e70755d5888a2bd34e44b160d1dbf955fe147adf020050aa3fbc68cdbe709d",
+ "internal/devinacp/client_test.go": "5bfff9b7f29d253450e4424ab44dab97877ddb39b3941607a4e264dd66da32cb",
"internal/testrole/main.go": "6d657eb85d8ddaff0ae5a3da281aa7aa0a4bdda860d179e99508110033765787",
"internal/testrole/main_test.go": "c7fbc4905bcef7d662c9e162a4c32c8a86d5c1a97820e9f4ee48f643c4e47a38",
"projection/projection.go": "4d3c823fd72a3ca5387dba3683838a1d7e455e9b18309acc839763a39b7bb35f",
"protocol/framing.go": "d4409314b72e09cfd472ad9a21d4c7a223b4341b26f58f6062a7c899e3f87482",
"protocol/framing_test.go": "9fcb13767fdcc7129d2c87bc2133a5800c69d6ac0166016dc266dca5fcfaa1c5",
- "runtime/capabilities.go": "76c27bdd7ddfa11a1d639915ac2eb2d573b7ccdd687a5f548b3731b7c1e1828f",
+ "runtime/capabilities.go": "cf6370334a16a64f2b78b5e9d6b7e1d479c18a8b57610cbf14aa2fd7001b787b",
"runtime/descriptor_unix.go": "df41b6867e9840933f186c93b6bc61861e7ea5252a365455686ea0414bfa0044",
"runtime/descriptor_windows.go": "2d9ffefe3af0154fa8042de6b67460d4e86dd3f4cdd9e986f180f7d0c535c9a5",
"runtime/request.go": "198c44fd6c547022a15b6d0d48e4d0130fa8afb687994365115576e4d874550d",
@@ -106,30 +112,32 @@
"sdk/typescript/src/runner.ts": "c58babd3ec3996a05988f3cb7dec061cf7c48f41cd5125625be3408cb9b207f0",
"sdk/typescript/tsconfig.json": "de0065da9acf19a93ba8646c90b7101dfd5a8667552d3d3c59e3effd813218aa",
"sensor/decode_fuzz_test.go": "d27f2fbbc069eded26a73c9cd9bace98dd8a9e34949576790b81a08d130fbaf2",
- "sensor/sensor.go": "5c503d07ac33e7894d635f2d98bcd6d165d442d60c127ed6aeac80ec319086c5",
- "sensor/sensor_test.go": "4ddbdde3e486c9e189a2ed4174ad413df107d43dc98f5242da3667a24c7a5da1",
+ "sensor/sensor.go": "498d4f69c9243a409b25704beab9d441bb312c7813d168138b85a69cff08d8e9",
+ "sensor/sensor_test.go": "9e0ec50ec3930c37b0e4cc56419be451fb56432f0f22efa145b55ddf6440cd4b",
"tests/cursor_control_proxy.mjs": "ab532aa56a9299f497f3ceeedb4b6a0beb26089b7c022ed774385dd149ea56e6",
+ "tests/devin_control_proxy.py": "0be724db777bb790d18c49fad6280ddf77c261f7ae3c947323cc0cc20aab0422",
"tests/e2e_claude_cli_test.sh": "b28c4d1963e326b4b3f158a7cfc1b92771e768ac9665dd43e8a339013cc11568",
"tests/e2e_codex_cli_test.sh": "dbae5224c87410a5a5d67023d7e23406453bc5493d981af9347993b4e1f562f2",
"tests/e2e_copilot_cli_test.sh": "61b1e44dcd598d2d33e7f04dec26bec74405a9e2456ecda46e94dc8d43ad4315",
"tests/e2e_cursor_cli_test.sh": "af46f1a40a535ed1f345ee51cba89a57fa912d6b67bed7df9bae0943131fa542",
+ "tests/e2e_devin_cli_test.sh": "337aa3fb41a9661a6a1a69edbee67f90a7b65f493c52054bbe8c018c342f66fd",
"tests/e2e_gemini_cli_test.sh": "42cd77c366c17de092ec46324b3fa764c08b78de0e14f0d9f62b998c2939d6e0",
"tests/e2e_kimi_cli_test.sh": "3689474b027ca2d83ae36c4c3372e93c555477b69d5d81507a8aa6aa1e88edb1",
"tests/e2e_opencode_cli_test.sh": "e74a781f36b8263f2dd0f9700f3eea7a443b289fb87b55af455ce64509e08546",
"tests/e2e_pi_cli_test.sh": "369707019ce0ee9ccb91ac1b9a16d5b8ae357fc9277ccb3407bcd7bd05aa86c0",
"tests/e2e_qwen_cli_test.sh": "8813610ff0ae253e59849f76a5dc6ecf0990c150ea0ede60ec833d36e6e4a68f",
"tests/e2e_runtime_cli_test.sh": "54f64c3ef21eb62aa08dd7cb11d288be805d14a248c5baf7c172babd0957b4c9",
- "tests/e2e_unified_runner.sh": "bcad531c79b0840220d9353dbb64358ecddedd868f5334178944593926550c74",
- "tests/endpoint-provenance.json": "3b9c5a2acab964d49a0c534e5dba01258e7f56708920a6463cf521b0000a446e",
- "tests/install_real_agent.py": "999cb3326468998f0474431afe6b5b5dbd21752f76e68600bc03adb0d49484b9",
+ "tests/e2e_unified_runner.sh": "c9c79e27f8daa4ff15399ed7ee892d821b4f4aa34af91dbccf57bf9031210b75",
+ "tests/endpoint-provenance.json": "6d817e51680eb833ec41e21982a6931393ff76df06551cf9b47880cc5b1f1796",
+ "tests/install_real_agent.py": "a8dc79bf0914ed5ab2a82c4f2471e805f24eef2adde2261fee22dbd5aabeb985",
"tests/mock_anthropic_server.js": "ecebea62f9e93791a79f1ae3dd3c67b8fa42490e9805b23b662b877edfdb0f0e",
"tests/model_control_proxy.py": "cd4733ab20c16770fe3bf6ac477935f7dc285cd89be020848ae4798cb509e5f2",
- "tests/real_agent_driver.py": "8c57b50823be6bbf58a39624ef5661a4270a04d82b5ee7909280c9efa10344fd",
- "tests/run_e2e_report.py": "e75da4aac49b70832e6ea98f295d6ebd9ef909538614136665c74f085220b1a5",
+ "tests/real_agent_driver.py": "789e72797137fedcc7afc61f14a943df3f57e02f0a2329d125beeb8ce9311394",
+ "tests/run_e2e_report.py": "a72cbc486c100b75fccf6ac787da1736e13426b44a15d9d5dddcb8177bbaf596",
"tests/runtime_capability_driver.py": "54a485fc4f16981f2542d0dbd903a12b80f31f071908a378607f26678aeea07f",
"tests/witness/main.go": "cd56bbd00aa44cc5baf6426c8461a8ebca4a8391518f6acfa2301ac36add7c5f",
"windtunnel/doc.go": "44e0bcde632da73e1f8b98beade3a34ca8e0d0ea79cdfb91d131de290b164fc4",
- "windtunnel/windtunnel_test.go": "d34929ffdb1927b2ee27cc79640b333b0571659d231977b91c3a8898cf79bc42",
+ "windtunnel/windtunnel_test.go": "30f9d99d8f0bb47439585cb1db8c576b724e940c58a9fb95a0fff735a0c930d8",
"wiring/wiring.go": "2cdf286fb26def02c3bad1d6c7e7e4419d366c732faf55ad3bb02a4ae127acdf",
"wiring/wiring_test.go": "ade8b847ecdcbee17439e27d186c7078f39490716df5b56badd43a2a05f01141"
},
diff --git a/labs/15-pitot/pitot-distribution/release-notes/2026-07-31-devin-acp-support.md b/labs/15-pitot/pitot-distribution/release-notes/2026-07-31-devin-acp-support.md
new file mode 100644
index 000000000..0b05a711b
--- /dev/null
+++ b/labs/15-pitot/pitot-distribution/release-notes/2026-07-31-devin-acp-support.md
@@ -0,0 +1,14 @@
+### Devin ACP support
+
+Pitot adds Devin CLI `3000.3.22` as its tenth supervised adapter. Devin uses a
+stateful ACP v1 client boundary instead of lifecycle hooks, while normalized
+events and Controller responses remain Pitot protocol v1.
+
+Use `pitot dev --host devin -- devin -p "
Every supervised adapter must pass a binary-observed prompt → real hook → projected Consumer → Controller allow/deny → tool-result loop on Ubuntu, macOS, and Windows.
+Every supervised adapter must pass a binary-observed prompt → real action boundary → projected Consumer → Controller allow/deny → tool-result loop on Ubuntu, macOS, and Windows.
-Supervised adapters: Claude · Cursor · Codex · GitHub Copilot CLI · Gemini · Kimi Code · OpenCode · Pi · Qwen Code
+Supervised adapters: Claude · Cursor · Codex · Devin · GitHub Copilot CLI · Gemini · Kimi Code · OpenCode · Pi · Qwen Code
-Supervised runtime capabilities: hook control · Consumer delivery · explicit request
+Supervised runtime capabilities: action control · Consumer delivery · explicit request
@@ -78,6 +78,27 @@ Kimi as the blocked tool result. The `shell-policy` sample is a demonstration
tripwire, not a general shell-security control; the point is that *your* code
made the decision.
+### Devin uses ACP
+
+Devin exposes the same Pitot control semantics through a stateful ACP boundary,
+so it does not need lifecycle-hook files:
+
+```bash
+pitot doctor --host devin
+pitot dev --host devin -- devin -p "Run: echo hello"
+```
+
+To attach to an already-running Pitot runtime, use the explicit single-prompt
+surface:
+
+```bash
+pitot acp devin --runtime "$PITOT_RUNTIME" --prompt "Run: echo hello"
+```
+
+Pitot selects only ACP's one-shot `allow_once` or `reject_once` options.
+Interactive, resume, and multi-turn Devin sessions are not supported in this
+initial adapter.
+
## Why Pitot?
Building above coding agents usually forces one of two expensive choices:
diff --git a/labs/15-pitot/publish.config.json b/labs/15-pitot/publish.config.json
index 1822513ba..d62054b35 100644
--- a/labs/15-pitot/publish.config.json
+++ b/labs/15-pitot/publish.config.json
@@ -82,6 +82,7 @@
"mock_anthropic_server.js",
"model_control_proxy.py",
"cursor_control_proxy.mjs",
+ "devin_control_proxy.py",
"endpoint-provenance.json",
"real_agent_driver.py",
"run_e2e_report.py",
@@ -109,4 +110,4 @@
"import_boundary": "scripts/check_import_boundary.sh",
"extra_lab_jobs": ".labkit/lab-jobs.yml"
}
-}
\ No newline at end of file
+}
diff --git a/labs/15-pitot/scripts/build_pitot.py b/labs/15-pitot/scripts/build_pitot.py
index ca4d2ba0e..39a042fba 100644
--- a/labs/15-pitot/scripts/build_pitot.py
+++ b/labs/15-pitot/scripts/build_pitot.py
@@ -90,7 +90,7 @@ def _iter_e2e_harness_files(repo: Path) -> list[tuple[str, Path]]:
path
for name in (
"e2e_unified_runner.sh", "install_real_agent.py", "mock_anthropic_server.js", "model_control_proxy.py",
- "cursor_control_proxy.mjs", "endpoint-provenance.json", "real_agent_driver.py", "run_e2e_report.py",
+ "cursor_control_proxy.mjs", "devin_control_proxy.py", "endpoint-provenance.json", "real_agent_driver.py", "run_e2e_report.py",
"runtime_capability_driver.py",
)
if (path := root / name).is_file()
diff --git a/labs/15-pitot/scripts/pitot_adapter_supervisor.py b/labs/15-pitot/scripts/pitot_adapter_supervisor.py
index 86aacd4b7..726649056 100644
--- a/labs/15-pitot/scripts/pitot_adapter_supervisor.py
+++ b/labs/15-pitot/scripts/pitot_adapter_supervisor.py
@@ -24,9 +24,9 @@
ID_PATTERN = re.compile(r"^[a-z][a-z0-9-]*$")
LABEL_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9 .+_-]{0,39}$")
VERSION_PATTERN = re.compile(r"^(?:[0-9]+\.){2}[0-9]+(?:-[0-9A-Za-z.-]+)?$")
-DIALECTS = {"anthropic_messages", "openai_chat", "openai_responses", "gemini_generate_content", "cursor_connect_proto"}
-INTEGRATIONS = {"native_command_hook", "pi_extension", "opencode_plugin"}
-INSTALLERS = {"npm", "kimi_release", "cursor_release"}
+DIALECTS = {"anthropic_messages", "openai_chat", "openai_responses", "gemini_generate_content", "cursor_connect_proto", "devin_connect_proto"}
+INTEGRATIONS = {"native_command_hook", "pi_extension", "opencode_plugin", "acp_client"}
+INSTALLERS = {"npm", "kimi_release", "cursor_release", "devin_release"}
class ContractError(ValueError):
@@ -46,15 +46,15 @@ def load_manifest(root: Path = ROOT) -> dict[str, object]:
def validate_manifest(value: object) -> None:
if not isinstance(value, dict) or set(value) != {"schema_version", "capabilities", "platforms", "agents"}:
raise ContractError("manifest must contain schema_version, capabilities, platforms, and agents")
- if value["schema_version"] != 6:
+ if value["schema_version"] != 7:
raise ContractError("unsupported manifest schema_version")
expected_capabilities = [
- {"id": "hook_control", "matrix": "agent_platform"},
+ {"id": "action_control", "matrix": "agent_platform"},
{"id": "consumer_delivery", "matrix": "agent_platform"},
{"id": "explicit_request", "matrix": "platform"},
]
if value["capabilities"] != expected_capabilities:
- raise ContractError("capabilities must exactly supervise hook control, Consumer delivery, and explicit request")
+ raise ContractError("capabilities must exactly supervise action control, Consumer delivery, and explicit request")
platforms = value["platforms"]
agents = value["agents"]
if not isinstance(platforms, list) or not isinstance(agents, list) or not agents:
@@ -172,7 +172,11 @@ def validate_endpoint_value(value: object, manifest: dict[str, object]) -> None:
raise ContractError(f"endpoint fixture {agent_id}/{platform} has an invalid redacted request shape")
if cell["dialect"] == "cursor_connect_proto" and (request["transport"], request["framing"]) != ("http2", "connect_envelope"):
raise ContractError(f"endpoint fixture {agent_id}/{platform} has invalid Cursor wire provenance")
- if cell["dialect"] != "cursor_connect_proto" and request["media_type"] != "application/json":
+ if cell["dialect"] == "devin_connect_proto" and (request["transport"], request["framing"]) != ("http1", "connect_envelope"):
+ raise ContractError(f"endpoint fixture {agent_id}/{platform} has invalid Devin wire provenance")
+ if cell["dialect"] in {"cursor_connect_proto", "devin_connect_proto"} and request["media_type"] != "application/connect+proto":
+ raise ContractError(f"endpoint fixture {agent_id}/{platform} has invalid Connect wire provenance")
+ if cell["dialect"] not in {"cursor_connect_proto", "devin_connect_proto"} and request["media_type"] != "application/json":
raise ContractError(f"endpoint fixture {agent_id}/{platform} has invalid JSON wire provenance")
if response != {"encoder": cell["dialect"], "framing": request["framing"], "tool_call": "native_shell", "acceptance": "nonce_tool_result_round_trip"}:
raise ContractError(f"endpoint fixture {agent_id}/{platform} lacks accepted response provenance")
@@ -241,11 +245,11 @@ def readme_block(manifest: dict[str, object]) -> str:
' ',
"
Every supervised adapter must pass a binary-observed prompt → real hook → projected Consumer → Controller allow/deny → tool-result loop on Ubuntu, macOS, and Windows.
', + 'Every supervised adapter must pass a binary-observed prompt → real action boundary → projected Consumer → Controller allow/deny → tool-result loop on Ubuntu, macOS, and Windows.
', "", f'Supervised adapters: {labels}
', "", - 'Supervised runtime capabilities: hook control · Consumer delivery · explicit request
', + 'Supervised runtime capabilities: action control · Consumer delivery · explicit request
', README_END, ] ) @@ -442,6 +446,9 @@ def contract_errors(root: Path, adapters: list[str] | None = None) -> list[str]: cursor_proxy = root / "labs/15-pitot/tests/cursor_control_proxy.mjs" if not cursor_proxy.is_file(): errors.append("missing pinned Cursor endpoint proxy") + devin_proxy = root / "labs/15-pitot/tests/devin_control_proxy.py" + if not devin_proxy.is_file(): + errors.append("missing pinned Devin endpoint proxy") reporter_path = root / ".github/scripts/pitot_e2e_report.py" reporter_text = reporter_path.read_text(encoding="utf-8") if reporter_path.is_file() else "" if "artifacts?per_page=100" not in reporter_text or "total != len(artifacts)" not in reporter_text: diff --git a/labs/15-pitot/tests/devin_control_proxy.py b/labs/15-pitot/tests/devin_control_proxy.py new file mode 100644 index 000000000..64633972e --- /dev/null +++ b/labs/15-pitot/tests/devin_control_proxy.py @@ -0,0 +1,254 @@ +#!/usr/bin/env python3 +"""Deterministic Connect/protobuf model endpoint for released Devin ACP E2E.""" + +from __future__ import annotations + +import argparse +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +import json +from pathlib import Path +import threading +import time + + +def varint(value: int) -> bytes: + output = bytearray() + while value > 127: + output.append((value & 127) | 128) + value >>= 7 + output.append(value) + return bytes(output) + + +def field_bytes(field: int, value: bytes) -> bytes: + return varint((field << 3) | 2) + varint(len(value)) + value + + +def field_string(field: int, value: str) -> bytes: + return field_bytes(field, value.encode()) + + +def field_enum(field: int, value: int) -> bytes: + return varint(field << 3) + varint(value) + + +def envelope(payload: bytes, flags: int = 0) -> bytes: + return bytes([flags]) + len(payload).to_bytes(4, "big") + payload + + +def tool_response(call_id: str, command: str) -> bytes: + call = field_string(1, call_id) + field_string(2, "exec") + field_string( + 3, json.dumps({"command": command}, separators=(",", ":")) + ) + message = field_string(1, f"msg-{call_id}") + field_enum(5, 10) + field_bytes(6, call) + return envelope(message) + envelope(b"{}", 2) + + +def text_response(text: str) -> bytes: + message = field_string(1, "pitot-final") + field_string(3, text) + field_enum(5, 2) + return envelope(message) + envelope(b"{}", 2) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--nonce", required=True) + parser.add_argument("--receipt", type=Path, required=True) + parser.add_argument("--ready-file", type=Path, required=True) + parser.add_argument("--canary-command", required=True) + parser.add_argument("--response-fault", choices=("none", "text"), default="none") + parser.add_argument("--request-log", type=Path, default=None) + args = parser.parse_args() + log_lock = threading.Lock() + + def rlog(line: str) -> None: + # Connection/request journal for CI triage: written to a file (never + # a pipe nobody drains) and printed by the driver after each phase. + if args.request_log is None: + return + with log_lock, open(args.request_log, "a", encoding="utf-8") as fh: + fh.write(f"{time.time():.6f} {line}\n") + fh.flush() + allow_id = f"pitot_tool_allow_{args.nonce}" + deny_id = f"pitot_tool_deny_{args.nonce}" + receipt: dict[str, object] = { + "schema_version": 1, + "agent": "devin", + "protocol": "devin_connect_proto", + "nonce": args.nonce, + "initial_prompt_observed": False, + "tool_call_response_emitted": False, + "tool_result_observed": False, + "allow_tool_call_response_emitted": False, + "allow_tool_result_observed": False, + "deny_tool_call_response_emitted": False, + "denied_result_observed": False, + "final_response_emitted": False, + "endpoint_observed": None, + "auxiliary_requests": 0, + } + + def save() -> None: + args.receipt.parent.mkdir(parents=True, exist_ok=True) + temporary = args.receipt.with_suffix(".tmp") + temporary.write_text(json.dumps(receipt, indent=2, sort_keys=True) + "\n", encoding="utf-8") + temporary.replace(args.receipt) + + class Handler(BaseHTTPRequestHandler): + # Devin's HTTP client pools connections. Under the default HTTP/1.0 + # the server closes the socket after every response, and a request + # written to the stale pooled connection can black-hole until the + # CLI's 10s team-settings deadline expires (observed on Windows). + # HTTP/1.1 keep-alive keeps pooled connections valid; every response + # below must therefore carry an exact content-length. + protocol_version = "HTTP/1.1" + + def log_message(self, *_: object) -> None: + return + + def setup(self) -> None: + super().setup() + rlog(f"CONN open client=:{self.client_address[1]}") + + def finish(self) -> None: + rlog(f"CONN close client=:{self.client_address[1]}") + super().finish() + + def do_GET(self) -> None: + rlog(f"REQ client=:{self.client_address[1]} GET {self.path}") + self.send_response(404) + self.send_header("content-length", "0") + self.end_headers() + rlog(f"RES client=:{self.client_address[1]} 404 GET {self.path}") + + def _read_body(self) -> bytes: + # Drain the request body exactly, including chunked framing: an + # unconsumed body on a kept-alive connection desyncs every later + # request the client sends on it. + if "chunked" in self.headers.get("transfer-encoding", "").lower(): + chunks = [] + while True: + size_line = self.rfile.readline(65537).split(b";", 1)[0].strip() + size = int(size_line or b"0", 16) + if size == 0: + while self.rfile.readline(65537) not in (b"\r\n", b"\n", b""): + pass # trailers + break + chunks.append(self.rfile.read(size)) + self.rfile.readline(65537) # CRLF after each chunk + return b"".join(chunks) + return self.rfile.read(int(self.headers.get("content-length", "0"))) + + def do_POST(self) -> None: + started = time.monotonic() + rlog( + f"REQ client=:{self.client_address[1]} POST {self.path} " + f"cl={self.headers.get('content-length', '-')} " + f"te={self.headers.get('transfer-encoding', '-')}" + ) + raw = self._read_body() + path = self.path.split("?", 1)[0] + content_type = self.headers.get("content-type", "").split(";", 1)[0].strip().lower() + if not path.endswith("/GetChatMessage"): + receipt["auxiliary_requests"] = int(receipt["auxiliary_requests"]) + 1 + save() + self.send_response(200) + self.send_header("content-type", "application/proto") + self.send_header("content-length", "0") + self.end_headers() + rlog( + f"RES client=:{self.client_address[1]} 200 {path} " + f"aux {int((time.monotonic() - started) * 1000)}ms" + ) + return + + lowered = raw.lower() + is_title = b"session title generator" in lowered + allow_result = f"PITOT_CANARY_RESULT PITOT_ALLOW {args.nonce}".encode() in raw + denied_result = deny_id.encode() in raw and any( + marker in lowered for marker in (b"reject", b"denied", b"permission") + ) + if is_title: + payload = text_response("Pitot E2E") + receipt["auxiliary_requests"] = int(receipt["auxiliary_requests"]) + 1 + elif not receipt["initial_prompt_observed"] and args.nonce.encode() in raw: + receipt["initial_prompt_observed"] = True + receipt["endpoint_observed"] = { + "transport": "http1", + "method": "POST", + "path": path, + "media_type": content_type, + "framing": "connect_envelope", + "request_shape": { + "service": "exa.api_server_pb.ApiServerService", + "method": "GetChatMessage", + "stream": "server", + "message": "GetChatMessageRequest", + }, + } + if args.response_fault == "text": + receipt["fault_response_emitted"] = "text" + payload = text_response(f"PITOT_E2E_COMPLETE {args.nonce}") + else: + payload = tool_response( + allow_id, f"{args.canary_command} PITOT_ALLOW {args.nonce}" + ) + receipt["tool_call_response_emitted"] = True + receipt["allow_tool_call_response_emitted"] = True + elif allow_result and not receipt["allow_tool_result_observed"]: + receipt["tool_result_observed"] = True + receipt["allow_tool_result_observed"] = True + receipt["deny_tool_call_response_emitted"] = True + payload = tool_response( + deny_id, f"{args.canary_command} PITOT_DENY {args.nonce}" + ) + elif denied_result and receipt["deny_tool_call_response_emitted"]: + receipt["denied_result_observed"] = True + receipt["final_response_emitted"] = True + payload = text_response(f"PITOT_E2E_COMPLETE {args.nonce}") + else: + receipt["unexpected_request"] = { + "path": path, + "bytes": len(raw), + "allow_result": allow_result, + "deny_call_id": deny_id.encode() in raw, + "rejection_marker": any( + marker in lowered for marker in (b"reject", b"denied", b"permission") + ), + } + save() + self.send_error(409, "request did not advance Devin control trajectory") + rlog(f"RES client=:{self.client_address[1]} 409 {path}") + return + save() + self.send_response(200) + self.send_header("content-type", "application/connect+proto") + self.send_header("content-length", str(len(payload))) + self.end_headers() + self.wfile.write(payload) + rlog( + f"RES client=:{self.client_address[1]} 200 {path} " + f"chat {int((time.monotonic() - started) * 1000)}ms" + ) + + class BurstTolerantServer(ThreadingHTTPServer): + # Devin bursts many concurrent connections at startup and session + # creation. socketserver's default listen backlog of 5 overflows on + # Windows, where an overflowed SYN is silently dropped and the + # client's retransmit schedule outlives the CLI's 10s team-settings + # deadline (or is RST -> instant ConnectionFailed). A deep backlog + # makes the local fetch path deterministic. + request_queue_size = 128 + daemon_threads = True + + server = BurstTolerantServer(("127.0.0.1", 0), Handler) + args.ready_file.write_text(f"http://127.0.0.1:{server.server_port}\n", encoding="utf-8") + save() + try: + server.serve_forever() + except KeyboardInterrupt: + return 0 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/labs/15-pitot/tests/e2e_devin_cli_test.sh b/labs/15-pitot/tests/e2e_devin_cli_test.sh new file mode 100755 index 000000000..437438365 --- /dev/null +++ b/labs/15-pitot/tests/e2e_devin_cli_test.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +exec "$(dirname "$0")/e2e_unified_runner.sh" "devin" diff --git a/labs/15-pitot/tests/e2e_unified_runner.sh b/labs/15-pitot/tests/e2e_unified_runner.sh index 7957d5ca9..bd78dfffd 100755 --- a/labs/15-pitot/tests/e2e_unified_runner.sh +++ b/labs/15-pitot/tests/e2e_unified_runner.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Real released-agent prompt-to-hook E2E. Direct hook invocation is forbidden. +# Real released-agent prompt-to-boundary E2E. Direct boundary invocation is forbidden. set -euo pipefail HOST="${1:-}" diff --git a/labs/15-pitot/tests/endpoint-provenance.json b/labs/15-pitot/tests/endpoint-provenance.json index 960e7c764..50ba0c73d 100644 --- a/labs/15-pitot/tests/endpoint-provenance.json +++ b/labs/15-pitot/tests/endpoint-provenance.json @@ -1277,6 +1277,93 @@ }, "runtime": "native", "version": "0.20.1" + }, + { + "agent": "devin", + "capture_sha256": "4e486e91fb33c8220d0772d0a9dc2552799b5af309c2fb8a73cf72f8cde1902b", + "dialect": "devin_connect_proto", + "executable_sha256": "c303dc3395149d946568f57e00bcea509b269ed361f6845eea38206f59113551", + "platform": "ubuntu", + "provenance": "pinned_real_cli_capture", + "request": { + "framing": "connect_envelope", + "media_type": "application/connect+proto", + "method": "POST", + "path": "/exa.api_server_pb.ApiServerService/GetChatMessage", + "request_shape": { + "message": "GetChatMessageRequest", + "method": "GetChatMessage", + "service": "exa.api_server_pb.ApiServerService", + "stream": "server" + }, + "transport": "http1" + }, + "response": { + "acceptance": "nonce_tool_result_round_trip", + "encoder": "devin_connect_proto", + "framing": "connect_envelope", + "tool_call": "native_shell" + }, + "runtime": "native", + "version": "3000.3.22" + }, + { + "agent": "devin", + "capture_sha256": "16888b9fe29244c7462d2637c55f3b1938375b4cac30bc978236c6726378c3dd", + "dialect": "devin_connect_proto", + "executable_sha256": "1252ab77359e1cc63c23fda7a88ff7711bab5f853c1ab6d3323cb319b71a60ee", + "platform": "macos", + "provenance": "pinned_real_cli_capture", + "request": { + "framing": "connect_envelope", + "media_type": "application/connect+proto", + "method": "POST", + "path": "/exa.api_server_pb.ApiServerService/GetChatMessage", + "request_shape": { + "message": "GetChatMessageRequest", + "method": "GetChatMessage", + "service": "exa.api_server_pb.ApiServerService", + "stream": "server" + }, + "transport": "http1" + }, + "response": { + "acceptance": "nonce_tool_result_round_trip", + "encoder": "devin_connect_proto", + "framing": "connect_envelope", + "tool_call": "native_shell" + }, + "runtime": "native", + "version": "3000.3.22" + }, + { + "agent": "devin", + "capture_sha256": "cf0add8cb81014d2bdefe14ce92e7e4b47b7ae40f22cf65ecff7696137b3411d", + "dialect": "devin_connect_proto", + "executable_sha256": "fd0901491ad7a42d8d2d7e0af6f1b53564bbc4e563a6bd3135111346c78e08f6", + "platform": "windows", + "provenance": "pinned_real_cli_capture", + "request": { + "framing": "connect_envelope", + "media_type": "application/connect+proto", + "method": "POST", + "path": "/exa.api_server_pb.ApiServerService/GetChatMessage", + "request_shape": { + "message": "GetChatMessageRequest", + "method": "GetChatMessage", + "service": "exa.api_server_pb.ApiServerService", + "stream": "server" + }, + "transport": "http1" + }, + "response": { + "acceptance": "nonce_tool_result_round_trip", + "encoder": "devin_connect_proto", + "framing": "connect_envelope", + "tool_call": "native_shell" + }, + "runtime": "native", + "version": "3000.3.22" } ], "schema_version": 2 diff --git a/labs/15-pitot/tests/install_real_agent.py b/labs/15-pitot/tests/install_real_agent.py index a8b1170c9..420073646 100755 --- a/labs/15-pitot/tests/install_real_agent.py +++ b/labs/15-pitot/tests/install_real_agent.py @@ -11,7 +11,10 @@ import platform as host_platform import shutil import subprocess +import tarfile import tempfile +import urllib.request +import zipfile LAB = Path(__file__).resolve().parent.parent @@ -89,6 +92,51 @@ def install(agent: dict[str, object], platform: str, runtime: str) -> None: temporary_link.unlink() temporary_link.symlink_to(installed) temporary_link.replace(link) + elif kind == "devin_release": + manifest_url = f"{package}/{version}/manifest.json" + with urllib.request.urlopen(manifest_url, timeout=30) as response: + manifest = json.loads(response.read()) + if manifest.get("version") != version: + raise RuntimeError("Devin release manifest version does not match the supervised pin") + machine = host_platform.machine().lower() + arch = "aarch64" if machine in {"arm64", "aarch64"} else "x86_64" + suffix = {"ubuntu": "unknown-linux", "macos": "apple-darwin", "windows": "pc-windows"}[platform] + target = f"{arch}-{suffix}" + release = manifest.get("platforms", {}).get(target) + if not isinstance(release, dict) or set(release) != {"url", "sha256"}: + raise RuntimeError(f"Devin release manifest omitted {target}") + with tempfile.TemporaryDirectory() as directory: + archive = Path(directory) / ("devin.zip" if platform == "windows" else "devin.tar.gz") + with urllib.request.urlopen(release["url"], timeout=120) as response: + archive.write_bytes(response.read()) + if hashlib.sha256(archive.read_bytes()).hexdigest() != release["sha256"]: + raise RuntimeError("Devin release archive SHA-256 mismatch") + executable_name = "devin.exe" if platform == "windows" else "devin" + if platform == "windows": + with zipfile.ZipFile(archive) as bundle: + matches = [name for name in bundle.namelist() if Path(name).name == executable_name] + if len(matches) != 1: + raise RuntimeError(f"Devin archive contained {len(matches)} executables") + executable_bytes = bundle.read(matches[0]) + else: + with tarfile.open(archive, "r:gz") as bundle: + matches = [member for member in bundle.getmembers() if member.isfile() and Path(member.name).name == executable_name] + if len(matches) != 1: + raise RuntimeError(f"Devin archive contained {len(matches)} executables") + source = bundle.extractfile(matches[0]) + if source is None: + raise RuntimeError("Devin executable could not be extracted") + executable_bytes = source.read() + installed_dir = Path.home() / ".local" / "share" / "pitot-devin" / version + installed_dir.mkdir(parents=True, exist_ok=True) + installed = installed_dir / executable_name + installed.write_bytes(executable_bytes) + installed.chmod(0o755) + destination = Path.home() / ".local" / "bin" + destination.mkdir(parents=True, exist_ok=True) + launcher = destination / executable_name + shutil.copy2(installed, launcher) + launcher.chmod(0o755) else: raise ValueError(f"unsupported installer: {kind}") diff --git a/labs/15-pitot/tests/real_agent_driver.py b/labs/15-pitot/tests/real_agent_driver.py index 28590db2d..0a49e2994 100755 --- a/labs/15-pitot/tests/real_agent_driver.py +++ b/labs/15-pitot/tests/real_agent_driver.py @@ -16,6 +16,7 @@ import sys import tempfile import time +import urllib.parse LAB = Path(__file__).resolve().parent.parent @@ -112,6 +113,30 @@ def configure( ) env["CODEX_HOME"] = str(home / ".codex") prompt_flag = ["exec", "--dangerously-bypass-approvals-and-sandbox", "--dangerously-bypass-hook-trust", "--model", "pitot-control"] + elif agent == "devin": + endpoint = urllib.parse.urlparse(proxy) + app_data = home / "AppData/Roaming" + local_app_data = home / "AppData/Local" + app_data.mkdir(parents=True, exist_ok=True) + local_app_data.mkdir(parents=True, exist_ok=True) + credentials = home / ".local/share/devin/credentials.toml" + credentials.parent.mkdir(parents=True, exist_ok=True) + credentials.write_text( + 'windsurf_api_key = "pitot-local-only"\n' + f"api_server_url = {json.dumps(proxy)}\n" + f"devin_webapp_host = {json.dumps(endpoint.netloc)}\n" + f"devin_api_url = {json.dumps(proxy)}\n", + encoding="utf-8", + ) + env.update({ + "WINDSURF_API_KEY": "pitot-local-only", + "WINDSURF_API_SERVER_URL": proxy, + "XDG_CONFIG_HOME": str(home / ".config"), + "XDG_DATA_HOME": str(home / ".local/share"), + "APPDATA": str(app_data), + "LOCALAPPDATA": str(local_app_data), + }) + prompt_flag = [] elif agent == "copilot": hooks = home / ".copilot/hooks" hooks.mkdir(parents=True, exist_ok=True) @@ -263,7 +288,7 @@ def capture_record(agent: dict[str, object], platform: str, installation: dict[s observed = proxy.get("endpoint_observed") dialect = proxy.get("protocol") if not isinstance(observed, dict) or dialect not in { - "anthropic_messages", "openai_chat", "openai_responses", "gemini_generate_content", "cursor_connect_proto", + "anthropic_messages", "openai_chat", "openai_responses", "gemini_generate_content", "cursor_connect_proto", "devin_connect_proto", }: raise RuntimeError("proxy did not binary-observe a supported request contract") digest = installation.get("executable_sha256") @@ -330,21 +355,29 @@ def validate_receipts( final_marker = f"PITOT_E2E_COMPLETE {nonce}" if exit_code != 0 or "hook: PreToolUse Failed" in output or final_marker not in output or not all(proxy.get(flag) is True for flag in proxy_flags): raise RuntimeError(f"agent loop incomplete (exit={exit_code}, proxy={proxy}, witnesses={witnesses})\n{output[-4000:]}") - if proxy.get("nonce") != nonce or len(witnesses) != 2: - raise RuntimeError("proxy and Pitot witness receipts do not identify exactly two hook actions") - if any(item.get("nonce") != nonce or item.get("host") != agent["id"] or item.get("valid") is not True for item in witnesses): - raise RuntimeError("Pitot hook witnesses escaped the nonce-bound real-agent session") - if [item.get("pitot_exit") for item in witnesses] != [0, 2]: - raise RuntimeError(f"real hook did not carry one allow and one deny: {witnesses}") - action_ids = [item.get("action_id") for item in witnesses] - if len(set(action_ids)) != 2 or not all(isinstance(item, str) and item.startswith("act_") for item in action_ids): - raise RuntimeError("hook actions lack unique Pitot correlation ids") + if proxy.get("nonce") != nonce: + raise RuntimeError("proxy receipt escaped the nonce-bound real-agent session") + if agent["integration"] == "acp_client": + if witnesses: + raise RuntimeError("ACP control unexpectedly entered the lifecycle-hook witness path") + else: + if len(witnesses) != 2: + raise RuntimeError("Pitot witness receipts do not identify exactly two hook actions") + if any(item.get("nonce") != nonce or item.get("host") != agent["id"] or item.get("valid") is not True for item in witnesses): + raise RuntimeError("Pitot hook witnesses escaped the nonce-bound real-agent session") + if [item.get("pitot_exit") for item in witnesses] != [0, 2]: + raise RuntimeError(f"real hook did not carry one allow and one deny: {witnesses}") requests = [item.get("value") for item in controller if item.get("receipt_type") == "request"] responses = [item.get("value") for item in controller if item.get("receipt_type") == "response"] if len(requests) != 2 or len(responses) != 2: - raise RuntimeError(f"Controller did not receive and resolve both hook actions: {controller}") + raise RuntimeError(f"Controller did not receive and resolve both boundary actions: {controller}") + action_ids = [item.get("action_id") for item in requests] + if len(set(action_ids)) != 2 or not all(isinstance(item, str) and item.startswith("act_") for item in action_ids): + raise RuntimeError("boundary actions lack unique Pitot correlation ids") + if witnesses and [item.get("action_id") for item in witnesses] != action_ids: + raise RuntimeError("hook correlation ids do not match the Controller actions") if [item.get("action_id") for item in requests] != action_ids or [item.get("action_id") for item in responses] != action_ids: - raise RuntimeError("Controller correlation ids do not match the real hook actions") + raise RuntimeError("Controller correlation ids do not match the real boundary actions") if [item.get("outcome") for item in responses] != ["allow", "deny"] or responses[1].get("message") != f"PITOT_CONTROLLER_DENY {nonce}": raise RuntimeError("external Controller did not produce the nonce-bound allow/deny trajectory") if len(consumers) != 2 or [item.get("action", {}).get("id") for item in consumers] != action_ids: @@ -382,7 +415,17 @@ def validate_receipts( "nonce": nonce, "receipts": {**{flag: True for flag in proxy_flags}, "consumer_observed": True, "controller_allow_observed": True, "controller_deny_observed": True, "deny_canary_absent": True, "final_output_observed": True, "cli_exit_zero": True}, "runtime": runtime_public, - "hooks": [{"host": item["host"], "action_kind": item["action_kind"], "action_id": item["action_id"], "pitot_exit": item["pitot_exit"], "nonce": item["nonce"]} for item in witnesses], + "boundaries": [ + { + "host": agent["id"], + "transport": "acp" if agent["integration"] == "acp_client" else "hook", + "action_kind": "shell", + "action_id": action_id, + "decision": outcome, + "nonce": nonce, + } + for action_id, outcome in zip(action_ids, ("allow", "deny"), strict=True) + ], "controller": {"id": "e2e-shell-controller", "action_ids": action_ids, "outcomes": ["allow", "deny"]}, "consumer": {"id": "e2e-audit", "action_ids": action_ids, "projection": "sha256"}, "canary": {"executions": canary, "denied_executions": 0}, @@ -442,6 +485,7 @@ def main() -> int: str(canary_executable), receipt_argument, windows_host() and not host_controls_wsl, ) ready, proxy_receipt, witness_receipt = base / "proxy.url", base / "proxy.json", base / "witness.jsonl" + proxy_request_log = base / "proxy-requests.log" runtime_descriptor = base / "runtime.json" runtime_config = base / "pitot.json" consumer_receipt = base / "consumer.jsonl" @@ -495,6 +539,8 @@ def main() -> int: ] else: proxy_command = ["node", str(LAB / "tests/cursor_control_proxy.mjs"), "--nonce", nonce, "--receipt", str(proxy_receipt), "--ready-file", str(ready), "--canary-command", canary_command, "--response-fault", args.response_fault] + elif args.agent == "devin": + proxy_command = [sys.executable, str(LAB / "tests/devin_control_proxy.py"), "--nonce", nonce, "--receipt", str(proxy_receipt), "--ready-file", str(ready), "--canary-command", canary_command, "--response-fault", args.response_fault, "--request-log", str(proxy_request_log)] else: proxy_command = [sys.executable, str(LAB / "tests/model_control_proxy.py"), "--agent", args.agent, "--nonce", nonce, "--receipt", str(proxy_receipt), "--ready-file", str(ready), "--canary-command", canary_command, "--response-fault", args.response_fault] proxy_process = subprocess.Popen( @@ -554,6 +600,14 @@ def main() -> int: "wsl.exe", "--distribution", "Ubuntu", "--cd", wsl_path(project), "--", "env", *assignments, executable, *flags, prompt, ] + elif args.agent == "devin": + command = [ + str(args.pitot.resolve()), "acp", "devin", + "--runtime", str(runtime_descriptor), + "--prompt", prompt, + "--exec", executable, + "--cwd", str(project), + ] else: command = [executable, "-p", prompt, *flags] if args.agent in {"copilot", "kimi"} else [executable, *flags, prompt] try: @@ -571,6 +625,14 @@ def main() -> int: # reporting layer cannot fail after a successful agent session. sys.stdout.buffer.write(completed.stdout.encode("utf-8", errors="replace")) sys.stdout.buffer.flush() + if args.agent == "devin" and proxy_request_log.is_file(): + # Connection-level journal from the proxy: shows whether a + # hanging fetch ever reached the server and on which socket. + journal = proxy_request_log.read_text(encoding="utf-8", errors="replace") + sys.stdout.buffer.write( + ("PITOT_PROXY_REQUEST_LOG begin\n" + journal[-12000:] + "PITOT_PROXY_REQUEST_LOG end\n").encode("utf-8", errors="replace") + ) + sys.stdout.buffer.flush() if args.expect_incompatible_response: observed = json.loads(proxy_receipt.read_text(encoding="utf-8")) if proxy_receipt.is_file() else {} if not ( diff --git a/labs/15-pitot/tests/run_e2e_report.py b/labs/15-pitot/tests/run_e2e_report.py index 5e34938d7..2e149dc70 100755 --- a/labs/15-pitot/tests/run_e2e_report.py +++ b/labs/15-pitot/tests/run_e2e_report.py @@ -16,6 +16,7 @@ ROOT = LAB.parents[1] if LAB.name == "15-pitot" else LAB MANIFEST = json.loads((LAB / "adapter-verification.json").read_text(encoding="utf-8")) AGENTS = {agent["id"] for agent in MANIFEST["agents"]} +AGENT_RECORDS = {agent["id"]: agent for agent in MANIFEST["agents"]} PLATFORMS = {platform["id"] for platform in MANIFEST["platforms"]} RESULT_PATTERN = re.compile(r"^PITOT_E2E_RESULT mode=real_cli evidence=nonce-correlated$", re.MULTILINE) RUNTIME_RESULT_PATTERN = re.compile(r"^PITOT_RUNTIME_E2E_RESULT capability=explicit_request evidence=nonce-correlated$", re.MULTILINE) @@ -25,7 +26,7 @@ def load_evidence(path: Path | None, *, agent: str) -> dict[str, object] | None: if path is None or not path.is_file(): return None value = json.loads(path.read_text(encoding="utf-8")) - required = {"schema_version", "agent", "cli", "prompt_hash", "protocol", "endpoint", "nonce", "receipts", "runtime", "hooks", "controller", "consumer", "canary"} + required = {"schema_version", "agent", "cli", "prompt_hash", "protocol", "endpoint", "nonce", "receipts", "runtime", "boundaries", "controller", "consumer", "canary"} if not isinstance(value, dict) or set(value) != required or value["schema_version"] != 2 or value["agent"] != agent: return None receipts = value.get("receipts") @@ -42,8 +43,9 @@ def load_evidence(path: Path | None, *, agent: str) -> dict[str, object] | None: nonce = value.get("nonce") if not isinstance(nonce, str) or not re.fullmatch(r"[0-9a-f]{32}", nonce): return None - hooks = value.get("hooks") - if not isinstance(hooks, list) or len(hooks) != 2 or [item.get("pitot_exit") for item in hooks] != [0, 2] or any(item.get("action_kind") != "shell" or item.get("host") != agent or item.get("nonce") != nonce for item in hooks): + boundaries = value.get("boundaries") + expected_transport = "acp" if AGENT_RECORDS[agent]["integration"] == "acp_client" else "hook" + if not isinstance(boundaries, list) or len(boundaries) != 2 or [item.get("decision") for item in boundaries] != ["allow", "deny"] or any(item.get("action_kind") != "shell" or item.get("host") != agent or item.get("nonce") != nonce or item.get("transport") != expected_transport for item in boundaries): return None endpoint = value.get("endpoint", {}) endpoint_required = {"fixture", "fixture_sha256", "provenance", "dialect", "request", "response", "executable_sha256"} @@ -81,7 +83,7 @@ def result_for(agent: str, platform: str, returncode: int, output: str, evidence markers = RESULT_PATTERN.findall(output) receipt = load_evidence(evidence_path, agent=agent) passed = returncode == 0 and len(markers) == 1 and receipt is not None - evidence = "binary-observed request, real hook control, projected Consumer, allow/deny canary, and final receipts" if passed else "real-agent control evidence contract failed" + evidence = "binary-observed request, real action control, projected Consumer, allow/deny canary, and final receipts" if passed else "real-agent control evidence contract failed" return { "schema_version": 2, @@ -97,7 +99,7 @@ def result_for(agent: str, platform: str, returncode: int, output: str, evidence "nonce": receipt["nonce"] if passed else None, "receipts": receipt["receipts"] if passed else None, "runtime": receipt["runtime"] if passed else None, - "hooks": receipt["hooks"] if passed else None, + "boundaries": receipt["boundaries"] if passed else None, "controller": receipt["controller"] if passed else None, "consumer": receipt["consumer"] if passed else None, "canary": receipt["canary"] if passed else None, diff --git a/labs/15-pitot/tests/test_adapter_supervisor.py b/labs/15-pitot/tests/test_adapter_supervisor.py index 2cc15fa67..afd88a1ae 100644 --- a/labs/15-pitot/tests/test_adapter_supervisor.py +++ b/labs/15-pitot/tests/test_adapter_supervisor.py @@ -24,14 +24,14 @@ def _manifest() -> dict[str, object]: def _provenance(manifest: dict[str, object]) -> dict[str, object]: - dialects = {"claude": "anthropic_messages", "codex": "openai_responses", "gemini": "gemini_generate_content", "cursor": "cursor_connect_proto"} - paths = {"anthropic_messages": "/v1/messages", "openai_responses": "/v1/responses", "gemini_generate_content": "/v1beta/models/pitot-control:streamGenerateContent", "cursor_connect_proto": "/agent.v1.AgentService/Run", "openai_chat": "/v1/chat/completions"} + dialects = {"claude": "anthropic_messages", "codex": "openai_responses", "gemini": "gemini_generate_content", "cursor": "cursor_connect_proto", "devin": "devin_connect_proto"} + paths = {"anthropic_messages": "/v1/messages", "openai_responses": "/v1/responses", "gemini_generate_content": "/v1beta/models/pitot-control:streamGenerateContent", "cursor_connect_proto": "/agent.v1.AgentService/Run", "devin_connect_proto": "/connectrpc.devin.api.ModelService/StreamResponse", "openai_chat": "/v1/chat/completions"} cells = [] for agent in manifest["agents"]: for platform in manifest["platforms"]: dialect = dialects.get(agent["id"], "openai_chat") - framing = "connect_envelope" if dialect == "cursor_connect_proto" else "sse" - request = {"transport": "http2" if dialect == "cursor_connect_proto" else "http1", "method": "POST", "path": paths[dialect], "media_type": "application/connect+proto" if dialect == "cursor_connect_proto" else "application/json", "framing": framing, "request_shape": {"fixture": "redacted"}} + framing = "connect_envelope" if dialect in {"cursor_connect_proto", "devin_connect_proto"} else "sse" + request = {"transport": "http2" if dialect == "cursor_connect_proto" else "http1", "method": "POST", "path": paths[dialect], "media_type": "application/connect+proto" if dialect in {"cursor_connect_proto", "devin_connect_proto"} else "application/json", "framing": framing, "request_shape": {"fixture": "redacted"}} response = {"encoder": dialect, "framing": framing, "tool_call": "native_shell", "acceptance": "nonce_tool_result_round_trip"} core = {"agent": agent["id"], "platform": platform["id"], "runtime": agent["runtime"][platform["id"]], "version": agent["version"], "executable_sha256": hashlib.sha256(f"{agent['id']}/{platform['id']}".encode()).hexdigest(), "dialect": dialect, "request": request, "response": response, "provenance": "pinned_real_cli_capture"} cells.append({**core, "capture_sha256": hashlib.sha256(json.dumps(core, sort_keys=True, separators=(",", ":")).encode()).hexdigest()}) @@ -57,6 +57,7 @@ def _fixture(root: Path) -> tuple[Path, list[str]]: (tests / "real_agent_driver.py").write_text("# canonical driver\n") (tests / "model_control_proxy.py").write_text("# proxy\n") (tests / "cursor_control_proxy.mjs").write_text("// cursor proxy\n") + (tests / "devin_control_proxy.py").write_text("# devin proxy\n") (tests / "e2e_unified_runner.sh").write_text("python3 real_agent_driver.py\n") readme = root / supervisor.README readme.parent.mkdir(parents=True) @@ -90,7 +91,7 @@ def test_real_repository_contract_is_complete(self): def test_matrix_contains_every_agent_on_every_platform(self): manifest = _manifest() matrix = supervisor.matrix(manifest) - self.assertEqual(len(matrix), 27) + self.assertEqual(len(matrix), 30) self.assertEqual( {(item["agent"], item["platform"]) for item in matrix if item["agent"] == "kimi"}, {("kimi", "ubuntu"), ("kimi", "macos"), ("kimi", "windows")}, @@ -219,7 +220,7 @@ def test_platform_swap_and_fabricated_capture_digest_fail(self): fixture_path.write_text(json.dumps(fixture)) self.assertTrue(any("fabricated capture digest" in error for error in supervisor.contract_errors(root, ids))) - def test_capture_merge_requires_all_27_accepted_unique_cells(self): + def test_capture_merge_requires_all_30_accepted_unique_cells(self): with tempfile.TemporaryDirectory() as temp: root = Path(temp) _fixture(root) @@ -231,9 +232,9 @@ def test_capture_merge_requires_all_27_accepted_unique_cells(self): (captures / f"{cell['agent']}-{cell['platform']}.json").write_text(json.dumps({"schema_version": 1, "accepted": True, "cell": cell})) output = root / "merged.json" supervisor.merge_captures(root, captures, output) - self.assertEqual(len(json.loads(output.read_text())["cells"]), 27) + self.assertEqual(len(json.loads(output.read_text())["cells"]), 30) next(captures.glob("*.json")).unlink() - with self.assertRaisesRegex(supervisor.ContractError, "exactly all 27"): + with self.assertRaisesRegex(supervisor.ContractError, "exactly all 30"): supervisor.merge_captures(root, captures, output) def test_capture_merge_rejects_mixed_versions_and_duplicates(self): @@ -247,7 +248,7 @@ def test_capture_merge_rejects_mixed_versions_and_duplicates(self): (captures / f"{index}.json").write_text(json.dumps({"schema_version": 1, "accepted": True, "cell": cell})) duplicate = provenance["cells"][0] (captures / "duplicate.json").write_text(json.dumps({"schema_version": 1, "accepted": True, "cell": duplicate})) - with self.assertRaisesRegex(supervisor.ContractError, "exactly all 27"): + with self.assertRaisesRegex(supervisor.ContractError, "exactly all 30"): supervisor.merge_captures(root, captures, root / "merged.json") (captures / "duplicate.json").unlink() first = captures / "0.json" diff --git a/labs/15-pitot/tests/test_e2e_reporting.py b/labs/15-pitot/tests/test_e2e_reporting.py index aead5a61b..03be8cf06 100644 --- a/labs/15-pitot/tests/test_e2e_reporting.py +++ b/labs/15-pitot/tests/test_e2e_reporting.py @@ -32,9 +32,9 @@ def _load(name: str, path: Path): def _receipt(agent="claude", platform="ubuntu"): record = next(item for item in runner.MANIFEST["agents"] if item["id"] == agent) - dialect = {"claude": "anthropic_messages", "codex": "openai_responses", "gemini": "gemini_generate_content", "cursor": "cursor_connect_proto"}.get(agent, "openai_chat") - framing = "connect_envelope" if dialect == "cursor_connect_proto" else "sse" - request = {"transport": "http2" if dialect == "cursor_connect_proto" else "http1", "method": "POST", "path": "/fixture", "media_type": "application/connect+proto" if dialect == "cursor_connect_proto" else "application/json", "framing": framing, "request_shape": {"fixture": "redacted"}} + dialect = {"claude": "anthropic_messages", "codex": "openai_responses", "gemini": "gemini_generate_content", "cursor": "cursor_connect_proto", "devin": "devin_connect_proto"}.get(agent, "openai_chat") + framing = "connect_envelope" if dialect in {"cursor_connect_proto", "devin_connect_proto"} else "sse" + request = {"transport": "http2" if dialect == "cursor_connect_proto" else "http1", "method": "POST", "path": "/fixture", "media_type": "application/connect+proto" if dialect in {"cursor_connect_proto", "devin_connect_proto"} else "application/json", "framing": framing, "request_shape": {"fixture": "redacted"}} response = {"encoder": dialect, "framing": framing, "tool_call": "native_shell", "acceptance": "nonce_tool_result_round_trip"} core = {"agent": agent, "platform": platform, "runtime": record["runtime"][platform], "version": record["version"], "executable_sha256": "d" * 64, "dialect": dialect, "request": request, "response": response, "provenance": "pinned_real_cli_capture"} fixture = {**core, "capture_sha256": hashlib.sha256(json.dumps(core, sort_keys=True, separators=(",", ":")).encode()).hexdigest()} @@ -58,9 +58,9 @@ def _receipt(agent="claude", platform="ubuntu"): "nonce": "e" * 32, "receipts": {name: True for name in ("initial_prompt_observed", "allow_tool_call_response_emitted", "allow_tool_result_observed", "deny_tool_call_response_emitted", "denied_result_observed", "final_response_emitted", "consumer_observed", "controller_allow_observed", "controller_deny_observed", "deny_canary_absent", "final_output_observed", "cli_exit_zero")}, "runtime": {"schema_version": 1, "instance_id": "instance", "pid": 1, "endpoint": "http://127.0.0.1:1", "config_sha256": "c" * 64}, - "hooks": [ - {"host": agent, "action_kind": "shell", "action_id": "act_allow", "pitot_exit": 0, "nonce": "e" * 32}, - {"host": agent, "action_kind": "shell", "action_id": "act_deny", "pitot_exit": 2, "nonce": "e" * 32}, + "boundaries": [ + {"host": agent, "transport": "acp" if agent == "devin" else "hook", "action_kind": "shell", "action_id": "act_allow", "decision": "allow", "nonce": "e" * 32}, + {"host": agent, "transport": "acp" if agent == "devin" else "hook", "action_kind": "shell", "action_id": "act_deny", "decision": "deny", "nonce": "e" * 32}, ], "controller": {"id": "e2e-shell-controller", "action_ids": ["act_allow", "act_deny"], "outcomes": ["allow", "deny"]}, "consumer": {"id": "e2e-audit", "action_ids": ["act_allow", "act_deny"], "projection": "sha256"}, @@ -76,7 +76,7 @@ def _result(agent="claude", platform="ubuntu", status="pass", mode="real_cli"): "platform": platform, "status": status, "verification_mode": mode, - "evidence": "binary-observed request, real hook control, projected Consumer, allow/deny canary, and final receipts" if status == "pass" else "real-agent control evidence contract failed", + "evidence": "binary-observed request, real action control, projected Consumer, allow/deny canary, and final receipts" if status == "pass" else "real-agent control evidence contract failed", "cli": receipt["cli"] if receipt else None, "protocol": receipt["protocol"] if receipt else None, "endpoint": receipt["endpoint"] if receipt else None, @@ -84,7 +84,7 @@ def _result(agent="claude", platform="ubuntu", status="pass", mode="real_cli"): "nonce": receipt["nonce"] if receipt else None, "receipts": receipt["receipts"] if receipt else None, "runtime": receipt["runtime"] if receipt else None, - "hooks": receipt["hooks"] if receipt else None, + "boundaries": receipt["boundaries"] if receipt else None, "controller": receipt["controller"] if receipt else None, "consumer": receipt["consumer"] if receipt else None, "canary": receipt["canary"] if receipt else None, @@ -277,7 +277,7 @@ def test_missing_artifacts_fail_every_inventory_cell(self): "a" * 40, "https://github.com/operatorstack/intelligence-flow/actions/runs/1", ) - self.assertEqual(len(results), 9) + self.assertEqual(len(results), 10) self.assertTrue( all( result["status"] == "fail" @@ -307,8 +307,8 @@ def test_rejects_incomplete_or_mismatched_causal_receipts(self): with self.assertRaisesRegex(ValueError, "incomplete causal receipts"): reporter.validate_result(value, agent="claude", platform="ubuntu") value = _result() - value["hooks"][0]["host"] = "cursor" - with self.assertRaisesRegex(ValueError, "hook receipt"): + value["boundaries"][0]["host"] = "cursor" + with self.assertRaisesRegex(ValueError, "boundary receipt"): reporter.validate_result(value, agent="claude", platform="ubuntu") value = _result() value["cli"]["version"] = "latest" diff --git a/labs/15-pitot/tests/test_install_real_agent.py b/labs/15-pitot/tests/test_install_real_agent.py new file mode 100644 index 000000000..219accbb6 --- /dev/null +++ b/labs/15-pitot/tests/test_install_real_agent.py @@ -0,0 +1,108 @@ +from __future__ import annotations + +import hashlib +import importlib.util +import io +import json +import os +from pathlib import Path +import tarfile +import tempfile +import unittest +from unittest import mock + + +ROOT = Path(__file__).resolve().parents[3] +SPEC = importlib.util.spec_from_file_location( + "install_real_agent", + ROOT / "labs/15-pitot/tests/install_real_agent.py", +) +installer = importlib.util.module_from_spec(SPEC) +assert SPEC and SPEC.loader +SPEC.loader.exec_module(installer) + + +def devin_archive(payload: bytes = b"released-devin") -> bytes: + output = io.BytesIO() + with tarfile.open(fileobj=output, mode="w:gz") as bundle: + info = tarfile.TarInfo("bin/devin") + info.mode = 0o755 + info.size = len(payload) + bundle.addfile(info, io.BytesIO(payload)) + return output.getvalue() + + +class Response: + def __init__(self, payload: bytes): + self.payload = payload + + def __enter__(self) -> "Response": + return self + + def __exit__(self, *_: object) -> None: + return None + + def read(self) -> bytes: + return self.payload + + +class DevinInstallerTests(unittest.TestCase): + def test_installs_attested_version_specific_release(self): + archive = devin_archive() + manifest = { + "version": "3000.3.22", + "platforms": { + "aarch64-unknown-linux": { + "url": "https://example.invalid/devin.tar.gz", + "sha256": hashlib.sha256(archive).hexdigest(), + } + }, + } + responses = [ + Response(json.dumps(manifest).encode()), + Response(archive), + ] + agent = { + "id": "devin", + "version": "3000.3.22", + "executable": "devin", + "installer": {"kind": "devin_release", "package": "https://static.devin.ai/cli"}, + } + with tempfile.TemporaryDirectory() as home, mock.patch.dict(os.environ, {"HOME": home}), \ + mock.patch.object(installer.host_platform, "machine", return_value="arm64"), \ + mock.patch.object(installer.urllib.request, "urlopen", side_effect=responses) as urlopen: + installer.install(agent, "ubuntu", "native") + installed = Path(home) / ".local/bin/devin" + self.assertEqual(installed.read_bytes(), b"released-devin") + self.assertTrue(os.access(installed, os.X_OK)) + self.assertEqual( + urlopen.call_args_list[0].args[0], + "https://static.devin.ai/cli/3000.3.22/manifest.json", + ) + + def test_rejects_archive_digest_mismatch(self): + archive = devin_archive() + manifest = { + "version": "3000.3.22", + "platforms": { + "x86_64-unknown-linux": { + "url": "https://example.invalid/devin.tar.gz", + "sha256": "0" * 64, + } + }, + } + agent = { + "id": "devin", + "version": "3000.3.22", + "executable": "devin", + "installer": {"kind": "devin_release", "package": "https://static.devin.ai/cli"}, + } + responses = [Response(json.dumps(manifest).encode()), Response(archive)] + with mock.patch.object(installer.host_platform, "machine", return_value="AMD64"), \ + mock.patch.object(installer.urllib.request, "urlopen", side_effect=responses): + with self.assertRaisesRegex(RuntimeError, "SHA-256 mismatch"): + installer.install(agent, "ubuntu", "native") + + +if __name__ == "__main__": + unittest.main() diff --git a/labs/15-pitot/tests/test_model_control_proxy.py b/labs/15-pitot/tests/test_model_control_proxy.py index c8ba9e6f3..c18a32d44 100644 --- a/labs/15-pitot/tests/test_model_control_proxy.py +++ b/labs/15-pitot/tests/test_model_control_proxy.py @@ -18,6 +18,10 @@ driver = importlib.util.module_from_spec(driver_spec) assert driver_spec and driver_spec.loader driver_spec.loader.exec_module(driver) +devin_spec = importlib.util.spec_from_file_location("devin_control_proxy", ROOT / "labs/15-pitot/tests/devin_control_proxy.py") +devin_proxy = importlib.util.module_from_spec(devin_spec) +assert devin_spec and devin_spec.loader +devin_spec.loader.exec_module(devin_proxy) class ModelControlProtocolTests(unittest.TestCase): @@ -76,6 +80,29 @@ def test_cursor_bridge_is_canonicalized_for_wsl(self): bridge = (base / "project/.cursor/hooks/beforeShellExecution").read_bytes() self.assertEqual(bridge, b"#!/usr/bin/env bash\necho hook\n") + def test_devin_routes_all_released_client_calls_to_isolated_proxy(self): + with tempfile.TemporaryDirectory() as temporary: + base = Path(temporary) + flags, environment = driver.configure( + "devin", base / "home", base / "project", "witness", + "http://127.0.0.1:9123", pitot_command="pitot", + witness_receipt="witness.jsonl", nonce="nonce", + runtime_path="runtime.json", + ) + self.assertEqual(flags, []) + self.assertEqual(environment["WINDSURF_API_SERVER_URL"], "http://127.0.0.1:9123") + self.assertEqual(environment["APPDATA"], str(base / "home/AppData/Roaming")) + self.assertEqual(environment["LOCALAPPDATA"], str(base / "home/AppData/Local")) + credentials = (base / "home/.local/share/devin/credentials.toml").read_text() + self.assertIn('api_server_url = "http://127.0.0.1:9123"', credentials) + + def test_devin_connect_fixture_carries_direct_exec_call(self): + payload = devin_proxy.tool_response("call-nonce", "echo nonce") + self.assertEqual(payload[0], 0) + self.assertIn(b"call-nonce", payload) + self.assertIn(b"exec", payload) + self.assertIn(b'{"command":"echo nonce"}', payload) + def test_windows_canary_command_is_shell_portable(self): command = driver.render_canary_command( r"D:\a\temp\pitot-testrole.exe", r"D:\a\temp\canary.jsonl", True,