Merge dogfood/orb-runtime into dogfood/v0

Orb runtime now routes the OpenCode ACP through the model gateway end to end.
Three-stage proof (Docker, AgentOS/OpenCode session, model turn) passes with
the echo token verified and zero leftover containers.
This commit is contained in:
-Puter
2026-07-25 01:22:34 +05:30
18 changed files with 2836 additions and 1 deletions

View File

@@ -154,20 +154,25 @@
"name": "@code/agents",
"version": "0.0.0",
"dependencies": {
"@agentos-software/opencode": "0.2.7",
"@code/backend": "workspace:*",
"@code/env": "workspace:*",
"@code/primitives": "workspace:*",
"@flue/runtime": "latest",
"@rivet-dev/agentos-core": "catalog:",
"convex": "catalog:",
"dockerode": "^5.0.1",
"effect": "catalog:",
"get-port": "^7.2.0",
"hono": "4.12.31",
"sandbox-agent": "0.4.2",
"valibot": "^1.4.2",
},
"devDependencies": {
"@code/config": "workspace:*",
"@flue/cli": "latest",
"@types/bun": "catalog:",
"@types/dockerode": "^4.0.1",
"typescript": "catalog:",
},
},
@@ -566,6 +571,8 @@
"@babylonjs/core": ["@babylonjs/core@7.54.3", "", {}, "sha512-P5ncXVd8GEUJLhwloP9V0oVwQYIrvZztguVeLlvd5Rx+9aQnenKjpV8auJ6SRsUlAmNZU4pFTKzwF6o2EUfhAw=="],
"@balena/dockerignore": ["@balena/dockerignore@1.0.2", "", {}, "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q=="],
"@base-ui/react": ["@base-ui/react@1.6.0", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@base-ui/utils": "0.3.1", "@floating-ui/react-dom": "^2.1.8", "@floating-ui/utils": "^0.2.11", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@date-fns/tz": "^1.2.0", "@types/react": "^17 || ^18 || ^19", "date-fns": "^4.0.0", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@date-fns/tz", "@types/react", "date-fns"] }, "sha512-/jzjTWJYXhRFO45Bev9lc3cHbmjzCMpUqbMZ2AgKy/z25mY9B6shGSNcXcjQar9n5doM0KYW1W8fcFv2jZBuMw=="],
"@base-ui/utils": ["@base-ui/utils@0.3.1", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@floating-ui/utils": "^0.2.11", "reselect": "^5.2.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-gFFiltORVmW/N6IILTGxizP3PBpVpysqML1ALY5Vk0mH+7faVkCknOU31goYHN5Aoek2dkjxva1XOD2Ce9WuIg=="],
@@ -832,6 +839,10 @@
"@gorhom/portal": ["@gorhom/portal@1.0.14", "", { "dependencies": { "nanoid": "^3.3.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-MXyL4xvCjmgaORr/rtryDNFy3kU4qUbKlwtQqqsygd0xX3mhKjOLn6mQK8wfu0RkoE0pBE0nAasRoHua+/QZ7A=="],
"@grpc/grpc-js": ["@grpc/grpc-js@1.14.4", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ=="],
"@grpc/proto-loader": ["@grpc/proto-loader@0.7.15", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ=="],
"@hono/node-server": ["@hono/node-server@2.0.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-bjD221KPLoJTWUwso1J6fGKiTXEUFedG/s0visavY4zakFPkeGURMRNly+FhBHs7T8Dz4qHaZIMX9ZoJHSJtKA=="],
"@hono/standard-validator": ["@hono/standard-validator@0.2.3", "", { "peerDependencies": { "@standard-schema/spec": "^1.0.0", "hono": ">=3.9.0" } }, "sha512-bp9vHu6Va6SfMHC3D4ZLBbT/woi+AZ9CRdTXQu3kLJuLh2W/Gb9UO4hijS+BQAGFXi4EGpXdetxpzwTAawSVeg=="],
@@ -932,6 +943,8 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="],
"@malept/cross-spawn-promise": ["@malept/cross-spawn-promise@1.1.1", "", { "dependencies": { "cross-spawn": "^7.0.1" } }, "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ=="],
"@mariozechner/clipboard": ["@mariozechner/clipboard@0.3.9", "", { "optionalDependencies": { "@mariozechner/clipboard-darwin-arm64": "0.3.9", "@mariozechner/clipboard-darwin-universal": "0.3.9", "@mariozechner/clipboard-darwin-x64": "0.3.9", "@mariozechner/clipboard-linux-arm64-gnu": "0.3.9", "@mariozechner/clipboard-linux-arm64-musl": "0.3.9", "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.9", "@mariozechner/clipboard-linux-x64-gnu": "0.3.9", "@mariozechner/clipboard-linux-x64-musl": "0.3.9", "@mariozechner/clipboard-win32-arm64-msvc": "0.3.9", "@mariozechner/clipboard-win32-x64-msvc": "0.3.9" } }, "sha512-ABnA53mdfkGZwOFUdZNv2S0CWGO/EIuPj8Vv9xmBFmSYg/qFc7ihO6q5FcQjvoE67kZpWkEc4AhD6B/os04yuA=="],
@@ -1354,6 +1367,20 @@
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A=="],
"@sandbox-agent/cli": ["@sandbox-agent/cli@0.4.2", "", { "dependencies": { "@sandbox-agent/cli-shared": "0.4.2" }, "optionalDependencies": { "@sandbox-agent/cli-darwin-arm64": "0.4.2", "@sandbox-agent/cli-darwin-x64": "0.4.2", "@sandbox-agent/cli-linux-arm64": "0.4.2", "@sandbox-agent/cli-linux-x64": "0.4.2", "@sandbox-agent/cli-win32-x64": "0.4.2" }, "bin": { "sandbox-agent": "bin/sandbox-agent" } }, "sha512-trO//ypJBSt5xkewuol9LOykvDgHwUXq8R+yQVS+0CmpN3lYUtewHkb+At9RVGRhDMmJZY2oasaXDnhfurQ33w=="],
"@sandbox-agent/cli-darwin-arm64": ["@sandbox-agent/cli-darwin-arm64@0.4.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+L1O8SI7k/LLhyB4dG0ghmz1cJHa0WtVjuRTrEE2gw/5EbGLWopPBsCVCmQ7snrQ4fPwtaiZDhfExcEj1VI7aw=="],
"@sandbox-agent/cli-darwin-x64": ["@sandbox-agent/cli-darwin-x64@0.4.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-dDg/EwWsdgVVbJiiCX1scSNRRA48u77SsC7Tuqrfzx4fIJMLuLiIcmEtXQyCBWysSyQNV2Cr+PYXXQfCb3xg8g=="],
"@sandbox-agent/cli-linux-arm64": ["@sandbox-agent/cli-linux-arm64@0.4.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-TGmTUexMoubmWQyTeaOJu0rDVl2h0Ifh1pZ0ceZy7u/6Eoqs2n46CbfQtasUxZJf10uxPgRyzEDhcdDrTYVQUA=="],
"@sandbox-agent/cli-linux-x64": ["@sandbox-agent/cli-linux-x64@0.4.2", "", { "os": "linux", "cpu": "x64" }, "sha512-H9Rbqq0DRkCHvakzefJUDrDa2y+vJjlYd5/tefzKbQ34locE13TGNygRLxdEVXpBECjK9wVdBwTVEphQNsOcjw=="],
"@sandbox-agent/cli-shared": ["@sandbox-agent/cli-shared@0.4.2", "", {}, "sha512-sjZXRkKeFXCSKR6hHzF2Af8CCRO3F3WFwVQJ22+sLTXJ2xskV8lkUE4egknQU9B5BC1Zumts/YiNCFQWG85awQ=="],
"@sandbox-agent/cli-win32-x64": ["@sandbox-agent/cli-win32-x64@0.4.2", "", { "os": "win32", "cpu": "x64" }, "sha512-lZNfHWPwQe/VH51Yvrl/ATCUvBZ3a+c8mwovojhQcmZlv4QuUQPkuvxhPqHRh9AyBx78L5J/ha46es2doa34nQ=="],
"@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
"@secure-exec/core": ["@secure-exec/core@0.2.1", "", { "dependencies": { "better-sqlite3": "^12.8.0" } }, "sha512-HsnUv6gClpMA1BBRmX86j30TKTZtgJC/fO1tVavr7IpM2zNKbHU8LgSlBd7mv2SNy02ImTmU/GnQ3aYB4NSbEg=="],
@@ -1590,6 +1617,10 @@
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
"@types/docker-modem": ["@types/docker-modem@3.0.6", "", { "dependencies": { "@types/node": "*", "@types/ssh2": "*" } }, "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg=="],
"@types/dockerode": ["@types/dockerode@4.0.1", "", { "dependencies": { "@types/docker-modem": "*", "@types/node": "*", "@types/ssh2": "*" } }, "sha512-cmUpB+dPN955PxBEuXE3f6lKO1hHiIGYJA46IVF3BJpNsZGvtBDcRnlrHYHtOH/B6vtDOyl2kZ2ShAu3mgc27Q=="],
"@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
@@ -1628,6 +1659,8 @@
"@types/retry": ["@types/retry@0.12.2", "", {}, "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow=="],
"@types/ssh2": ["@types/ssh2@1.15.5", "", { "dependencies": { "@types/node": "^18.11.18" } }, "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ=="],
"@types/stats.js": ["@types/stats.js@0.17.4", "", {}, "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA=="],
"@types/three": ["@types/three@0.165.0", "", { "dependencies": { "@tweenjs/tween.js": "~23.1.1", "@types/stats.js": "*", "@types/webxr": "*", "fflate": "~0.8.2", "meshoptimizer": "~0.18.1" } }, "sha512-AJK8JZAFNBF0kBXiAIl5pggYlzAGGA8geVYQXAcPCEDRbyA+oEjkpUBcJJrtNz6IiALwzGexFJGZG2yV3WsYBw=="],
@@ -1722,6 +1755,8 @@
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
"acp-http-client": ["acp-http-client@0.4.2", "", { "dependencies": { "@agentclientprotocol/sdk": "^0.16.1" } }, "sha512-3wtPieF08YIU4vNXaoL5up/1D0if4i9IX3Ye5q/bwbcwg1BKsazIK/VNNfvN4ldbPjWul69IqIOpGRS3I0qo3Q=="],
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"agent-cli-detector": ["agent-cli-detector@0.1.4", "", { "bin": { "agent-cli-detector": "dist/cli.js" } }, "sha512-qPgevFvpaQoBaRJVKzr8R7h1WPvV3DtbgRIQlne4le66KBzXx5hNBwo/+NTw67LgkKBlhCzksrdautpUdlls0Q=="],
@@ -1756,6 +1791,8 @@
"asap": ["asap@2.0.6", "", {}, "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="],
"asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="],
"asn1.js": ["asn1.js@4.10.1", "", { "dependencies": { "bn.js": "^4.0.0", "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0" } }, "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw=="],
"assert": ["assert@2.1.0", "", { "dependencies": { "call-bind": "^1.0.2", "is-nan": "^1.3.2", "object-is": "^1.1.5", "object.assign": "^4.1.4", "util": "^0.12.5" } }, "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw=="],
@@ -1800,6 +1837,8 @@
"basic-ftp": ["basic-ftp@5.3.1", "", {}, "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw=="],
"bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="],
"better-auth": ["better-auth@1.6.15", "", { "dependencies": { "@better-auth/core": "1.6.15", "@better-auth/drizzle-adapter": "1.6.15", "@better-auth/kysely-adapter": "1.6.15", "@better-auth/memory-adapter": "1.6.15", "@better-auth/mongo-adapter": "1.6.15", "@better-auth/prisma-adapter": "1.6.15", "@better-auth/telemetry": "1.6.15", "@better-auth/utils": "0.4.1", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.5", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.17 || ^0.29.0", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": "^0.45.2", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-0nuQuEru3ZrLF+9xFUuN3llAmR+6gHLtLunoXaZxB9lXGjSmfBcc6SZUgYq4DfzugPnLvdnzYazsyprZFSFC4Q=="],
"better-call": ["better-call@1.3.5", "", { "dependencies": { "@better-auth/utils": "^0.4.0", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-kOFJkBP7utAQLEYrobZm3vkTH8mXq5GNgvjc5/XEST1ilVHaxXUXfeDeFlqoETMtyqS4+3/h4ONX2i++ebZrvA=="],
@@ -1862,6 +1901,8 @@
"buffer-xor": ["buffer-xor@1.0.3", "", {}, "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ=="],
"buildcheck": ["buildcheck@0.0.7", "", {}, "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA=="],
"builtin-status-codes": ["builtin-status-codes@3.0.0", "", {}, "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ=="],
"bun-ffi-structs": ["bun-ffi-structs@0.2.4", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-AJzsqoVFs1KBbJbWHIYrVZLDC3NhTqqh25awRXqzoLzmBAKr5oqk6+CwuYHAekKx+VBCYVohBoKuRq40dV+TYg=="],
@@ -1998,6 +2039,8 @@
"cosmiconfig": ["cosmiconfig@9.0.2", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg=="],
"cpu-features": ["cpu-features@0.0.10", "", { "dependencies": { "buildcheck": "~0.0.6", "nan": "^2.19.0" } }, "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA=="],
"create-ecdh": ["create-ecdh@4.0.4", "", { "dependencies": { "bn.js": "^4.1.0", "elliptic": "^6.5.3" } }, "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A=="],
"create-hash": ["create-hash@1.2.0", "", { "dependencies": { "cipher-base": "^1.0.1", "inherits": "^2.0.1", "md5.js": "^1.3.4", "ripemd160": "^2.0.1", "sha.js": "^2.4.0" } }, "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg=="],
@@ -2168,6 +2211,10 @@
"dnssd-advertise": ["dnssd-advertise@1.1.6", "", {}, "sha512-Ndrrf6BMPalkQPd/zubL+4YghH2J9NspapQ09uDXwYbvOPkP0oaqf5CkcwJ0b50kS2O3ul6yVu+jz+RY62Cejg=="],
"docker-modem": ["docker-modem@5.0.7", "", { "dependencies": { "debug": "^4.1.1", "readable-stream": "^3.5.0", "split-ca": "^1.0.1", "ssh2": "^1.15.0" } }, "sha512-XJgGhoR/CLpqshm4d3L7rzH6t8NgDFUIIpztYlLHIApeJjMZKYJMz2zxPsYxnejq5h3ELYSw/RBsi3t5h7gNTA=="],
"dockerode": ["dockerode@5.0.1", "", { "dependencies": { "@balena/dockerignore": "^1.0.2", "@grpc/grpc-js": "^1.11.1", "@grpc/proto-loader": "^0.7.13", "docker-modem": "^5.0.7", "protobufjs": "^7.3.2", "tar-fs": "^2.1.4" } }, "sha512-avsq/xk4YPIrn0CgleX5bjT9Y8IT1p9PxrNQ++RBQ2WEyFfHCTDsT9kmyxz+H/axnjAwg8wJWEIuPGOUuNupiA=="],
"dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="],
"dom-helpers": ["dom-helpers@3.4.0", "", { "dependencies": { "@babel/runtime": "^7.1.2" } }, "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA=="],
@@ -2446,6 +2493,8 @@
"get-own-enumerable-keys": ["get-own-enumerable-keys@1.0.0", "", {}, "sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA=="],
"get-port": ["get-port@7.2.0", "", {}, "sha512-afP4W205ONCuMoPBqcR6PSXnzX35KTcJygfJfcp+QY+uwm3p20p1YczWXhlICIzGMCxYBQcySEcOgsJcrkyobg=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="],
@@ -2768,6 +2817,8 @@
"lodash-es": ["lodash-es@4.18.1", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="],
"lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="],
"lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="],
"lodash.throttle": ["lodash.throttle@4.1.1", "", {}, "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ=="],
@@ -3004,6 +3055,8 @@
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
"nan": ["nan@2.28.0", "", {}, "sha512-fTsDz99OTq2sVePhGdp4qQhggZFtKr64ZNVyVajRKtMOkJxYekplBh577PiJB12v/D3s2E5cGtOI45LWp6rnLQ=="],
"nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="],
"nanostores": ["nanostores@1.4.1", "", {}, "sha512-PGd3uPojJB9Z07d5NX3Db/SOSBbyy3wLMUGq0GpnEEJfVzY9mq7daPMAZ3jObV5D3Jn+YKND636eI5ULg7F80Q=="],
@@ -3442,6 +3495,8 @@
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"sandbox-agent": ["sandbox-agent@0.4.2", "", { "dependencies": { "@sandbox-agent/cli-shared": "0.4.2", "acp-http-client": "0.4.2" }, "optionalDependencies": { "@sandbox-agent/cli": "0.4.2" }, "peerDependencies": { "@cloudflare/sandbox": ">=0.1.0", "@daytonaio/sdk": ">=0.12.0", "@e2b/code-interpreter": ">=1.0.0", "@fly/sprites": ">=0.0.1", "@vercel/sandbox": ">=0.1.0", "computesdk": ">=0.1.0", "dockerode": ">=4.0.0", "get-port": ">=7.0.0", "modal": ">=0.1.0" }, "optionalPeers": ["@cloudflare/sandbox", "@daytonaio/sdk", "@e2b/code-interpreter", "@fly/sprites", "@vercel/sandbox", "computesdk", "dockerode", "get-port", "modal"] }, "sha512-fH6WDQEaIrgiu93LxZcy+4Dx+t+/cslu+hzXImDyUlsaL6jV2jIv4fdxELkALlo7uzyEDVK9lmqs9qy65RHwBQ=="],
"sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="],
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
@@ -3538,6 +3593,8 @@
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
"split-ca": ["split-ca@1.0.1", "", {}, "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ=="],
"split-on-first": ["split-on-first@1.1.0", "", {}, "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw=="],
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
@@ -3546,6 +3603,8 @@
"sql.js": ["sql.js@1.14.1", "", {}, "sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A=="],
"ssh2": ["ssh2@1.17.0", "", { "dependencies": { "asn1": "^0.2.6", "bcrypt-pbkdf": "^1.0.2" }, "optionalDependencies": { "cpu-features": "~0.0.10", "nan": "^2.23.0" } }, "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ=="],
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
"stackframe": ["stackframe@1.3.4", "", {}, "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="],
@@ -3700,6 +3759,8 @@
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
"tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="],
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
"type-fest": ["type-fest@5.8.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA=="],
@@ -3972,6 +4033,8 @@
"@google/genai/ws": ["ws@8.21.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw=="],
"@grpc/grpc-js/@grpc/proto-loader": ["@grpc/proto-loader@0.8.1", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg=="],
"@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.12", "", {}, "sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g=="],
"@jest/types/@types/node": ["@types/node@26.1.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw=="],
@@ -4050,6 +4113,12 @@
"@testing-library/dom/pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
"@types/docker-modem/@types/node": ["@types/node@26.1.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw=="],
"@types/dockerode/@types/node": ["@types/node@26.1.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw=="],
"@types/ssh2/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
"@types/ws/@types/node": ["@types/node@26.1.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw=="],
"@types/yauzl/@types/node": ["@types/node@26.1.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw=="],
@@ -4608,6 +4677,12 @@
"@testing-library/dom/pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
"@types/docker-modem/@types/node/undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
"@types/dockerode/@types/node/undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
"@types/ssh2/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"@types/ws/@types/node/undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
"@types/yauzl/@types/node/undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],

View File

@@ -59,7 +59,8 @@
"build:agents": "vp run --filter @code/agents build",
"docs:update": "bun run scripts/update-docs.ts",
"subtree": "bun run scripts/subtree.ts",
"fix": "ultracite fix"
"fix": "ultracite fix",
"orb:proof": "bun run scripts/orb-proof.ts"
},
"dependencies": {},
"devDependencies": {

View File

@@ -12,20 +12,25 @@
"run:zopu": "bun --env-file=../../.env flue run zopu"
},
"dependencies": {
"@agentos-software/opencode": "0.2.7",
"@code/backend": "workspace:*",
"@code/env": "workspace:*",
"@code/primitives": "workspace:*",
"@flue/runtime": "latest",
"@rivet-dev/agentos-core": "catalog:",
"convex": "catalog:",
"dockerode": "^5.0.1",
"effect": "catalog:",
"get-port": "^7.2.0",
"hono": "4.12.31",
"sandbox-agent": "0.4.2",
"valibot": "^1.4.2"
},
"devDependencies": {
"@code/config": "workspace:*",
"@flue/cli": "latest",
"@types/bun": "catalog:",
"@types/dockerode": "^4.0.1",
"typescript": "catalog:"
}
}

View File

@@ -0,0 +1,105 @@
# Orb Runtime
One logical execution workspace for one ProjectIssue run. An Orb bundles an AgentOS actor, an OpenCode harness session, a Docker sandbox, a mounted repository workspace, project context, model-gateway configuration, process/log handling, normalized execution events, and a cleanup lifecycle.
## Architecture
```
┌──────────────────────────────────────────────────┐
│ OrbRuntime │
│ creates OrbHandle instances │
├──────────────────────────────────────────────────┤
│ OrbHandle │
│ ┌─────────────┐ ┌────────────────────────┐ │
│ │ AgentOS VM │ │ SandboxAgent + Docker │ │
│ │ (OpenCode │◄──►│ (sandbox-agent server │ │
│ │ ACP agent) │ │ in a Docker container)│ │
│ └─────────────┘ └────────────────────────┘ │
│ │ │ │
│ session events runProcess / │
│ → normalized createProcess │
│ OrbEvent │
└──────────────────────────────────────────────────┘
```
The AgentOS VM runs the OpenCode ACP adapter (lightweight agent loop, session management, durable identity). Heavy execution — package installs, test suites, builds — runs inside a Docker container hosting a sandbox-agent server. The `DockerSandboxProvider` calls `SandboxAgent.start({ sandbox: docker(...) })`, which starts the server in a Docker container with a dynamically-mapped host port and returns a `SandboxAgent` client whose `baseUrl` both the main process and the AgentOS sidecar subprocess reach over `127.0.0.1`. The sandbox filesystem is mounted into the VM at `/mnt/sandbox`.
## Domain model
| Type | Description |
| --- | --- |
| `OrbId` | Branded identifier for one Orb |
| `OrbRunId` | Branded identifier for one run attempt |
| `OrbSessionId` | Branded identifier for one OpenCode session |
| `OrbIdentity` | `{ projectId, runId, workUnitId }` — derives the actor key |
| `OrbState` | `creating → prepared → running → needs-input → completed/failed/cancelled → disposed` |
| `RunState` | `queued → provisioning → preparing → running → verifying → succeeded/failed/cancelled` |
| `OrbEvent` | Normalized, secret-free execution event |
Orb state and run state are separate state machines. The VM/sandbox lease state is never conflated with the product work-unit state.
## Tagged errors
| Error | Reasons |
| --- | --- |
| `OrbStateError` | `InvalidTransition`, `AlreadyDisposed`, `NotRunning` |
| `OrbSandboxError` | `DockerUnavailable`, `ContainerStart`, `CommandFailed`, `ContainerCleanup` |
| `OrbSessionError` | `OpenSession`, `PromptFailed`, `AgentNotInstalled`, `SessionNotFound` |
| `OrbConfigurationError` | `MissingGateway`, `MissingIdentity`, `InvalidModelConfig` |
## Environment variables
| Variable | Required | Description |
| --- | --- | --- |
| `ORB_PROOF` | Proof only | Set to `1` to run the proof fixture |
| `ORB_GATEWAY_API_KEY` | Proof only | Model gateway API key |
| `ORB_GATEWAY_BASE_URL` | Proof only | Model gateway base URL (OpenAI-compatible `/v1`) |
| `ORB_GATEWAY_MODEL` | Proof only | Model name |
| `ORB_GATEWAY_PROVIDER` | Proof only | Provider identifier |
| `ORB_DOCKER_IMAGE` | Optional | Docker image (default: `oven/bun:1.3-debian`) |
| `ORB_DOCKER_WORKSPACE` | Optional | Host workspace root (default: `/tmp/orb-workspaces`) |
| `RIVET_ENDPOINT` | Optional | AgentOS/RivetKit sidecar endpoint |
No permanent provider credentials are stored in project files. API keys are injected at runtime via the OpenCode config written to the VM filesystem, and all event output is passed through `redactSecrets`.
## Docker requirements
- Docker daemon running and accessible via the Docker socket (`/var/run/docker.sock`)
- The sandbox uses `rivetdev/sandbox-agent` as its Docker image by default
- The `sandbox-agent/docker` provider creates containers with `AutoRemove` and a dynamically allocated host port
- Writable bind mount is the project workspace only (mounted at `/home/sandbox` inside the container)
- The AgentOS sidecar subprocess reaches the sandbox-agent server over `127.0.0.1:<hostPort>`
## Local startup
```bash
# Run the proof fixture
ORB_PROOF=1 \
ORB_GATEWAY_API_KEY=your-key \
ORB_GATEWAY_BASE_URL=https://ai.example.com/v1 \
ORB_GATEWAY_MODEL=glm-5.2 \
ORB_GATEWAY_PROVIDER=cheaptricks \
bun run scripts/orb-proof.ts
```
Stable markers: `ORB_PROOF_PASSED` (exit 0), `ORB_PROOF_BLOCKED` (exit 2), `ORB_PROOF_FAILED` (exit 1).
## Filesystem layout
```
SandboxAgent container (/home/sandbox = host bind mount)
/home/sandbox/repository/ — project checkout
/home/sandbox/control/ — issue + context files
AgentOS VM (host process)
/mnt/sandbox/ — sandbox mount (via SandboxAgent baseUrl)
/mnt/sandbox/repository/ — repo (via sandbox)
/root/.config/opencode/ — OpenCode config (chmod 600)
```
## Current limitations
- No automatic merge or production deployment capability.
- No multi-region support.
- Interactive PTY sessions are not wired through the sandbox agent.
- The model turn stage depends on a reachable OpenAI-compatible gateway; if the gateway is unreachable the proof reports BLOCKED at that stage but still passes Docker and AgentOS/OpenCode.

View File

@@ -0,0 +1,170 @@
/* eslint-disable no-console -- diagnostic skip messages in a Docker-guarded suite */
import { spawn } from "node:child_process";
import { setTimeout as sleepTimer } from "node:timers/promises";
import { Effect } from "effect";
import { describe, expect, it as vitestIt } from "vitest";
import { DockerSandboxProvider } from "./docker-sandbox";
import { OrbSandboxError } from "./domain";
// Real containers need more than the 5s default; bind a generous timeout here.
const it = (name: string, fn: () => Promise<void>): void => {
vitestIt(name, fn, 60_000);
};
// Node-compatible Docker availability check.
const runCliExit = (args: readonly string[]): Promise<number | null> =>
// eslint-disable-next-line promise/avoid-new -- wrapping one-shot child close in a single promise
new Promise((resolve) => {
const [command = "docker", ...rest] = args;
const proc = spawn(command, rest, {
stdio: ["ignore", "pipe", "pipe"],
});
proc.on("error", () => resolve(null));
proc.on("close", (code) => resolve(code));
});
const dockerAvailable = async (): Promise<boolean> => {
try {
const code = await runCliExit([
"docker",
"version",
"--format",
"{{.Server.Version}}",
]);
return code === 0;
} catch {
return false;
}
};
// Top-level await so describe.skipIf evaluates Docker availability at registration.
const HAVE_DOCKER = await dockerAvailable();
const tmpDir = (): string =>
`/tmp/orb-test-${Date.now()}-${Math.trunc(Math.random() * 1e6)}`;
describe.skipIf(!HAVE_DOCKER)(
"DockerSandboxProvider (SandboxAgent + Docker)",
() => {
it("starts a SandboxAgent server and exposes a reachable baseUrl", async () => {
const provider = await Effect.runPromise(
DockerSandboxProvider.create({
hostWorkspacePath: tmpDir(),
})
);
try {
const client = await provider.start();
expect(provider.isStarted).toBe(true);
// The SandboxAgent client must have a baseUrl property — this is what
// AgentOS serializes so the sidecar can reach the sandbox.
const { baseUrl } = client as unknown as { baseUrl: string };
expect(baseUrl).toBeTruthy();
expect(baseUrl).toMatch(/^https?:\/\//u);
// Run a command to verify the sandbox-agent server actually works.
const result = await client.runProcess({
args: ["-c", "echo hello-orb"],
command: "sh",
});
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("hello-orb");
} finally {
await provider.dispose();
}
});
it("is idempotent: start returns the same client on repeated calls", async () => {
const provider = await Effect.runPromise(
DockerSandboxProvider.create({
hostWorkspacePath: tmpDir(),
})
);
try {
const c1 = await provider.start();
const c2 = await provider.start();
expect(c1).toBe(c2);
} finally {
await provider.dispose();
}
});
it("disposes cleanly and frees the Docker container", async () => {
const provider = await Effect.runPromise(
DockerSandboxProvider.create({
hostWorkspacePath: tmpDir(),
})
);
await provider.start();
expect(provider.isStarted).toBe(true);
await provider.dispose();
expect(provider.isStarted).toBe(false);
// Give AutoRemove a moment.
// eslint-disable-next-line no-await-in-loop -- single settling wait
await sleepTimer(2000);
// Second dispose is a safe no-op.
await expect(provider.dispose()).resolves.toBeUndefined();
});
it("binds the host workspace into the sandbox container", async () => {
const workspace = tmpDir();
const { spawn: nodeSpawn } = await import("node:child_process");
// eslint-disable-next-line promise/avoid-new -- one-shot shell write
await new Promise<void>((resolve) => {
const p = nodeSpawn(
"sh",
[
"-c",
`mkdir -p ${workspace} && echo proof-file > ${workspace}/marker.txt`,
],
{
stdio: ["ignore", "pipe", "pipe"],
}
);
p.on("close", () => resolve());
});
const provider = await Effect.runPromise(
DockerSandboxProvider.create({
hostWorkspacePath: workspace,
})
);
try {
const client = await provider.start();
const result = await client.runProcess({
args: ["-c", "cat /home/sandbox/marker.txt"],
command: "sh",
});
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("proof-file");
} finally {
await provider.dispose();
}
});
}
);
describe.skipIf(HAVE_DOCKER)("DockerSandboxProvider without Docker", () => {
it("create fails with DockerUnavailable", async () => {
const error = await Effect.runPromise(
Effect.flip(
DockerSandboxProvider.create({
hostWorkspacePath: "/tmp/orb-test-nodocker",
})
)
);
expect(error).toBeInstanceOf(OrbSandboxError);
expect(error.reason).toBe("DockerUnavailable");
});
});
if (!HAVE_DOCKER) {
console.warn(
"[docker-sandbox.test.ts] Docker daemon unavailable; SandboxAgent tests skipped."
);
}

View File

@@ -0,0 +1,118 @@
import type {
AgentOsSandboxClient,
AgentOsSandboxProvider,
} from "@rivet-dev/agentos-core";
import { Effect } from "effect";
import { OrbSandboxError } from "./domain";
// ---------------------------------------------------------------------------
// Docker sandbox — AgentOsSandboxProvider backed by sandbox-agent + Docker
// ---------------------------------------------------------------------------
//
// AgentOS serializes sandbox mounts through getSerializableClientConfig,
// which reads client.baseUrl and passes it to the sidecar. An in-process
// client object can never satisfy that contract because it has no network
// endpoint. The supported boundary is a standard SandboxAgent client:
//
// SandboxAgent.start({ sandbox: docker({ image, binds }) })
//
// starts a sandbox-agent server inside a Docker container, dynamically maps
// a host port, and returns a SandboxAgent client whose baseUrl the sidecar
// can reach over HTTP on 127.0.0.1:<hostPort>. Both the main process and
// the sidecar subprocess run on the host, so localhost connectivity works.
const SANDBOX_AGENT_IMAGE = "rivetdev/sandbox-agent:0.5.0-rc.2-full";
const DEFAULT_WORKSPACE = "/home/sandbox";
// ---------------------------------------------------------------------------
// DockerSandboxProvider — wraps SandboxAgent.start with the docker provider
// ---------------------------------------------------------------------------
export interface DockerSandboxOptions {
readonly containerName?: string;
readonly hostWorkspacePath: string;
readonly image?: string;
readonly workDir?: string;
}
export class DockerSandboxProvider implements AgentOsSandboxProvider {
private readonly image: string;
private readonly binds: string[];
private client: AgentOsSandboxClient | null = null;
private disposeFn: (() => Promise<void>) | null = null;
constructor(options: DockerSandboxOptions) {
this.image = options.image ?? SANDBOX_AGENT_IMAGE;
// Bind the host workspace read-write into the sandbox container so the
// restricted writable area is the project workspace only.
this.binds = [
`${options.hostWorkspacePath}:${options.workDir ?? DEFAULT_WORKSPACE}`,
];
}
static readonly create = Effect.fn("DockerSandboxProvider.create")(
function* createProvider(options: DockerSandboxOptions) {
// eslint-disable-next-line no-use-before-define -- defined at module bottom
yield* checkDockerAvailable();
return new DockerSandboxProvider(options);
}
);
async start(): Promise<AgentOsSandboxClient> {
if (this.client) {
return this.client;
}
const { SandboxAgent } = await import("sandbox-agent");
const { docker } = await import("sandbox-agent/docker");
const sandboxAgent = await SandboxAgent.start({
sandbox: docker({
binds: this.binds,
image: this.image,
}),
});
this.client = sandboxAgent as unknown as AgentOsSandboxClient;
this.disposeFn = async () => {
// destroySandbox permanently tears down the backing Docker container;
// dispose() alone only closes the HTTP connection.
await sandboxAgent.destroySandbox();
};
return this.client;
}
async dispose(): Promise<void> {
const dispose = this.disposeFn;
this.client = null;
this.disposeFn = null;
if (dispose) {
await dispose();
}
}
get isStarted(): boolean {
return this.client !== null;
}
}
// ---------------------------------------------------------------------------
// Docker availability check
// ---------------------------------------------------------------------------
const checkDockerAvailable = Effect.fn("Docker.checkAvailable")(
function* checkDockerAvailable() {
yield* Effect.tryPromise({
catch: (cause) =>
new OrbSandboxError({
message: `Docker is not available: ${cause instanceof Error ? cause.message : String(cause)}`,
reason: "DockerUnavailable",
}),
try: async () => {
const { default: Docker } = await import("dockerode");
const docker = new Docker();
await docker.ping();
},
});
}
);

View File

@@ -0,0 +1,132 @@
import { Effect } from "effect";
import { describe, expect, it } from "vitest";
import {
orbActorKey,
OrbStateError,
transitionOrbState,
transitionRunState,
} from "./domain";
const runTransition = (from: string, to: string) =>
Effect.runSync(
Effect.flip(transitionOrbState({ from: from as never, to: to as never }))
);
const runTransitionOk = (from: string, to: string) =>
Effect.runSync(transitionOrbState({ from: from as never, to: to as never }));
const runRunState = (from: string, to: string) =>
Effect.runSync(
Effect.flip(transitionRunState({ from: from as never, to: to as never }))
);
const runRunStateOk = (from: string, to: string) =>
Effect.runSync(transitionRunState({ from: from as never, to: to as never }));
describe("orbActorKey", () => {
it("produces a stable key from the identity triple", () => {
const key = orbActorKey({
projectId: "prj-1",
runId: "run-1",
workUnitId: "wrk-1",
});
expect(key).toBe("project:prj-1:work:wrk-1:run:run-1");
});
it("preserves order across different identities", () => {
expect(orbActorKey({ projectId: "a", runId: "b", workUnitId: "c" })).toBe(
"project:a:work:c:run:b"
);
});
});
describe("transitionOrbState", () => {
it("allows creating -> prepared", () => {
expect(runTransitionOk("creating", "prepared")).toBe("prepared");
});
it("allows prepared -> running", () => {
expect(runTransitionOk("prepared", "running")).toBe("running");
});
it("allows running -> needs-input", () => {
expect(runTransitionOk("running", "needs-input")).toBe("needs-input");
});
it("allows running -> completed", () => {
expect(runTransitionOk("running", "completed")).toBe("completed");
});
it("allows completed -> disposed", () => {
expect(runTransitionOk("completed", "disposed")).toBe("disposed");
});
it("allows needs-input -> running (resume)", () => {
expect(runTransitionOk("needs-input", "running")).toBe("running");
});
it("rejects disposed -> running", () => {
const error = runTransition("disposed", "running");
expect(error).toBeInstanceOf(OrbStateError);
expect(error.reason).toBe("AlreadyDisposed");
});
it("rejects completed -> creating", () => {
const error = runTransition("completed", "creating");
expect(error).toBeInstanceOf(OrbStateError);
expect(error.reason).toBe("InvalidTransition");
});
it("rejects prepared -> needs-input (skipping running)", () => {
const error = runTransition("prepared", "needs-input");
expect(error).toBeInstanceOf(OrbStateError);
expect(error.reason).toBe("InvalidTransition");
});
it("rejects cancelled -> running", () => {
const error = runTransition("cancelled", "running");
expect(error).toBeInstanceOf(OrbStateError);
expect(error.reason).toBe("InvalidTransition");
});
});
describe("transitionRunState", () => {
it("allows queued -> provisioning", () => {
expect(runRunStateOk("queued", "provisioning")).toBe("provisioning");
});
it("allows provisioning -> preparing", () => {
expect(runRunStateOk("provisioning", "preparing")).toBe("preparing");
});
it("allows running -> verifying", () => {
expect(runRunStateOk("running", "verifying")).toBe("verifying");
});
it("allows verifying -> succeeded", () => {
expect(runRunStateOk("verifying", "succeeded")).toBe("succeeded");
});
it("allows running -> cancelled", () => {
expect(runRunStateOk("running", "cancelled")).toBe("cancelled");
});
it("rejects succeeded -> running (terminal)", () => {
const error = runRunState("succeeded", "running");
expect(error).toBeInstanceOf(OrbStateError);
expect(error.reason).toBe("InvalidTransition");
});
it("rejects failed -> running (terminal)", () => {
const error = runRunState("failed", "running");
expect(error).toBeInstanceOf(OrbStateError);
expect(error.reason).toBe("InvalidTransition");
});
it("rejects queued -> succeeded (skipping steps)", () => {
const error = runRunState("queued", "succeeded");
expect(error).toBeInstanceOf(OrbStateError);
expect(error.reason).toBe("InvalidTransition");
});
});

View File

@@ -0,0 +1,259 @@
/* eslint-disable max-classes-per-file -- each domain failure has a distinct tagged reason. */
import { Effect, Schema } from "effect";
const MeaningfulString = Schema.String.check(
Schema.makeFilter((value) => value.trim().length > 0, {
expected: "a non-empty string",
})
);
// ---------------------------------------------------------------------------
// Branded identifiers
// ---------------------------------------------------------------------------
export const OrbId = MeaningfulString.pipe(Schema.brand("OrbId"));
export type OrbId = typeof OrbId.Type;
export const OrbRunId = MeaningfulString.pipe(Schema.brand("OrbRunId"));
export type OrbRunId = typeof OrbRunId.Type;
export const OrbSessionId = MeaningfulString.pipe(Schema.brand("OrbSessionId"));
export type OrbSessionId = typeof OrbSessionId.Type;
// ---------------------------------------------------------------------------
// Actor identity — one Orb per project/issue/run triple
// ---------------------------------------------------------------------------
export const OrbIdentity = Schema.Struct({
projectId: MeaningfulString,
runId: MeaningfulString,
workUnitId: MeaningfulString,
});
export type OrbIdentity = typeof OrbIdentity.Type;
/** Stable RivetKit actor key derived from the identity triple. */
export const orbActorKey = (identity: OrbIdentity): string =>
`project:${identity.projectId}:work:${identity.workUnitId}:run:${identity.runId}`;
// ---------------------------------------------------------------------------
// State machines — product state stays separate from run state
// ---------------------------------------------------------------------------
export const OrbState = Schema.Literals([
"creating",
"prepared",
"running",
"needs-input",
"completed",
"failed",
"cancelled",
"disposed",
]);
export type OrbState = typeof OrbState.Type;
export const RunState = Schema.Literals([
"queued",
"provisioning",
"preparing",
"running",
"verifying",
"succeeded",
"failed",
"cancelled",
]);
export type RunState = typeof RunState.Type;
const ORB_TRANSITIONS: Readonly<Record<OrbState, readonly OrbState[]>> = {
cancelled: ["disposed"],
completed: ["disposed"],
creating: ["prepared", "running", "failed", "cancelled", "disposed"],
disposed: [],
failed: ["disposed"],
"needs-input": ["running", "completed", "failed", "cancelled", "disposed"],
prepared: ["running", "failed", "cancelled", "disposed"],
running: ["needs-input", "completed", "failed", "cancelled", "disposed"],
};
const RUN_TRANSITIONS: Readonly<Record<RunState, readonly RunState[]>> = {
cancelled: [],
failed: [],
preparing: ["running", "verifying", "failed", "cancelled"],
provisioning: ["preparing", "running", "failed", "cancelled"],
queued: ["provisioning", "preparing", "running", "failed", "cancelled"],
running: ["verifying", "succeeded", "failed", "cancelled"],
succeeded: [],
verifying: ["succeeded", "failed", "cancelled"],
};
// ---------------------------------------------------------------------------
// Tagged errors
// ---------------------------------------------------------------------------
export const OrbStateErrorReason = Schema.Literals([
"InvalidTransition",
"AlreadyDisposed",
"NotRunning",
]);
export type OrbStateErrorReason = typeof OrbStateErrorReason.Type;
export class OrbStateError extends Schema.TaggedErrorClass<OrbStateError>()(
"OrbStateError",
{
from: Schema.String,
message: Schema.String,
reason: OrbStateErrorReason,
to: Schema.String,
}
) {}
export const OrbSandboxErrorReason = Schema.Literals([
"DockerUnavailable",
"ContainerStart",
"CommandFailed",
"ContainerCleanup",
]);
export type OrbSandboxErrorReason = typeof OrbSandboxErrorReason.Type;
export class OrbSandboxError extends Schema.TaggedErrorClass<OrbSandboxError>()(
"OrbSandboxError",
{
message: Schema.String,
reason: OrbSandboxErrorReason,
}
) {}
export const OrbSessionErrorReason = Schema.Literals([
"OpenSession",
"PromptFailed",
"AgentNotInstalled",
"SessionNotFound",
"PermissionDenied",
]);
export type OrbSessionErrorReason = typeof OrbSessionErrorReason.Type;
export class OrbSessionError extends Schema.TaggedErrorClass<OrbSessionError>()(
"OrbSessionError",
{
message: Schema.String,
reason: OrbSessionErrorReason,
}
) {}
export const OrbConfigurationErrorReason = Schema.Literals([
"MissingGateway",
"MissingIdentity",
"InvalidModelConfig",
]);
export type OrbConfigurationErrorReason =
typeof OrbConfigurationErrorReason.Type;
export class OrbConfigurationError extends Schema.TaggedErrorClass<OrbConfigurationError>()(
"OrbConfigurationError",
{
message: Schema.String,
reason: OrbConfigurationErrorReason,
}
) {}
// ---------------------------------------------------------------------------
// Configuration types
// ---------------------------------------------------------------------------
export const OrbModelGatewayConfig = Schema.Struct({
apiKey: MeaningfulString,
baseUrl: MeaningfulString,
model: MeaningfulString,
provider: MeaningfulString,
});
export type OrbModelGatewayConfig = typeof OrbModelGatewayConfig.Type;
export const OrbProjectContext = Schema.Struct({
artifacts: Schema.Array(
Schema.Struct({
content: Schema.String,
path: MeaningfulString,
})
),
contextFiles: Schema.Array(
Schema.Struct({
content: Schema.String,
path: MeaningfulString,
})
),
issueBody: MeaningfulString,
issueTitle: MeaningfulString,
repositoryUrl: Schema.UndefinedOr(MeaningfulString),
});
export type OrbProjectContext = typeof OrbProjectContext.Type;
// ---------------------------------------------------------------------------
// Transition helpers
// ---------------------------------------------------------------------------
export const transitionOrbState = Effect.fn("Orb.transitionState")(
function* transitionOrbState(input: {
readonly from: OrbState;
readonly to: OrbState;
}) {
if (input.from === input.to) {
return input.to;
}
if (input.from === "disposed") {
return yield* Effect.fail(
new OrbStateError({
from: input.from,
message: "Orb is already disposed",
reason: "AlreadyDisposed",
to: input.to,
})
);
}
if (!ORB_TRANSITIONS[input.from].includes(input.to)) {
return yield* Effect.fail(
new OrbStateError({
from: input.from,
message: `Cannot transition orb from ${input.from} to ${input.to}`,
reason: "InvalidTransition",
to: input.to,
})
);
}
return input.to;
}
);
export const transitionRunState = Effect.fn("Orb.transitionRunState")(
function* transitionRunState(input: {
readonly from: RunState;
readonly to: RunState;
}) {
if (input.from === input.to) {
return input.to;
}
if (
input.from === "succeeded" ||
input.from === "failed" ||
input.from === "cancelled"
) {
return yield* Effect.fail(
new OrbStateError({
from: input.from,
message: `Run is already in terminal state ${input.from}`,
reason: "InvalidTransition",
to: input.to,
})
);
}
if (!RUN_TRANSITIONS[input.from].includes(input.to)) {
return yield* Effect.fail(
new OrbStateError({
from: input.from,
message: `Cannot transition run from ${input.from} to ${input.to}`,
reason: "InvalidTransition",
to: input.to,
})
);
}
return input.to;
}
);

View File

@@ -0,0 +1,134 @@
/* eslint-disable no-non-null-assertion -- test assertions on defined events */
import { describe, expect, it } from "vitest";
import { makeOrbEvent, normalizeSessionEvent, redactSecrets } from "./events";
describe("redactSecrets", () => {
it("redacts api_key patterns", () => {
const input = "Using api_key=sk-abc123def456ghi789jkl012mno345";
const result = redactSecrets(input);
expect(result).not.toContain("sk-abc123");
expect(result).toContain("[REDACTED]");
});
it("redacts Bearer tokens", () => {
const input = "Authorization: Bearer eyJhbGciOiJIUzI1";
const result = redactSecrets(input);
expect(result).toContain("Bearer [REDACTED]");
expect(result).not.toContain("eyJhbGciOiJIUzI1");
});
it("redacts token= patterns", () => {
const input = 'token: "my-secret-token-value"';
const result = redactSecrets(input);
expect(result).not.toContain("my-secret-token-value");
});
it("preserves non-secret text", () => {
expect(redactSecrets("just regular text")).toBe("just regular text");
});
});
describe("makeOrbEvent", () => {
it("creates an event with sequence and timestamp", () => {
const event = makeOrbEvent(5, "session_opened", { text: "opened" });
expect(event.sequence).toBe(5);
expect(event.type).toBe("session_opened");
expect(event.text).toBe("opened");
expect(event.timestamp).toBeDefined();
});
it("creates an event with command and exitCode", () => {
const event = makeOrbEvent(3, "command_executed", {
command: "bun test",
exitCode: 0,
});
expect(event.command).toBe("bun test");
expect(event.exitCode).toBe(0);
});
});
describe("normalizeSessionEvent", () => {
it("normalizes agent_message_chunk", () => {
const event = normalizeSessionEvent({
rawText: "Working on the fix",
sequence: 5,
sessionId: "sess-1",
timestamp: "2026-07-24T12:00:00Z",
type: "agent_message_chunk",
});
expect(event).toBeDefined();
expect(event!.type).toBe("agent_message_chunk");
expect(event!.text).toBe("Working on the fix");
expect(event!.sequence).toBe(5);
});
it("normalizes agent_message to agent_message_completed", () => {
const event = normalizeSessionEvent({
content: [{ text: "Done", type: "text" }],
sequence: 10,
sessionId: "sess-1",
timestamp: "2026-07-24T12:00:05Z",
type: "agent_message",
});
expect(event).toBeDefined();
expect(event!.type).toBe("agent_message_completed");
expect(event!.text).toBe("Done");
});
it("normalizes tool_call", () => {
const event = normalizeSessionEvent({
sequence: 3,
sessionId: "sess-1",
timestamp: "2026-07-24T12:00:01Z",
title: "bash",
toolCallId: "tc-1",
type: "tool_call",
});
expect(event).toBeDefined();
expect(event!.type).toBe("tool_call_started");
expect(event!.toolName).toBe("bash");
expect(event!.toolCallId).toBe("tc-1");
});
it("normalizes permission_request", () => {
const event = normalizeSessionEvent({
sequence: 7,
sessionId: "sess-1",
timestamp: "2026-07-24T12:00:03Z",
type: "permission_request",
});
expect(event).toBeDefined();
expect(event!.type).toBe("permission_requested");
});
it("returns undefined for unmapped types", () => {
const event = normalizeSessionEvent({
sequence: 1,
sessionId: "sess-1",
timestamp: "2026-07-24T12:00:00Z",
type: "session_config",
});
expect(event).toBeUndefined();
});
it("returns undefined when type is missing", () => {
const event = normalizeSessionEvent({
sequence: 1,
sessionId: "sess-1",
});
expect(event).toBeUndefined();
});
it("redacts secrets in persisted event copies", () => {
const event = normalizeSessionEvent({
rawText: "Using api_key=sk-secret123456789012345678",
sequence: 2,
sessionId: "sess-1",
timestamp: "2026-07-24T12:00:00Z",
type: "agent_message_chunk",
});
expect(event).toBeDefined();
expect(event!.text).not.toContain("sk-secret");
});
});

View File

@@ -0,0 +1,216 @@
import { Schema } from "effect";
// ---------------------------------------------------------------------------
// Normalized Orb events — domain-meaningful, secret-free
// ---------------------------------------------------------------------------
export const OrbEventVariant = Schema.Literals([
"session_opened",
"agent_message_chunk",
"agent_message_completed",
"agent_thought_chunk",
"tool_call_started",
"tool_call_completed",
"permission_requested",
"permission_denied",
"command_executed",
"session_failed",
"session_closed",
"vm_booted",
"vm_shutdown",
]);
export type OrbEventVariant = typeof OrbEventVariant.Type;
export const OrbEvent = Schema.Struct({
command: Schema.UndefinedOr(Schema.String),
exitCode: Schema.UndefinedOr(Schema.Int),
sequence: Schema.Number,
text: Schema.UndefinedOr(Schema.String),
timestamp: Schema.String,
toolCallId: Schema.UndefinedOr(Schema.String),
toolName: Schema.UndefinedOr(Schema.String),
type: OrbEventVariant,
});
export type OrbEvent = typeof OrbEvent.Type;
/** Construct an OrbEvent with auto-incrementing sequence and timestamp. */
export const makeOrbEvent = (
sequence: number,
type: OrbEventVariant,
fields?: Partial<Omit<OrbEvent, "sequence" | "timestamp" | "type">>
): OrbEvent =>
({
command: fields?.command ?? undefined,
exitCode: fields?.exitCode ?? undefined,
sequence,
text: fields?.text ?? undefined,
timestamp: new Date().toISOString(),
toolCallId: fields?.toolCallId ?? undefined,
toolName: fields?.toolName ?? undefined,
type,
}) as unknown as OrbEvent;
// ---------------------------------------------------------------------------
// Secret redaction — applied only to persisted/logged event copies,
// never to prompts sent to the model.
// ---------------------------------------------------------------------------
const REDACT_PATTERNS = [
/(?:api[_-]?key|token|secret|password|credential)["'\s:=]+(?<value>[^\s"'},]+)/giu,
/sk-(?<key>[a-zA-Z0-9]{20,})/gu,
/Bearer\s+[a-zA-Z0-9._-]+/gu,
];
export const redactSecrets = (text: string): string => {
let result = text;
for (const pattern of REDACT_PATTERNS) {
result = result.replaceAll(pattern, (match) =>
match.toLowerCase().includes("bearer")
? "Bearer [REDACTED]"
: "[REDACTED]"
);
}
return result;
};
// ---------------------------------------------------------------------------
// Translation from AgentOS session stream entries to normalized OrbEvent
// ---------------------------------------------------------------------------
interface AcpSessionUpdateLike {
readonly type?: string;
readonly sessionUpdate?: string;
readonly rawText?: string;
readonly text?: string;
readonly content?: unknown;
readonly toolCallId?: string | null;
readonly toolCallStatus?: string;
readonly title?: string | null;
readonly sessionId?: string;
}
interface SessionStreamEntryLike {
readonly afterSequence?: number;
readonly content?: unknown;
readonly durability?: string;
readonly rawText?: string;
readonly sequence?: number;
readonly sessionId?: string;
readonly sessionUpdate?: string;
readonly text?: string;
readonly timestamp?: string;
readonly title?: string | null;
readonly toolCallId?: string | null;
readonly type?: string;
}
const IGNORED_TYPES = new Set([
"session_config",
"agent_description",
"agent_capability",
]);
const MESSAGE_TYPES = new Set([
"agent_message_chunk",
"agent_message",
"agent_thought_chunk",
]);
const TOOL_CALL_TYPES = new Set([
"tool_call",
"tool_call_status",
"tool_call_update",
]);
const extractText = (entry: AcpSessionUpdateLike): string | undefined => {
if (entry.rawText !== undefined) {
return entry.rawText;
}
if (entry.text !== undefined) {
return entry.text;
}
if (typeof entry.content === "string") {
return entry.content;
}
if (Array.isArray(entry.content)) {
const texts = entry.content
.filter(
(block): block is { readonly type: string; readonly text?: unknown } =>
typeof block === "object" && block !== null && "type" in block
)
.map((block) => (typeof block.text === "string" ? block.text : undefined))
.filter((text): text is string => text !== undefined);
return texts.length > 0 ? texts.join("") : undefined;
}
return undefined;
};
const optionalToolFields = (
raw: SessionStreamEntryLike
): { toolCallId?: string; toolName?: string } => ({
...(raw.toolCallId === null || raw.toolCallId === undefined
? {}
: { toolCallId: raw.toolCallId }),
...(raw.title === null || raw.title === undefined
? {}
: { toolName: raw.title }),
});
const fromMessage = (
type: string,
sequence: number,
raw: SessionStreamEntryLike
): OrbEvent | undefined => {
const text = extractText(raw);
if (text === undefined) {
// agent_message completes even without text; chunk/thought do not.
return type === "agent_message"
? makeOrbEvent(sequence, "agent_message_completed", {})
: undefined;
}
const variant = (
type === "agent_message" ? "agent_message_completed" : type
) as OrbEventVariant;
return makeOrbEvent(sequence, variant, { text: redactSecrets(text) });
};
const fromToolCall = (
type: string,
sequence: number,
raw: SessionStreamEntryLike
): OrbEvent =>
makeOrbEvent(
sequence,
type === "tool_call" ? "tool_call_started" : "tool_call_completed",
optionalToolFields(raw)
);
/**
* Translate one AgentOS SessionStreamEntry into zero or one normalized OrbEvent.
* Returns undefined for event types that have no domain-meaningful mapping yet.
* Secret redaction is applied so persisted event copies never leak credentials.
*/
export const normalizeSessionEvent = (
raw: SessionStreamEntryLike
): OrbEvent | undefined => {
const type = raw.type ?? raw.sessionUpdate;
if (type === undefined) {
return undefined;
}
const sequence = raw.sequence ?? 0;
if (IGNORED_TYPES.has(type)) {
return undefined;
}
if (type === "permission_request") {
return makeOrbEvent(sequence, "permission_requested", {
text: "Permission requested",
});
}
if (MESSAGE_TYPES.has(type)) {
return fromMessage(type, sequence, raw);
}
if (TOOL_CALL_TYPES.has(type)) {
return fromToolCall(type, sequence, raw);
}
return undefined;
};

View File

@@ -0,0 +1,7 @@
// oxlint-disable-next-line no-barrel-file -- The Orb module exposes its public surface here.
export * from "./domain";
export * from "./events";
export * from "./docker-sandbox";
export * from "./opencode-config";
export * from "./permission-policy";
export * from "./runtime";

View File

@@ -0,0 +1,82 @@
/* eslint-disable no-non-null-assertion -- test assertions on defined objects */
import { Effect } from "effect";
import { describe, expect, it } from "vitest";
import { OrbConfigurationError } from "./domain";
import {
opencodeSetupCommands,
prepareOpenCodeConfig,
} from "./opencode-config";
const validGateway = {
apiKey: "test-key-123",
baseUrl: "https://gateway.example.com/v1",
model: "test-model",
provider: "test-provider",
};
const validContext = {
artifacts: [],
contextFiles: [],
issueBody: "Fix the bug",
issueTitle: "Bug fix",
repositoryUrl: undefined,
};
describe("prepareOpenCodeConfig", () => {
it("produces valid config JSON with provider and model", () => {
const result = Effect.runSync(
prepareOpenCodeConfig({
context: validContext,
gateway: validGateway,
})
);
const parsed = JSON.parse(result.configJson);
expect(parsed.model).toBe("test-provider/test-model");
expect(parsed.provider["test-provider"].baseUrl).toBe(
"https://gateway.example.com/v1"
);
expect(parsed.provider["test-provider"].apiKey).toBe("test-key-123");
});
it("sets config path under opencode config directory", () => {
const result = Effect.runSync(
prepareOpenCodeConfig({
context: validContext,
gateway: validGateway,
})
);
expect(result.configPath).toContain("opencode");
expect(result.configPath).toContain("config.json");
expect(result.instructionsPath).toBe("/workspace/control/issue.md");
});
it("rejects empty base URL", () => {
const error = Effect.runSync(
Effect.flip(
prepareOpenCodeConfig({
context: validContext,
gateway: { ...validGateway, baseUrl: " " },
})
)
);
expect(error).toBeInstanceOf(OrbConfigurationError);
expect(error.reason).toBe("InvalidModelConfig");
});
});
describe("opencodeSetupCommands", () => {
it("produces mkdir, write, and chmod commands", () => {
const config = Effect.runSync(
prepareOpenCodeConfig({
context: validContext,
gateway: validGateway,
})
);
const commands = opencodeSetupCommands(config);
expect(commands.length).toBe(3);
expect(commands[0]).toContain("mkdir");
expect(commands[1]).toContain("cat >");
expect(commands[2]).toContain("chmod 600");
});
});

View File

@@ -0,0 +1,76 @@
import { Effect } from "effect";
import { OrbConfigurationError } from "./domain";
import type { OrbModelGatewayConfig, OrbProjectContext } from "./domain";
// ---------------------------------------------------------------------------
// OpenCode configuration — prepared inside the AgentOS VM filesystem
// ---------------------------------------------------------------------------
const OPENCODE_CONFIG_DIR = "/root/.config/opencode";
const OPENCODE_CONFIG_PATH = `${OPENCODE_CONFIG_DIR}/config.json`;
const AGENT_INSTRUCTIONS_PATH = "/workspace/control/issue.md";
export interface PreparedOpenCodeConfig {
readonly configJson: string;
readonly configPath: string;
readonly instructionsPath: string;
}
const validateGateway = (
gateway: OrbModelGatewayConfig
): Effect.Effect<void, OrbConfigurationError> =>
gateway.baseUrl.trim().length === 0
? Effect.fail(
new OrbConfigurationError({
message: "Model gateway base URL must not be empty",
reason: "InvalidModelConfig",
})
)
: Effect.void;
/**
* Build the OpenCode configuration JSON and file layout for one Orb run.
* The configuration points OpenCode at the model gateway with run-scoped
* credentials injected at runtime — never committed to project files.
*/
export const prepareOpenCodeConfig = Effect.fn("Orb.prepareOpenCodeConfig")(
function* prepareOpenCodeConfig(input: {
readonly context: OrbProjectContext;
readonly gateway: OrbModelGatewayConfig;
}) {
yield* validateGateway(input.gateway);
const config = {
$schema: "https://opencode.ai/config.json",
model: `${input.gateway.provider}/${input.gateway.model}`,
provider: {
[input.gateway.provider]: {
apiKey: input.gateway.apiKey,
baseUrl: input.gateway.baseUrl,
models: {
[input.gateway.model]: {
name: input.gateway.model,
},
},
},
},
};
const configJson = JSON.stringify(config, null, 2);
return {
configJson,
configPath: OPENCODE_CONFIG_PATH,
instructionsPath: AGENT_INSTRUCTIONS_PATH,
} satisfies PreparedOpenCodeConfig;
}
);
/** Shell commands that stage the OpenCode config directory inside a VM. */
export const opencodeSetupCommands = (config: PreparedOpenCodeConfig) =>
[
`mkdir -p ${OPENCODE_CONFIG_DIR}`,
`cat > ${config.configPath} << 'ORB_EOF'\n${config.configJson}\nORB_EOF`,
`chmod 600 ${config.configPath}`,
] as const;

View File

@@ -0,0 +1,107 @@
import { describe, expect, it } from "vitest";
import { evaluatePermission, isDangerousPermission } from "./permission-policy";
const allowOption = { id: "allow", title: "Allow" };
const denyOption = { id: "deny", title: "Deny" };
describe("isDangerousPermission", () => {
it("flags merge operations", () => {
expect(
isDangerousPermission({
requestId: "r1",
toolCall: { title: "git merge main" },
})
).toBe(true);
});
it("flags production deployment", () => {
expect(
isDangerousPermission({
requestId: "r2",
toolCall: { title: "deploy to production" },
})
).toBe(true);
});
it("flags secret access", () => {
expect(
isDangerousPermission({
requestId: "r3",
toolCall: { title: "read secrets" },
})
).toBe(true);
});
it("flags credential access", () => {
expect(
isDangerousPermission({
requestId: "r4",
toolCall: { title: "access credentials" },
})
).toBe(true);
});
it("does not flag safe operations", () => {
expect(
isDangerousPermission({
requestId: "r5",
toolCall: { title: "run tests" },
})
).toBe(false);
});
it("does not flag file edits", () => {
expect(
isDangerousPermission({
requestId: "r6",
toolCall: { title: "edit src/index.ts" },
})
).toBe(false);
});
});
describe("evaluatePermission", () => {
it("allows safe operations and picks allow option", () => {
const decision = evaluatePermission({
options: [allowOption, denyOption],
requestId: "r1",
toolCall: { title: "run bun test" },
});
expect(decision.allow).toBe(true);
expect(decision.optionId).toBe("allow");
});
it("denies dangerous operations and picks deny option", () => {
const decision = evaluatePermission({
options: [allowOption, denyOption],
requestId: "r2",
toolCall: { title: "git merge main" },
});
expect(decision.allow).toBe(false);
expect(decision.optionId).toBe("deny");
});
it("falls back to last option when no explicit deny exists", () => {
const decision = evaluatePermission({
options: [
{ id: "ok", title: "OK" },
{ id: "cancel", title: "Cancel" },
],
requestId: "r3",
toolCall: { title: "deploy to production" },
});
expect(decision.allow).toBe(false);
expect(decision.optionId).toBe("cancel");
});
it("handles empty options", () => {
const decision = evaluatePermission({
options: [],
requestId: "r4",
toolCall: { title: "run tests" },
});
expect(decision.allow).toBe(true);
expect(decision.optionId).toBeUndefined();
});
});

View File

@@ -0,0 +1,90 @@
/**
* Narrow permission policy for Orb sessions.
*
* Denies merge, production deployment, secret access, and external
* communications. Allows all other operations. Used as the callback for
* ACP permission_request events with permissionPolicy: "ask".
*/
interface PermissionOption {
readonly id: string;
readonly title?: string;
readonly description?: string;
}
interface PermissionRequestLike {
readonly requestId: string;
readonly options?: readonly PermissionOption[];
readonly toolCall?: {
readonly title?: string;
readonly kind?: string;
readonly name?: string;
};
}
const DENY_PATTERNS = [
/\bmerge\b/iu,
/\bdeploy\b.*\bprod/iu,
/\bproduction\b/iu,
/\bsecret/iu,
/\bcredential/iu,
/\bpassword\b/iu,
/\bapi[_-]?key\b/iu,
/\bpush\s+to\s+(?<branch>main|master)\b/iu,
];
/** Evaluate whether a permission request is dangerous. */
export const isDangerousPermission = (
request: PermissionRequestLike
): boolean => {
const text = [
request.toolCall?.title,
request.toolCall?.kind,
request.toolCall?.name,
]
.filter((s): s is string => typeof s === "string")
.join(" ");
return DENY_PATTERNS.some((pattern) => pattern.test(text));
};
/**
* Pick the option ID that matches the desired decision. Falls back to the
* last option (typically deny) for safety when no explicit deny option exists,
* or the first option (typically allow) when no explicit allow option exists.
*/
const pickOption = (
options: readonly PermissionOption[],
allow: boolean
): string | undefined => {
if (options.length === 0) {
return undefined;
}
if (allow) {
const match = options.find(
(o) =>
/allow|accept|yes|permit/iu.test(o.title ?? "") ||
/allow|accept|yes|permit/iu.test(o.description ?? "")
);
return match?.id ?? options[0]?.id;
}
const match = options.find(
(o) =>
/deny|reject|no|cancel/iu.test(o.title ?? "") ||
/deny|reject|no|cancel/iu.test(o.description ?? "")
);
return match?.id ?? options.at(-1)?.id;
};
export interface PermissionDecision {
readonly allow: boolean;
readonly optionId: string | undefined;
}
/** Evaluate a permission request and return the decision. */
export const evaluatePermission = (
request: PermissionRequestLike
): PermissionDecision => {
const dangerous = isDangerousPermission(request);
const optionId = pickOption(request.options ?? [], !dangerous);
return { allow: !dangerous, optionId };
};

View File

@@ -0,0 +1,125 @@
/* eslint-disable no-non-null-assertion -- test assertions on defined objects */
import { Effect } from "effect";
import { describe, expect, it } from "vitest";
import { OrbConfigurationError } from "./domain";
import { makeOrbEvent } from "./events";
import type { OrbEvent } from "./events";
import { OrbHandle, OrbRuntime } from "./runtime";
const validGateway = {
apiKey: "test-key",
baseUrl: "https://gw.example.com/v1",
model: "m1",
provider: "p1",
};
const validContext = {
artifacts: [],
contextFiles: [],
issueBody: "Do the thing",
issueTitle: "Thing",
repositoryUrl: undefined,
};
const validIdentity = {
projectId: "prj-1",
runId: "run-1",
workUnitId: "wrk-1",
};
describe("OrbRuntime.createOrb validation", () => {
it("rejects missing gateway API key before touching Docker", () => {
const runtime = new OrbRuntime();
// This must fail at validation, not at Docker — if Docker is the failure,
// that indicates the validation ordering is wrong.
const error = Effect.runSync(
Effect.flip(
runtime.createOrb({
context: validContext,
docker: {
containerName: "orb-test",
hostWorkspacePath: "/tmp/orb-test",
},
gateway: { ...validGateway, apiKey: " " },
identity: validIdentity,
})
)
);
expect(error).toBeInstanceOf(OrbConfigurationError);
expect(error.reason).toBe("MissingGateway");
});
});
describe("OrbHandle event lifecycle", () => {
it("emits events to listeners", () => {
const handle = new OrbHandle(
"orb-x" as never,
"run-x" as never,
validIdentity
);
const events: OrbEvent[] = [];
handle.onEvent((e) => events.push(e));
handle.emitEvent(makeOrbEvent(1, "vm_booted", { text: "booted" }));
handle.emitEvent(
makeOrbEvent(2, "command_executed", { command: "ls", exitCode: 0 })
);
expect(events.length).toBe(2);
expect(events[0]!.type).toBe("vm_booted");
expect(events[1]!.type).toBe("command_executed");
expect(events[1]!.command).toBe("ls");
});
it("unsubscribes listeners correctly", () => {
const handle = new OrbHandle(
"orb-y" as never,
"run-y" as never,
validIdentity
);
const events: OrbEvent[] = [];
const unsub = handle.onEvent((e) => events.push(e));
handle.emitEvent(makeOrbEvent(1, "session_opened"));
unsub();
handle.emitEvent(makeOrbEvent(2, "session_closed"));
expect(events.length).toBe(1);
});
});
describe("OrbHandle state transitions", () => {
it("starts in creating state", () => {
const handle = new OrbHandle(
"orb-z" as never,
"run-z" as never,
validIdentity
);
expect(handle.state).toBe("creating");
});
it("transitions to prepared then running", () => {
const handle = new OrbHandle(
"orb-a" as never,
"run-a" as never,
validIdentity
);
Effect.runSync(handle.setOrbState("prepared"));
expect(handle.state).toBe("prepared");
Effect.runSync(handle.setOrbState("running"));
expect(handle.state).toBe("running");
});
it("rejects invalid transition", () => {
const handle = new OrbHandle(
"orb-b" as never,
"run-b" as never,
validIdentity
);
const error = Effect.runSync(
Effect.flip(handle.setOrbState("needs-input"))
);
expect(error.reason).toBe("InvalidTransition");
});
});

View File

@@ -0,0 +1,763 @@
/* eslint-disable prefer-destructuring -- field captures before mutation are intentional */
/* eslint-disable max-classes-per-file -- runtime and handle form one service. */
import opencodePkg from "@agentos-software/opencode";
import { AgentOs } from "@rivet-dev/agentos-core";
import type { SessionStreamEntry } from "@rivet-dev/agentos-core";
import { Effect } from "effect";
import type { DockerSandboxOptions } from "./docker-sandbox";
import { DockerSandboxProvider } from "./docker-sandbox";
import {
OrbConfigurationError,
OrbSandboxError,
OrbSessionError,
orbActorKey,
transitionOrbState,
transitionRunState,
} from "./domain";
import type {
OrbIdentity,
OrbId,
OrbModelGatewayConfig,
OrbProjectContext,
OrbRunId,
OrbSessionId,
OrbState,
OrbStateError,
RunState,
} from "./domain";
import type { OrbEvent } from "./events";
import { makeOrbEvent, normalizeSessionEvent, redactSecrets } from "./events";
import { prepareOpenCodeConfig } from "./opencode-config";
import { evaluatePermission } from "./permission-policy";
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export interface OrbEnv {
readonly dockerImage?: string;
readonly dockerWorkspace?: string;
readonly rivetEndpoint?: string;
}
export interface OrbCreateInput {
readonly context: OrbProjectContext;
readonly docker: Omit<DockerSandboxOptions, "image">;
readonly gateway: OrbModelGatewayConfig;
readonly identity: OrbIdentity;
}
// ---------------------------------------------------------------------------
// OrbHandle
// ---------------------------------------------------------------------------
export class OrbHandle {
readonly id: OrbId;
readonly runId: OrbRunId;
readonly identity: OrbIdentity;
readonly actorKey: string;
private vm: AgentOs | null = null;
private dockerProvider: DockerSandboxProvider | null = null;
private sessionId: string | undefined;
private orbState: OrbState = "creating";
private runState: RunState = "queued";
private eventSequence = 0;
private readonly eventListeners = new Set<(event: OrbEvent) => void>();
private unsubscribeSession: (() => void) | null = null;
private context: OrbProjectContext | undefined;
private gateway: OrbModelGatewayConfig | undefined;
constructor(id: OrbId, runId: OrbRunId, identity: OrbIdentity) {
this.id = id;
this.runId = runId;
this.identity = identity;
this.actorKey = orbActorKey(identity);
}
get state(): OrbState {
return this.orbState;
}
get currentSessionId(): string | undefined {
return this.sessionId;
}
get docker(): DockerSandboxProvider | null {
return this.dockerProvider;
}
get vmInstance(): AgentOs | null {
return this.vm;
}
// -----------------------------------------------------------------------
// Event API
// -----------------------------------------------------------------------
onEvent(listener: (event: OrbEvent) => void): () => void {
this.eventListeners.add(listener);
return () => {
this.eventListeners.delete(listener);
};
}
emitEvent(event: OrbEvent): void {
for (const listener of this.eventListeners) {
listener(event);
}
}
private nextSequence(): number {
this.eventSequence += 1;
return this.eventSequence;
}
// -----------------------------------------------------------------------
// State transitions
// -----------------------------------------------------------------------
readonly setOrbState = (to: OrbState): Effect.Effect<void, OrbStateError> =>
transitionOrbState({ from: this.orbState, to }).pipe(
Effect.tap((next) =>
Effect.sync(() => {
this.orbState = next;
})
),
Effect.asVoid
);
readonly setRunState = (to: RunState): Effect.Effect<void, OrbStateError> =>
transitionRunState({ from: this.runState, to }).pipe(
Effect.tap((next) =>
Effect.sync(() => {
this.runState = next;
})
),
Effect.asVoid
);
// -----------------------------------------------------------------------
// Internal attachment
// -----------------------------------------------------------------------
_attachVm(vm: AgentOs): void {
this.vm = vm;
this.unsubscribeSession = vm.onSessionEvent((entry: SessionStreamEntry) => {
const normalized = normalizeSessionEvent(entry);
if (normalized) {
this.emitEvent({ ...normalized, sequence: this.nextSequence() });
}
if (
typeof entry === "object" &&
entry !== null &&
"type" in entry &&
entry.type === "permission_request"
) {
void this.handlePermissionRequest(
entry as unknown as {
requestId: string;
options: {
description?: string;
id: string;
title?: string;
}[];
toolCall?: { kind?: string; name?: string; title?: string };
}
);
}
});
this.emitEvent(
makeOrbEvent(this.nextSequence(), "vm_booted", { text: "VM booted" })
);
}
_attachDocker(provider: DockerSandboxProvider): void {
this.dockerProvider = provider;
}
_configure(input: {
readonly context: OrbProjectContext;
readonly gateway: OrbModelGatewayConfig;
}): void {
this.context = input.context;
this.gateway = input.gateway;
}
// -----------------------------------------------------------------------
// Permission handler
// -----------------------------------------------------------------------
private async handlePermissionRequest(request: {
readonly requestId: string;
readonly options: {
description?: string;
id: string;
title?: string;
}[];
readonly toolCall?: { kind?: string; name?: string; title?: string };
}): Promise<void> {
const vm = this.vm;
const sessionId = this.sessionId;
if (!vm || !sessionId) {
return;
}
const decision = evaluatePermission(request);
if (!decision.allow) {
this.emitEvent(
makeOrbEvent(this.nextSequence(), "permission_denied", {
text: `Permission denied for: ${request.toolCall?.title ?? "unknown"}`,
})
);
}
if (decision.optionId) {
await vm
.respondPermission({
optionId: decision.optionId,
requestId: request.requestId,
sessionId,
})
.catch(
// eslint-disable-next-line no-empty-function -- best-effort permission response
() => {}
);
}
}
// -----------------------------------------------------------------------
// Repository preparation
// -----------------------------------------------------------------------
readonly prepareRepository = Effect.fn("Orb.prepareRepository")(
function* prepareRepository(
this: OrbHandle,
input: { readonly baseBranch?: string; readonly branchName?: string }
) {
if (!this.dockerProvider) {
return yield* Effect.fail(
new OrbSandboxError({
message: "Docker sandbox is not attached",
reason: "ContainerStart",
})
);
}
yield* this.setOrbState("prepared");
yield* this.setRunState("provisioning");
const client = yield* Effect.tryPromise({
catch: (cause) =>
new OrbSandboxError({
message: `Failed to start Docker sandbox: ${cause instanceof Error ? cause.message : String(cause)}`,
reason: "ContainerStart",
}),
try: () => {
const dp = this.dockerProvider;
if (!dp) {
throw new Error("Docker provider detached");
}
return dp.start();
},
});
const repoUrl = this.context?.repositoryUrl;
if (repoUrl) {
const branch = input.branchName ?? "main";
const base = input.baseBranch ?? "main";
// eslint-disable-next-line no-use-before-define -- module-level helper
const baseRef = `origin/${base}`;
// eslint-disable-next-line no-use-before-define -- module-level helper
const cloneCmd = `git clone --branch ${shellQuote(base)} --single-branch ${shellQuote(repoUrl)} /home/sandbox/repository || git clone ${shellQuote(repoUrl)} /home/sandbox/repository`;
// eslint-disable-next-line no-use-before-define -- module-level helper
const checkoutCmd = `cd /home/sandbox/repository && git checkout -b ${shellQuote(branch)} ${shellQuote(baseRef)} 2>/dev/null || git checkout ${shellQuote(branch)} 2>/dev/null || true`;
const result = yield* Effect.tryPromise({
catch: (cause) =>
new OrbSandboxError({
message: `Repository checkout failed: ${cause instanceof Error ? cause.message : String(cause)}`,
reason: "CommandFailed",
}),
try: () =>
client.runProcess({
args: ["-c", `${cloneCmd} && ${checkoutCmd}`],
command: "sh",
cwd: "/home/sandbox",
timeoutMs: 300_000,
}),
});
if (result.exitCode !== 0) {
return yield* Effect.fail(
new OrbSandboxError({
message: `Repository clone failed: ${result.stderr}`,
reason: "CommandFailed",
})
);
}
} else {
yield* Effect.tryPromise({
catch: (cause) =>
new OrbSandboxError({
message: `Failed to create workspace: ${cause instanceof Error ? cause.message : String(cause)}`,
reason: "CommandFailed",
}),
try: () =>
client.runProcess({
args: ["-c", "mkdir -p /home/sandbox/repository"],
command: "sh",
cwd: "/home/sandbox",
}),
});
}
const ctx = this.context;
const vm = this.vm;
if (ctx && vm) {
yield* Effect.tryPromise({
catch: () => null, // eslint-disable-next-line no-empty-function -- best-effort context staging
try: () =>
vm
.mkdir("/mnt/sandbox/control", { recursive: true })
.then(() =>
vm.writeFile(
"/mnt/sandbox/control/issue.md",
`# ${ctx.issueTitle}\n\n${ctx.issueBody}\n`
)
),
});
for (const file of ctx.contextFiles) {
yield* Effect.tryPromise({
catch: () => null,
try: () =>
vm.writeFile(`/mnt/sandbox/control/${file.path}`, file.content),
});
}
}
yield* this.setRunState("preparing");
}
);
// -----------------------------------------------------------------------
// OpenCode session
// -----------------------------------------------------------------------
readonly openSession = Effect.fn("Orb.openSession")(
function* openSession(this: OrbHandle) {
if (!this.vm) {
return yield* Effect.fail(
new OrbSessionError({
message: "AgentOS VM is not attached",
reason: "OpenSession",
})
);
}
if (!this.gateway || !this.context) {
return yield* Effect.fail(
new OrbConfigurationError({
message: "Orb is not configured with gateway and context",
reason: "MissingGateway",
})
);
}
const config = yield* prepareOpenCodeConfig({
context: this.context,
gateway: this.gateway,
});
const vm = this.vm;
yield* Effect.tryPromise({
catch: (cause) =>
new OrbSessionError({
message: `Failed to create config directory: ${cause instanceof Error ? cause.message : String(cause)}`,
reason: "OpenSession",
}),
try: () => vm.mkdir("/root/.config/opencode", { recursive: true }),
});
yield* Effect.tryPromise({
catch: (cause) =>
new OrbSessionError({
message: `Failed to write OpenCode config: ${cause instanceof Error ? cause.message : String(cause)}`,
reason: "OpenSession",
}),
try: () => vm.writeFile(config.configPath, config.configJson),
});
// Restrict the config file containing the run-scoped gateway key.
yield* Effect.tryPromise({
catch: () => null, // eslint-disable-next-line no-empty-function -- best-effort hardening
try: () => vm.exec(`chmod 600 ${config.configPath}`),
}).pipe(Effect.ignore);
const agents = yield* Effect.tryPromise({
catch: (cause) =>
new OrbSessionError({
message: `Failed to list agents: ${cause instanceof Error ? cause.message : String(cause)}`,
reason: "AgentNotInstalled",
}),
try: () => vm.listAgents(),
});
const hasOpencode = agents.some(
(a) => a.id === "opencode" && a.installed
);
if (!hasOpencode) {
return yield* Effect.fail(
new OrbSessionError({
message: "OpenCode agent is not installed in the VM",
reason: "AgentNotInstalled",
})
);
}
yield* Effect.tryPromise({
catch: (cause) =>
new OrbSessionError({
message: `Failed to open OpenCode session: ${cause instanceof Error ? cause.message : String(cause)}`,
reason: "OpenSession",
}),
try: () =>
vm.openSession({
agent: "opencode",
cwd: "/mnt/sandbox/repository",
permissionPolicy: "ask",
skipOsInstructions: false,
}),
});
const sessionInfo = yield* Effect.tryPromise({
catch: (cause) =>
new OrbSessionError({
message: `Failed to read session info: ${cause instanceof Error ? cause.message : String(cause)}`,
reason: "OpenSession",
}),
try: () => vm.getSession(),
});
this.sessionId = sessionInfo.sessionId;
this.emitEvent(
makeOrbEvent(this.nextSequence(), "session_opened", {
text: `Session ${sessionInfo.sessionId} opened`,
})
);
yield* this.setOrbState("running");
yield* this.setRunState("running");
return sessionInfo.sessionId as OrbSessionId;
}
);
// -----------------------------------------------------------------------
// Send task — raw prompt to model, redacted copy in events only
// -----------------------------------------------------------------------
readonly sendTask = Effect.fn("Orb.sendTask")(function* sendTask(
this: OrbHandle,
prompt: string
) {
if (!this.vm || !this.sessionId) {
return yield* Effect.fail(
new OrbSessionError({
message: "No active OpenCode session",
reason: "SessionNotFound",
})
);
}
// eslint-disable-next-line prefer-destructuring -- captured before mutation
const vm = this.vm;
// eslint-disable-next-line prefer-destructuring -- captured before mutation
const sessionId = this.sessionId;
// Send raw prompt — no redaction of outgoing content.
const result = yield* Effect.tryPromise({
catch: (cause) =>
new OrbSessionError({
message: `Prompt failed: ${cause instanceof Error ? cause.message : String(cause)}`,
reason: "PromptFailed",
}),
try: () =>
vm.prompt({
content: [{ text: prompt, type: "text" }],
sessionId,
}),
});
return result;
});
// -----------------------------------------------------------------------
// Execute command via Docker
// -----------------------------------------------------------------------
readonly executeCommand = Effect.fn("Orb.executeCommand")(
function* executeCommand(
this: OrbHandle,
input: {
readonly command: string;
readonly cwd?: string;
readonly env?: Readonly<Record<string, string>>;
readonly timeoutMs?: number;
}
) {
if (!this.dockerProvider) {
return yield* Effect.fail(
new OrbSandboxError({
message: "Docker sandbox is not attached",
reason: "ContainerStart",
})
);
}
const client = yield* Effect.tryPromise({
catch: (cause) =>
new OrbSandboxError({
message: `Failed to get sandbox client: ${cause instanceof Error ? cause.message : String(cause)}`,
reason: "ContainerStart",
}),
try: () => {
const dp = this.dockerProvider;
if (!dp) {
throw new Error("Docker provider detached");
}
return dp.start();
},
});
const result = yield* Effect.tryPromise({
catch: (cause) =>
new OrbSandboxError({
message: `Command failed: ${cause instanceof Error ? cause.message : String(cause)}`,
reason: "CommandFailed",
}),
try: () =>
client.runProcess({
args: ["-c", input.command],
command: "sh",
...(input.cwd === undefined ? {} : { cwd: input.cwd }),
...(input.env === undefined ? {} : { env: input.env }),
...(input.timeoutMs === undefined
? {}
: { timeoutMs: input.timeoutMs }),
}),
});
// Emit redacted copy for logs/UI.
this.emitEvent(
makeOrbEvent(this.nextSequence(), "command_executed", {
command: redactSecrets(input.command),
exitCode: result.exitCode ?? undefined,
})
);
return result;
}
);
// -----------------------------------------------------------------------
// Cancel / dispose
// -----------------------------------------------------------------------
readonly cancel = Effect.fn("Orb.cancel")(function* cancel(this: OrbHandle) {
// eslint-disable-next-line prefer-destructuring -- captured before mutation
const vm = this.vm;
// eslint-disable-next-line prefer-destructuring -- captured before mutation
const sessionId = this.sessionId;
if (vm && sessionId) {
yield* Effect.tryPromise({
catch: () => null,
try: () => vm.cancelPrompt({ sessionId }),
}).pipe(Effect.ignore);
}
yield* this.setOrbState("cancelled");
yield* this.setRunState("cancelled");
this.emitEvent(
makeOrbEvent(this.nextSequence(), "session_closed", {
text: "Orb cancelled",
})
);
});
readonly dispose = Effect.fn("Orb.dispose")(
function* dispose(this: OrbHandle) {
// eslint-disable-next-line prefer-destructuring -- captured before mutation
const vm = this.vm;
// eslint-disable-next-line prefer-destructuring -- captured before mutation
const sessionId = this.sessionId;
// eslint-disable-next-line prefer-destructuring -- captured before mutation
const docker = this.dockerProvider;
if (vm && sessionId) {
yield* Effect.tryPromise({
catch: () => null,
try: () => vm.cancelPrompt({ sessionId }),
}).pipe(Effect.ignore);
}
// Dispose the VM before its sandbox so the mount unbinds cleanly.
if (vm) {
if (this.unsubscribeSession) {
this.unsubscribeSession();
this.unsubscribeSession = null;
}
yield* Effect.tryPromise({
catch: () => null,
try: () => vm.dispose(),
}).pipe(Effect.ignore);
}
if (docker) {
yield* Effect.tryPromise({
catch: () => null,
try: () => docker.dispose(),
}).pipe(Effect.ignore);
}
yield* this.setOrbState("disposed");
this.eventListeners.clear();
this.vm = null;
this.dockerProvider = null;
this.sessionId = undefined;
}
);
}
// ---------------------------------------------------------------------------
// OrbRuntime
// ---------------------------------------------------------------------------
export class OrbRuntime {
private readonly orbs = new Map<string, OrbHandle>();
private readonly env: OrbEnv;
constructor(env: OrbEnv = {}) {
this.env = env;
}
readonly createOrb = Effect.fn("OrbRuntime.createOrb")(function* createOrb(
this: OrbRuntime,
input: OrbCreateInput
) {
if (!input.gateway.apiKey.trim()) {
return yield* Effect.fail(
new OrbConfigurationError({
message: "Model gateway API key is required",
reason: "MissingGateway",
})
);
}
const orbId =
`orb-${input.identity.projectId}-${input.identity.runId}` as OrbId;
const runId = `run-${input.identity.runId}` as OrbRunId;
const handle = new OrbHandle(orbId, runId, input.identity);
handle._configure({
context: input.context,
gateway: input.gateway,
});
// 1. Create and start Docker sandbox (the provider owns its container).
const dockerOptions: DockerSandboxOptions = {
...input.docker,
...(this.env.dockerImage === undefined
? {}
: { image: this.env.dockerImage }),
};
const provider = yield* DockerSandboxProvider.create(dockerOptions);
handle._attachDocker(provider);
// 2-3. Start the sandbox, create the AgentOS VM, and link OpenCode. Any
// failure here disposes the VM before its sandbox so neither leaks.
let createdVm: AgentOs | null = null;
const vm = yield* Effect.gen(function* vm() {
const sandboxClient = yield* Effect.tryPromise({
catch: (cause) =>
new OrbSandboxError({
message: `Failed to start Docker sandbox: ${cause instanceof Error ? cause.message : String(cause)}`,
reason: "ContainerStart",
}),
try: () => provider.start(),
});
const created = yield* Effect.tryPromise({
catch: (cause) =>
new OrbSessionError({
message: `Failed to create AgentOS VM: ${cause instanceof Error ? cause.message : String(cause)}`,
reason: "OpenSession",
}),
try: () =>
AgentOs.create({
database: {
path: `/tmp/${orbId}.db`,
type: "sqlite_file",
},
sandbox: {
client: sandboxClient,
dispose: false,
mountPath: "/mnt/sandbox",
readOnly: false,
sandboxRoot: "/home/sandbox",
},
software: [opencodePkg],
}),
});
createdVm = created;
yield* Effect.tryPromise({
catch: (cause) =>
new OrbSessionError({
message: `Failed to link OpenCode: ${cause instanceof Error ? cause.message : String(cause)}`,
reason: "AgentNotInstalled",
}),
try: () => created.linkSoftware({ path: opencodePkg.packagePath }),
});
return created;
}).pipe(
Effect.tapError(() =>
Effect.tryPromise({
catch: () => null,
// eslint-disable-next-line no-use-before-define -- module-level cleanup helper
try: () => disposeVmBeforeSandbox(createdVm, provider),
}).pipe(Effect.ignore)
)
);
// 4. Attach VM to handle.
handle._attachVm(vm);
this.orbs.set(orbId, handle);
return handle;
});
getOrb(id: string): OrbHandle | undefined {
return this.orbs.get(id);
}
listOrbs(): readonly OrbHandle[] {
return [...this.orbs.values()];
}
}
// ---------------------------------------------------------------------------
// Partial-failure cleanup — dispose the VM before its sandbox
// ---------------------------------------------------------------------------
const disposeVmBeforeSandbox = async (
vm: AgentOs | null,
docker: DockerSandboxProvider
): Promise<void> => {
if (vm) {
try {
await vm.dispose();
} catch {
// best-effort; container removal below is the hard guarantee
}
}
await docker.dispose();
};
// ---------------------------------------------------------------------------
// Shell quoting
// ---------------------------------------------------------------------------
// eslint-disable-next-line no-use-before-define -- module-level helper
const shellQuote = (value: string): string =>
`'${value.replaceAll("'", `'"'"'`)}'`;

370
scripts/orb-proof.ts Normal file
View File

@@ -0,0 +1,370 @@
/**
* Orb Runtime Proof Fixture
*
* Opt-in integration proof that exercises the Orb lifecycle in distinct,
* independently reported stages, each with a stable marker:
*
* STAGE 1 Docker sandbox lifecycle ORB_STAGE_DOCKER_OK / _BLOCKED
* STAGE 2 AgentOS VM + OpenCode session ORB_STAGE_AGENTOS_OK / _BLOCKED
* STAGE 3 Model turn via the gateway ORB_STAGE_MODEL_TURN_OK / _BLOCKED
*
* A missing or unreachable model gateway is BLOCKED, never passed. The proof
* only prints ORB_PROOF_PASSED when the model turn completes. It never prints
* gateway secrets; only provider/model identifiers and reachability labels.
*
* Gateway credentials resolve from ORB_GATEWAY_* with safe fallbacks to the
* shared AGENT_MODEL_* variables. Container removal is always verified after
* disposal.
*
* Usage:
* ORB_PROOF=1 \
* ORB_GATEWAY_API_KEY=... \
* ORB_GATEWAY_BASE_URL=https://ai.example.com/v1 \
* ORB_GATEWAY_MODEL=glm-5.2 \
* ORB_GATEWAY_PROVIDER=cheaptricks \
* bun run scripts/orb-proof.ts
*/
/* eslint-disable no-console -- proof fixture is a CLI tool */
import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { Effect } from "effect";
import { DockerSandboxProvider } from "../packages/agents/src/orb/docker-sandbox";
import type { DockerSandboxOptions } from "../packages/agents/src/orb/docker-sandbox";
import type { OrbEvent } from "../packages/agents/src/orb/events";
import { OrbRuntime } from "../packages/agents/src/orb/runtime";
import type { OrbHandle } from "../packages/agents/src/orb/runtime";
const MARKER = {
blocked: "ORB_PROOF_BLOCKED",
failed: "ORB_PROOF_FAILED",
passed: "ORB_PROOF_PASSED",
} as const;
const STAGE = {
agentosBlocked: "ORB_STAGE_AGENTOS_BLOCKED",
agentosOk: "ORB_STAGE_AGENTOS_OK",
dockerBlocked: "ORB_STAGE_DOCKER_BLOCKED",
dockerOk: "ORB_STAGE_DOCKER_OK",
modelBlocked: "ORB_STAGE_MODEL_TURN_BLOCKED",
modelOk: "ORB_STAGE_MODEL_TURN_OK",
} as const;
interface Gateway {
apiKey: string;
baseUrl: string;
model: string;
provider: string;
}
const envValue = (key: string): string | undefined => process.env[key];
const resolveGateway = (): Gateway | null => {
const apiKey =
envValue("ORB_GATEWAY_API_KEY") ?? envValue("AGENT_MODEL_API_KEY");
const baseUrl =
envValue("ORB_GATEWAY_BASE_URL") ?? envValue("AGENT_MODEL_BASE_URL");
const model = envValue("ORB_GATEWAY_MODEL") ?? envValue("AGENT_MODEL_NAME");
const provider =
envValue("ORB_GATEWAY_PROVIDER") ?? envValue("AGENT_MODEL_PROVIDER");
if (
!apiKey?.trim() ||
!baseUrl?.trim() ||
!model?.trim() ||
!provider?.trim()
) {
return null;
}
return { apiKey, baseUrl, model, provider };
};
const runCli = async (args: readonly string[]): Promise<string> => {
try {
const proc = Bun.spawn([...args], { stderr: "pipe", stdout: "pipe" });
const stdout = await new Response(proc.stdout).text();
await proc.exited;
return stdout;
} catch {
return "";
}
};
const dockerVersion = async (): Promise<string | null> => {
const out = await runCli([
"docker",
"version",
"--format",
"{{.Server.Version}}",
]);
const trimmed = out.trim();
return trimmed.length > 0 ? trimmed : null;
};
const TINY_PROJECT: Record<string, string> = {
"index.test.ts": `import { test } from "node:test";\nimport assert from "node:assert/strict";\nimport { add } from "./index.ts";\n\ntest("add", () => {\n assert.equal(add(1, 2), 3);\n});\n`,
"index.ts": `export function add(a: number, b: number): number {\n return a + b;\n}\n`,
"package.json": `{"name":"tiny","type":"module","scripts":{"test":"node --test"}}\n`,
};
const prepareProject = async (hostWorkspace: string): Promise<void> => {
await mkdir(path.join(hostWorkspace, "repository"), { recursive: true });
for (const [file, content] of Object.entries(TINY_PROJECT)) {
// eslint-disable-next-line no-await-in-loop -- sequential file writes are intentional
await writeFile(path.join(hostWorkspace, "repository", file), content);
}
};
// ---------------------------------------------------------------------------
// STAGE 1 — Docker sandbox via SandboxAgent (standalone, no sidecar required)
// ---------------------------------------------------------------------------
const stageDocker = async (image: string | undefined): Promise<boolean> => {
const version = await dockerVersion();
if (!version) {
console.log("[docker] Docker daemon not available");
console.log(STAGE.dockerBlocked);
return false;
}
console.log(`[docker] server version ${version}`);
const hostWorkspace = `/tmp/orb-proof-docker-${Date.now()}`;
const options: DockerSandboxOptions =
image === undefined
? { hostWorkspacePath: hostWorkspace }
: { hostWorkspacePath: hostWorkspace, image };
try {
await prepareProject(hostWorkspace);
const provider = await Effect.runPromise(
DockerSandboxProvider.create(options)
);
const client = await provider.start();
const { baseUrl } = client as unknown as { baseUrl: string };
console.log(`[docker] SandboxAgent baseUrl: ${baseUrl}`);
console.log("[docker] running npm install in sandbox...");
const install = await client.runProcess({
args: ["-c", "npm install"],
command: "sh",
cwd: "/home/sandbox/repository",
timeoutMs: 60_000,
});
console.log(` npm install exit: ${install.exitCode}`);
await provider.dispose();
if (install.exitCode !== 0) {
console.log("[docker] sandbox lifecycle did not complete cleanly");
console.log(STAGE.dockerBlocked);
return false;
}
console.log(STAGE.dockerOk);
return true;
} catch (error) {
console.log(
`[docker] stage failed: ${error instanceof Error ? error.message : String(error)}`
);
console.log(STAGE.dockerBlocked);
return false;
}
};
// ---------------------------------------------------------------------------
// STAGE 2 — AgentOS VM + OpenCode session
// ---------------------------------------------------------------------------
const stageAgentOs = async (
gateway: Gateway,
image: string | undefined,
events: OrbEvent[]
): Promise<{ container: string; handle: OrbHandle } | null> => {
const container = `orb-proof-${Date.now()}`;
const hostWorkspace = `/tmp/orb-proof-${Date.now()}`;
const runtime = new OrbRuntime(
image === undefined ? {} : { dockerImage: image }
);
const createResult = await Effect.runPromise(
Effect.match(
runtime.createOrb({
context: {
artifacts: [],
contextFiles: [],
issueBody: "Run npm test and report the result.",
issueTitle: "Proof: run tests",
},
docker: { hostWorkspacePath: hostWorkspace },
gateway,
identity: {
projectId: "proof",
runId: `proof-${Date.now()}`,
workUnitId: "proof-wu",
},
}),
{
onFailure: (error) => ({ error, ok: false as const }),
onSuccess: (handle) => ({ handle, ok: true as const }),
}
)
);
if (!createResult.ok) {
console.log(
`[agentos] creation failed (${createResult.error.reason}): ${createResult.error.message}`
);
console.log(STAGE.agentosBlocked);
return null;
}
const { handle } = createResult;
handle.onEvent((event) => {
events.push(event);
const summary = (event.text ?? event.command ?? event.type).slice(0, 160);
console.log(` [event] ${event.type}: ${summary}`);
});
try {
const vm = handle.vmInstance;
if (!vm) {
throw new Error("AgentOS VM is not attached");
}
const agents = await vm.listAgents();
const opencode = agents.find((a) => a.id === "opencode" && a.installed);
if (!opencode) {
throw new Error("OpenCode agent is not installed in the VM");
}
console.log("[agentos] OpenCode agent available");
await Effect.runPromise(handle.openSession());
console.log(`[agentos] session opened: ${handle.currentSessionId ?? "?"}`);
console.log(STAGE.agentosOk);
return { container, handle };
} catch (error) {
console.log(
`[agentos] stage failed: ${error instanceof Error ? error.message : String(error)}`
);
await Effect.runPromise(handle.dispose().pipe(Effect.ignore)).catch(() => {
// best-effort cleanup
});
console.log(STAGE.agentosBlocked);
return null;
}
};
// ---------------------------------------------------------------------------
// STAGE 3 — Model turn through the gateway
// ---------------------------------------------------------------------------
const stageModelTurn = async (handle: OrbHandle): Promise<boolean> => {
const vm = handle.vmInstance;
const sessionId = handle.currentSessionId;
if (!vm || !sessionId) {
console.log("[model] no active session to drive a model turn");
console.log(STAGE.modelBlocked);
return false;
}
try {
console.log("[model] sending bounded turn to gateway...");
const result = await vm.prompt({
content: [
{ text: "Reply with exactly: ORB_PROOF_MODEL_ECHO", type: "text" },
],
sessionId,
});
console.log(
`[model] turn completed (stopReason=${result.stopReason ?? "?"})`
);
console.log(STAGE.modelOk);
return true;
} catch (error) {
// An unreachable/missing gateway surfaces here as a prompt failure.
console.log(
`[model] gateway turn failed: ${error instanceof Error ? error.message : String(error)}`
);
console.log(STAGE.modelBlocked);
return false;
}
};
// ---------------------------------------------------------------------------
// Orchestration
// ---------------------------------------------------------------------------
const runProof = async (): Promise<number> => {
const image = envValue("ORB_DOCKER_IMAGE");
const events: OrbEvent[] = [];
// STAGE 1 — Docker sandbox.
console.log("[stage 1] Docker sandbox lifecycle...");
const dockerOk = await stageDocker(image);
if (!dockerOk) {
console.log(`\n${MARKER.blocked}`);
return 2;
}
// STAGE 2 + 3 — AgentOS/OpenCode + model turn.
const gateway = resolveGateway();
if (!gateway) {
console.error(
"[gateway] missing config: set ORB_GATEWAY_* (or AGENT_MODEL_* fallbacks)."
);
console.log(STAGE.agentosBlocked);
console.log(`\n${MARKER.blocked}`);
return 2;
}
// Identifiers only — never the key or base URL.
console.log(`[gateway] provider=${gateway.provider} model=${gateway.model}`);
console.log("[stage 2] AgentOS VM + OpenCode session...");
const orb = await stageAgentOs(gateway, image, events);
if (!orb) {
console.log(`\n${MARKER.blocked}`);
return 2;
}
console.log("[stage 3] model turn...");
let modelOk = false;
try {
modelOk = await stageModelTurn(orb.handle);
} finally {
console.log("[dispose] disposing Orb...");
await Effect.runPromise(orb.handle.dispose().pipe(Effect.ignore)).catch(
() => {
// best-effort cleanup
}
);
console.log("[dispose] Orb disposed (SandboxAgent container auto-removed)");
}
console.log(
`\nStreamed ${events.length} normalized events (${[...new Set(events.map((e) => e.type))].join(", ")}).`
);
if (modelOk) {
console.log(`\n${MARKER.passed}`);
return 0;
}
console.log(`\n${MARKER.blocked}`);
return 2;
};
// Entry point
if (envValue("ORB_PROOF") !== "1") {
console.error(
"Set ORB_PROOF=1 to run the proof fixture (requires Docker + model gateway)."
);
console.error(MARKER.blocked);
process.exit(2);
}
(async () => {
try {
const code = await runProof();
process.exit(code);
} catch (error) {
console.error("[proof] unexpected failure:", error);
console.error(MARKER.failed);
process.exit(1);
}
})();