diff --git a/.gitignore b/.gitignore index 9b55bf51..6b744bb1 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ work/bombadil-src/ .env.* .turbo/ work/ +runtime-distributions/codex/ apps/synth_desktop/src-tauri/generated-resources/ apps/synth_desktop/browser/runtime runtime-distributions/trace/ diff --git a/PUBLIC_EXPORT_MANIFEST.json b/PUBLIC_EXPORT_MANIFEST.json index 624d4c57..4cea79af 100644 --- a/PUBLIC_EXPORT_MANIFEST.json +++ b/PUBLIC_EXPORT_MANIFEST.json @@ -25,10 +25,10 @@ "sha256": "67f4f45d3477325d1f91edb1a493a27215dacacb755dba0d379a4bd4d151b142" }, { - "bytes": 670, + "bytes": 699, "mode": "100644", "path": ".gitignore", - "sha256": "394215016871aab847b5a8984b3d96b4e967671891afecde7dfd3a9f5932ecd3" + "sha256": "9e93c1f315df3c0921b899953ac6ec07774ce823977b398a035ce73ceaf5c624" }, { "bytes": 3, @@ -67,10 +67,10 @@ "sha256": "49898a5b3fba843f274e014e99a57fad372bb2cebf2f827a43d6dbb3420148d0" }, { - "bytes": 7762, + "bytes": 10694, "mode": "100644", "path": "README.md", - "sha256": "e6574f787ed372c9672520951a7bd7f33dfc2ce60f7e198370c8f2017e7afd0e" + "sha256": "ef1572c89956c1e652010ec6bc8cfffb76585f84276a50bb650db045f0ef156c" }, { "bytes": 633, @@ -1051,10 +1051,10 @@ "sha256": "908ab5f929066f14fc27ae2e1bda3630c73d6bc1933d7ffc4665b7764cc69457" }, { - "bytes": 32486, + "bytes": 33386, "mode": "100644", "path": "apps/synth_desktop/src-tauri/src/codex_oauth.rs", - "sha256": "2d88189842d4f8311100f7e6baca9a2e15bae73fbb1da5a35113f78182ea747b" + "sha256": "f5b9901b4e3952063c13c2bc64935538ab3dbd49375a79467fdce38e784638de" }, { "bytes": 119, @@ -1645,10 +1645,10 @@ "sha256": "7b33991eb0340ba474e3d828ea420ac86176a251b6264aefc0e4aa3b0dd2b935" }, { - "bytes": 23521, + "bytes": 24038, "mode": "100644", "path": "apps/synth_desktop/src-tauri/src/model_catalog.rs", - "sha256": "9be5dc1e76d2e86b5c304e398a626d77c8ed3c83b61962449b2a24e621b22ec1" + "sha256": "3d60868f708a4d1414bb3ec3131d7b8c9c2d1c59397bd7fd67fdd74f61f462a2" }, { "bytes": 6111, @@ -2593,10 +2593,10 @@ "sha256": "d235803e4919c54d3b24baeacaed5899e10eb26b93956d0cebeaa57fdd9bde41" }, { - "bytes": 45592, + "bytes": 48207, "mode": "100644", "path": "apps/synth_desktop/src-tauri/src/session/codex/event_pump.rs", - "sha256": "4ff1fdfe2c70a4cf8e9e3162dc9d92fd88d9c5c70a888cd2921e5b8a40a265e0" + "sha256": "849c320de77da5cd5da6238a4af07ebc7ba936e04f4d10c828a098c4de943cf1" }, { "bytes": 47523, @@ -2611,16 +2611,16 @@ "sha256": "0e5cc452ba0589aa7983bfaf36230f66b663932d9590eb808d22f7c07cd835ee" }, { - "bytes": 53157, + "bytes": 53323, "mode": "100644", "path": "apps/synth_desktop/src-tauri/src/session/codex/home.rs", - "sha256": "1c4dde3dd33c466a18e0e491b6b5cb7868a4db9e5b39be217bc6e279baa8b370" + "sha256": "9ff949434361a3f650487283e876c86bbc63ec6f024a0f96f3b64498b87a8124" }, { - "bytes": 80126, + "bytes": 80748, "mode": "100644", "path": "apps/synth_desktop/src-tauri/src/session/codex/manager.rs", - "sha256": "cf315b1835377ea7f74b9e54383398d51746acdb6f6e3bd94b9d108a0a817763" + "sha256": "dd524d121c1c713f3a84d7560e3973cea8a448ee3cac6266ca839f37ed3f13f3" }, { "bytes": 874, @@ -3037,10 +3037,10 @@ "sha256": "eb093a67c030ede668cd59cc2a391a5eeda660c824837df6b295b7aa4e43376a" }, { - "bytes": 1269, + "bytes": 1333, "mode": "100644", "path": "apps/synth_desktop/src-tauri/tauri.package.json", - "sha256": "cc963c7e8b0e3d25479b2065e93fae4f7fe77a396c2f87e4ca9110e47bec493b" + "sha256": "73f01b5cf64f68a2f1906d4ffd6a4e7a9678bbf24837740b3fba46eddc9561b6" }, { "bytes": 2737, @@ -3625,10 +3625,10 @@ "sha256": "ad4cc64383218f3a352408a8f886df1bef808acfbc1aa65782f6861f80bdbc68" }, { - "bytes": 50648, + "bytes": 50691, "mode": "100644", "path": "apps/synth_desktop/src/renderer/src/bridge/types.ts", - "sha256": "3dd68b448d6fa3ca1163243faa3b6e0c0d5cde2b7c080a3041fab9baf4174888" + "sha256": "960e54ad12b555b79282195cb1629924b02bc7d05e5f41a92e41e17e5575ee2c" }, { "bytes": 10320, @@ -3697,10 +3697,10 @@ "sha256": "2fad9a83cd0b6aff0461106dd00fdaa3695608d89a4d9d33de7deb3b2c8a38f4" }, { - "bytes": 7735, + "bytes": 9885, "mode": "100644", "path": "apps/synth_desktop/src/renderer/src/components/ChatgptCodexSubscriptionCard.tsx", - "sha256": "be0ca980bf1beb053d3fca6000529da8f66ed8dd790c983e247973a725003ef8" + "sha256": "3c4196e5fffb84fa314a5200f33d7ddbb330ef79dc58c6f7b10946e3f33dd54f" }, { "bytes": 10392, @@ -3924,6 +3924,12 @@ "path": "apps/synth_desktop/src/renderer/src/components/OnDeviceModelsSettings.tsx", "sha256": "3f256f7bea7159bcbede5bf73b5ac0a9b32e3c82f78ac183cc9ac95a028573fd" }, + { + "bytes": 3446, + "mode": "100644", + "path": "apps/synth_desktop/src/renderer/src/components/OpenRouterSetupCard.tsx", + "sha256": "c79dd223abcc303ee4a1904b61ffd3ff15d6177bb0dae39dc8ab357cb93b0c2c" + }, { "bytes": 2621, "mode": "100644", @@ -4039,10 +4045,10 @@ "sha256": "d661ff70db1aa19a6920d39dbd91cd0e065592d3a34f62804318ec5585151e01" }, { - "bytes": 28666, + "bytes": 28758, "mode": "100644", "path": "apps/synth_desktop/src/renderer/src/components/SettingsPage.tsx", - "sha256": "ffa275ec0aaa98446626ae53f1b3eaf82aa92cdd51df30841bf40cf14067e174" + "sha256": "eb236c78d3417922c41db8d03e6f193bc6f00a5bbc7b86a15f5c6d269c5e774c" }, { "bytes": 32778, @@ -4111,10 +4117,10 @@ "sha256": "803caaa12289784dd4b4d440ac3578f951eba31e7424907b29e59ec0934821af" }, { - "bytes": 76499, + "bytes": 76639, "mode": "100644", "path": "apps/synth_desktop/src/renderer/src/components/VisualHost.tsx", - "sha256": "bf6d2c453cb1ec6e151d99d78beacf6581ab061a026282becc9c9097d812de79" + "sha256": "842d8e98c5d75224f58c312a6fe2d014d29cf1f1cb461f72a365b674c3c288d2" }, { "bytes": 5258, @@ -4621,10 +4627,10 @@ "sha256": "5393bae999969f16323f3ce32bda11c072ed32b909400cfc54fbc6b58b39f993" }, { - "bytes": 71315, + "bytes": 71519, "mode": "100644", "path": "apps/synth_desktop/src/renderer/src/runtime/desktopBridge.ts", - "sha256": "c070ead99014105ea85c73456304cbfa6d21b9e78af90bea11941a8623dbb79f" + "sha256": "04e680cf34e16d78165825added7404c4dd9bd1706e83326ca97459303234dfb" }, { "bytes": 1117, @@ -4675,22 +4681,22 @@ "sha256": "036fcf289f475afbc5f2f1efae2165b154aa8d7e35c9675a580100c136f61d43" }, { - "bytes": 11719, + "bytes": 12116, "mode": "100644", "path": "apps/synth_desktop/src/renderer/src/runtime/modelCapabilities.ts", - "sha256": "9c599d540a66621950d03a3adb9a8de910964ff7ded4519c8fe24f8fafe7e1d9" + "sha256": "8d54ac93caa364548398917e69f641536de3b04c088aca48505ce660494fc965" }, { - "bytes": 5354, + "bytes": 5472, "mode": "100644", "path": "apps/synth_desktop/src/renderer/src/runtime/modelCatalog.ts", - "sha256": "6c2d24e1470803322cfb3a2825af2ed3c7dcca9e55062885df408e749d151e1c" + "sha256": "c5147bdf09a33c76748e55b4e4b6211321086afcdc38d55cfbe419809b6ea7a0" }, { - "bytes": 1746, + "bytes": 1839, "mode": "100644", "path": "apps/synth_desktop/src/renderer/src/runtime/modelPerformanceLabels.ts", - "sha256": "690b3e978abc9cfe3b7657d1a570d86b000a62d72f5440d5dd350a4a36d2e441" + "sha256": "18406132b63bd04aefdbdd9070be7a6ba441ae745273ea1a7c9dbb3f243fe76c" }, { "bytes": 433, @@ -4705,10 +4711,10 @@ "sha256": "6241545b3058f07d3e0d854f0e312b5e111031956c24ba8848fa1bae2c5e0e99" }, { - "bytes": 11098, + "bytes": 11320, "mode": "100644", "path": "apps/synth_desktop/src/renderer/src/runtime/nativeCodex.ts", - "sha256": "d2542ade03b05c0bf172864a3cd2ca3df6cb05411ad0768d8b6c3a53e3b12d55" + "sha256": "3e92862dba55f019d15c44a490573b7339356b16336de160857754a5739fd795" }, { "bytes": 5206, @@ -4903,10 +4909,10 @@ "sha256": "4b7e12a6496187e9a657b495b25fd3625a301d78afebcccd94e16711a569abff" }, { - "bytes": 92358, + "bytes": 92575, "mode": "100644", "path": "apps/synth_desktop/src/renderer/src/runtime/sessionView.ts", - "sha256": "52d02c2e18994d52247814fafe7e7641366fcd6881516ee9acb2c61e4e55c2e6" + "sha256": "03ee5e98777c0221a75ef69e6aee99e687c4e7b8310a8e86876ef49cccd78f5a" }, { "bytes": 166, @@ -5053,10 +5059,10 @@ "sha256": "d73cfb7c340ea6da90f946d1a540ac7c7f575d4b75909099cc249195197716fd" }, { - "bytes": 19097, + "bytes": 19647, "mode": "100644", "path": "apps/synth_desktop/src/renderer/src/types/landing.ts", - "sha256": "3a423525c182141cc2c940a72a6d516fe42b17ff31302f99a2325dd8dcbd167f" + "sha256": "84dd73676d10d00f1a9407586498e90cf59cfa8df87142dfd9ab9311ad27923e" }, { "bytes": 1911, @@ -5172,6 +5178,180 @@ "path": "docs/sft_tinker_base_models.toml", "sha256": "079255628bef49ec64d64c8cc7402d7153382476b40529e30fd654d710b0565d" }, + { + "bytes": 278, + "mode": "100644", + "path": "examples/runebench/.gitignore", + "sha256": "e8895bc3cd0a6ce7934c08eeadf3293e2df9abc5f4484cb903be476173bf1767" + }, + { + "bytes": 832, + "mode": "100644", + "path": "examples/runebench/Dockerfile", + "sha256": "924ac39db152c5864e5a3f436d3c829d0e79f35fa1ef16d3c56d73de2cf68752" + }, + { + "bytes": 1066, + "mode": "100644", + "path": "examples/runebench/LICENSE.rs-sdk", + "sha256": "d82581c022eba07df3d8abcbaab32f8e43a5a8adc99690925d4843ed724e9f1b" + }, + { + "bytes": 1383, + "mode": "100644", + "path": "examples/runebench/PROMPT.md", + "sha256": "6dd8f878809f4e6f7ba9fb73dcea542df81283458fe18881f9aeb04a53fe4c4c" + }, + { + "bytes": 3833, + "mode": "100644", + "path": "examples/runebench/README.md", + "sha256": "0b51884e15fdda667d0bbfdf5456cdd54b7f4b4f3d8ed57b52cd8f97db84b214" + }, + { + "bytes": 606, + "mode": "100644", + "path": "examples/runebench/THIRD_PARTY.md", + "sha256": "c505b1a5bb5a6670f59c936bb7f4c4d8bc11e018155e3db9722edca1abe644bb" + }, + { + "bytes": 338, + "mode": "100644", + "path": "examples/runebench/build_deps.mjs", + "sha256": "f9f82075d8b128126d4488285e0a1701233d8dd541cdb74be2ca3e5d65f53bf0" + }, + { + "bytes": 2359, + "mode": "100644", + "path": "examples/runebench/build_traces.py", + "sha256": "03013ab29c987eea71c3950a3f9bdb8e76a253f9468b096e54a0f4c8714c12ab" + }, + { + "bytes": 4231, + "mode": "100644", + "path": "examples/runebench/build_web.mjs", + "sha256": "5fcee4dfaaecbaea07fe816b25329b7a9a9534b32cdabdb5c7a848fa8473bdd0" + }, + { + "bytes": 376, + "mode": "100644", + "path": "examples/runebench/compose.yaml", + "sha256": "6090b4bc3afb1955e41aa792c964d79e611b2da67b379bbaf79034e5557e14f6" + }, + { + "bytes": 134, + "mode": "100644", + "path": "examples/runebench/docker-config/config.json", + "sha256": "d86931ace37a780a1e0972436142aca0a214e33d3088f26bea7b3c9e85cb3e38" + }, + { + "bytes": 3672, + "mode": "100644", + "path": "examples/runebench/engine-hook.ts", + "sha256": "e2888b17bd7f5cee895fef474dd687ba9d22b86b1a3fadb706c7b4c96f691197" + }, + { + "bytes": 818, + "mode": "100644", + "path": "examples/runebench/entrypoint.sh", + "sha256": "cd07ff888a0fa949e948165b8eff4a0e839942c6d07682d5b5e82dd81bb32eaa" + }, + { + "bytes": 1569, + "mode": "100644", + "path": "examples/runebench/evidence_api.py", + "sha256": "b7cd5182a6d018e8f5c2f00a1368743b5979cbce917cbd80edaab0f69863763d" + }, + { + "bytes": 2242, + "mode": "100644", + "path": "examples/runebench/example.sh", + "sha256": "54b330b90b8961cf89c2ff40b91d5269640cc7f10900c9a97ca3a153acffec9a" + }, + { + "bytes": 825, + "mode": "100644", + "path": "examples/runebench/manifest.json", + "sha256": "6822ab6826cbefe974f052b00b81bb1cce5330052716915d7e399c7c174e8246" + }, + { + "bytes": 7191, + "mode": "100644", + "path": "examples/runebench/observe.ts", + "sha256": "76f6cd849c028b47ee3b00e6673eb792e69f945a19350b6ed25fa2f39edc5ef7" + }, + { + "bytes": 908, + "mode": "100644", + "path": "examples/runebench/provision.ts", + "sha256": "0d82a91098b1940d41a51faa7e7c191a00b9c5b8e00117bf26eea87f07940906" + }, + { + "bytes": 11868, + "mode": "100644", + "path": "examples/runebench/query.py", + "sha256": "ed5c4415bcc0f4a315d7e066015dcff7f6577331f463e123851d6c900bb6ca20" + }, + { + "bytes": 35172, + "mode": "100644", + "path": "examples/runebench/requirements.lock", + "sha256": "6c1c0bfb8140354949213be5338f3ccfc3cb7701109fcdb6cfffa722802b0590" + }, + { + "bytes": 24, + "mode": "100644", + "path": "examples/runebench/requirements.txt", + "sha256": "3943bc174edbfd5309a7a2fba82b7158247a7b44836ecaeaaa918cc6c3951829" + }, + { + "bytes": 14423, + "mode": "100644", + "path": "examples/runebench/run.py", + "sha256": "71fa8ce1fb9c31e02f8e9fd2a73726a13eeb134c88a57c3f514ab4b877f14896" + }, + { + "bytes": 3744, + "mode": "100644", + "path": "examples/runebench/sample.py", + "sha256": "2cc18b51460d6e50b1a53581fc3946dd24b352f3a0b3fc52b94cd4d014cce738" + }, + { + "bytes": 1528667, + "mode": "100644", + "path": "examples/runebench/sample.tar.gz", + "sha256": "d2d2da454fce87bb827fc927020824a717fb4c996f6b001ab2d4231ac11fbca6" + }, + { + "bytes": 22584, + "mode": "100644", + "path": "examples/runebench/save-generator.ts", + "sha256": "d872ba6447b2858b101a581ca27929fb9372ebe6627f312b2b48ce82cda2d8b1" + }, + { + "bytes": 716, + "mode": "100644", + "path": "examples/runebench/scenario.json", + "sha256": "9d5e48309804b99a56e15e5c5f0c757cf1343fa87dc9b52abaeefea678b4d364" + }, + { + "bytes": 6374, + "mode": "100644", + "path": "examples/runebench/serve.py", + "sha256": "e86f2aeaf1e8b397ad6470da86c01cb56a3610db0ad079887646176f8af809ab" + }, + { + "bytes": 1041, + "mode": "100644", + "path": "examples/runebench/smoke.ts", + "sha256": "7398e1ec6913fd68734c3efa23475c3925426b6a1e24e23cabe12acec4ee50ba" + }, + { + "bytes": 38061, + "mode": "100644", + "path": "examples/runebench/viewer.tsx", + "sha256": "56811e56ec09b892374dc36d1aa5bd2b032f8c80f7c5739ddc6a9290b113e735" + }, { "bytes": 9, "mode": "100644", @@ -5263,16 +5443,16 @@ "sha256": "674d6b0ee1334dcf1fd4718c85fdd08117a2f05c998231ca258e8babfdbe9373" }, { - "bytes": 108191, + "bytes": 112384, "mode": "100644", "path": "package-lock.json", - "sha256": "64e9ef6e12cf3a6019c8bad59bd65e45ad58ab3d033b079acf75baa3431fceab" + "sha256": "b2f78899106c61bc0247d50c4b3ccdf4c1429be55eacf32e355be85c3513cfaf" }, { - "bytes": 1238, + "bytes": 1270, "mode": "100644", "path": "package.json", - "sha256": "fb38f8c061db2f13694328fae488ca01301f5b4adbcb7d91dddc9837d64816e2" + "sha256": "ae0e2b5b3876da4f7e807e871ede5ad4017e679c64569dab9ef36e386f7e1039" }, { "bytes": 190, @@ -5377,10 +5557,10 @@ "sha256": "d62c6fb74f4b8bf80e909a4c9e7cfabab645216267b7049c57cfa53191f4e9a0" }, { - "bytes": 10138, + "bytes": 11348, "mode": "100644", "path": "packages/visuals-react/src/captureBarrier.ts", - "sha256": "a0cf17ed388d807095c122415603b51a649ac8058ecf656df50a4ddb4217a5db" + "sha256": "5dd9626ef550726827d8ad8037630058c6642cf09dc4e66fa1e4235dff990b2c" }, { "bytes": 16483, @@ -5419,10 +5599,10 @@ "sha256": "50ad3d3a20a4a96e23c49720f4758df99f98c53414a4c533c2fc9ebc63750e0e" }, { - "bytes": 3813, + "bytes": 4196, "mode": "100644", "path": "packages/visuals-react/src/session.ts", - "sha256": "95451a687320148403af29d056a7a5d3c53c820b2864e016a31459112cc3f714" + "sha256": "bdefe77dd2cbd540f3569a41843df55759bda6a2f5a365e913a8f8eb63d05fc3" }, { "bytes": 7244, @@ -5683,16 +5863,16 @@ "sha256": "fbc7b9a1cda41563685f2fd9a7e8bd5234fef2e0c54a174efc92342329885c34" }, { - "bytes": 5406, + "bytes": 5261, "mode": "100644", "path": "packages/workshop-visuals/components/agent_trace.v1/EvidenceClient.tsx", - "sha256": "9dfe893172da70cc11c91c39601bd9445847c8ae6c6edb829cd4b747232a7543" + "sha256": "9713b3eda24ba68baecff807be37b2f3f9a84e67933e4611e3be8c6dc26b8188" }, { - "bytes": 2823, + "bytes": 2900, "mode": "100644", "path": "packages/workshop-visuals/components/agent_trace.v1/README.md", - "sha256": "a1eb265adc6292d5ffcda22036d6a1566c2db2a7f595f7a64e552049266730a4" + "sha256": "edeb282612b219932ca217d7983c196236dd9bfe90556af48950104659e0a689" }, { "bytes": 3084, @@ -5724,6 +5904,12 @@ "path": "packages/workshop-visuals/components/agent_trace.v1/archive.ts", "sha256": "0488557de71377d6712043936383ad1e2c81c0db6a7d955d43b46f4cfb979101" }, + { + "bytes": 382, + "mode": "100644", + "path": "packages/workshop-visuals/components/agent_trace.v1/evidenceEndpoint.ts", + "sha256": "721a247f7f61a2d27a459a0bcd24dcf0bc287a41dfc20d9de8e9d5dd84067459" + }, { "bytes": 3222, "mode": "100644", @@ -6984,6 +7170,18 @@ "path": "pnpm-lock.yaml", "sha256": "3077e9ff7f710add160baab1c55df15c6f023069be25736229ddb44d993da52f" }, + { + "bytes": 10926, + "mode": "100644", + "path": "resources/codex/LICENSE", + "sha256": "d17f227e4df5da1600391338865ce0f3055211760a36688f816941d58232d8dc" + }, + { + "bytes": 240, + "mode": "100644", + "path": "resources/codex/NOTICE", + "sha256": "3ce7450e8b97d94d27b8c99175b21f86bd786cea3620e5d6b5a7c41e7ed35cee" + }, { "bytes": 105, "mode": "100644", @@ -6996,6 +7194,12 @@ "path": "rust-toolchain.toml", "sha256": "ebc5360fd3b6b35d0578ec1607d2a671e74fa5c3527a7fee576e15e57eed175d" }, + { + "bytes": 1966, + "mode": "100644", + "path": "scripts/bootstrap-deps.sh", + "sha256": "08fb3c0fc24f691c3c2b4a0de0179c0073e2da5bd80b431a99fef74360f5385d" + }, { "bytes": 12405, "mode": "100755", @@ -7009,10 +7213,10 @@ "sha256": "08655706e84ae98c9d9b34626ac3d9c6502698b7d6ab86c4422f5df9ff2acdce" }, { - "bytes": 5564, + "bytes": 5605, "mode": "100755", "path": "scripts/build-tier.sh", - "sha256": "2650c8750b1249e52049c390cede29ea390c7126f42053e8b98b9d7c735dbfb0" + "sha256": "cd6a190bafccd68eede834e1f94a34a9fe55af90b7cbcc1aff62b858c22ef06d" }, { "bytes": 3686, @@ -7123,10 +7327,10 @@ "sha256": "7f2cde7cb9e63e50d23f141e926048a2a0d19fe2452f648ea14233ed72b3f142" }, { - "bytes": 4500, + "bytes": 4866, "mode": "100755", "path": "scripts/install.sh", - "sha256": "99a4b87a956bae3f83734bc32da9b5b53291a881b46be8f27736791f3d561415" + "sha256": "187496966f711e55ac063c2d27bb46248fefdb410596425bace44df7838cc5c9" }, { "bytes": 506, @@ -7164,6 +7368,12 @@ "path": "scripts/laguna/test.sh", "sha256": "7ea8973bcbe4886603b9b43686cf0baaf0ed23e92d774e22e70fe195db8b3c45" }, + { + "bytes": 519, + "mode": "100644", + "path": "scripts/local-toolchain-env.sh", + "sha256": "8cc9688300a760706a3379516b4b78bb5fa703732f35ce5cf792dc9fb5224964" + }, { "bytes": 654, "mode": "100644", @@ -7218,6 +7428,12 @@ "path": "scripts/seal-adhoc-trace-import.sh", "sha256": "4cdfbcad58afb4bc1f3f4c9cf8640baddd1caae4d9011d7b6eaa189ce42117c2" }, + { + "bytes": 1033, + "mode": "100644", + "path": "scripts/stage-codex-runtime.sh", + "sha256": "b80c76ac53b44900310a2c138cdf3a4b4c8fcc4beb5aa0c6d46a745e7debd0f3" + }, { "bytes": 2813, "mode": "100755", @@ -7267,10 +7483,10 @@ "sha256": "5cb03b8ce312bacc444102bf05403214746b0ea9d7ced895e9d0b0ab3c85276d" }, { - "bytes": 2641, + "bytes": 2709, "mode": "100755", "path": "scripts/workshop.sh", - "sha256": "296e740b6451377afac1fd1bbb75b75f9a21d32d1f1810f7ed8d76c829bc2c38" + "sha256": "0cf2b85a86afb37129f384928db5a43dc75ccb6d09d85399940abecfaa9e7a08" }, { "bytes": 1843, @@ -7687,10 +7903,10 @@ "sha256": "a45a6a8cc94c53e97bf2dc719e6172ad10ae9fea1091559456396e8fa1b244ea" } ], - "policySha256": "f5ef8fbb4560ee9e401c26c94da6f6189732be76291915027bd7ee3f5f5684dd", + "policySha256": "d8f9d3188ed10080a0bf93da6ff1ebdf54063198ffd6cc384acf4de4c97f040a", "schema": "workshop.public-export-manifest.v1", "source": { - "commit": "7ef1b6fb69770e997e8e62c6d9cacffbd26d06b5", - "tree": "76ea0cabbe5a32ded58158131ae1c1e3e5a6e4b8" + "commit": "f90a8105b800ecb3a49b38c69f76214f3d71573f", + "tree": "95a5dcf7de4f4846b2cd341def6c2db55942c4a5" } } diff --git a/README.md b/README.md index 3be69a70..b32ff500 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,32 @@ artifacts, container evaluations, and optimization workflows. Official downloads and checksums: [usesynth.ai/download](https://www.usesynth.ai/download). Product documentation: [docs.usesynth.ai](https://docs.usesynth.ai). +## ChatGPT, Astra, and OpenRouter + +After building, open **Settings → Models → ChatGPT subscription** and complete +the browser login. If the callback port is occupied, paste the full redirect URL +into the manual callback field. Cancel abandons that attempt. “Connected” means +signed in, not guaranteed access to every model; reconnect if the session expires. + +Select **ChatGPT → GPT-6 Astra** (`gpt-6-astra`) to use your subscription. +Alternatively, use **OpenRouter → API credits** in Settings to select an absolute +path to a private env file containing `OPENROUTER_API_KEY=your-key`. Restrict it +with `chmod 600 /absolute/path/to/private.env`; never commit it or paste the key +into the path field. Preserve existing Synth variables: this env-file setting is +shared. This setup does not import credentials into Keychain. + +Select OpenRouter Astra (`openai/gpt-6-astra`) explicitly. OpenRouter credits are +separate from ChatGPT allowance; failed ChatGPT requests never automatically +switch to a paid API. Key detection does not verify credit or model entitlement. +Astra offers Low through Max reasoning (Medium default), text/image input, a +1,050,000-token context, conservative 250,000-token auto-compaction, and Standard +service. Access still depends on the chosen account/provider. + +Source builds bundle native Codex 0.153.0, with no global Codex or Node required +when launching from Finder. Remove an obsolete `SYNTH_CODEX_BIN` override if the +runtime check fails. These changes apply to builds from this source, not older +downloaded binaries. + ## Install or build locally The v0.10 macOS app is **ad-hoc signed and not Apple-notarized**. It does not @@ -22,14 +48,20 @@ Cloud models and account features require network access and their own credentia From a source checkout on an Apple silicon Mac: ```bash -./scripts/install.sh -./scripts/workshop.sh build-and-run +./scripts/install.sh --bootstrap && ./scripts/workshop.sh build-and-run ``` -Install `uv` and provide Python 3.11 or newer as `python3` on your PATH. Use the -repository's pinned Node and Rust toolchains and the Xcode prerequisites checked -by `./scripts/install.sh --check`. That check reports missing prerequisites -without modifying local configuration. +Bootstrap installs Homebrew if absent, Node 20/npm, Python 3.12, Rust stable, +uv, jq, ripgrep, Git, and locked npm dependencies. Homebrew's +[official interactive installer](https://brew.sh) may request administrator +approval. Apple command-line tools must provide Swift 6+: if missing, the script +opens Apple's installer and asks you to finish it and rerun. Older tools require +an update or selecting Xcode 16+. Apple prompts/licenses are not automated. +Existing `.env`, shell profiles, and the global Rust default are preserved; +the build scripts locate the Homebrew tools without shell configuration. +If prerequisites are already installed, use `./scripts/install.sh` instead. +`./scripts/install.sh --check` checks prerequisites without changing local +configuration; `--dry-run` previews project setup without installing anything. The build fetches exact public Containers, Optimizers, and MLX source revisions into `work/build-sources`. No sibling repositories or release credentials are @@ -48,6 +80,23 @@ The supported source-build entrypoints are [install.sh](scripts/install.sh) and [workshop.sh](scripts/workshop.sh). Private release/test orchestration is not included in this public checkout. +## Try four-agent review + +After installing dependencies, start with the bundled RuneBench recording: + +```bash +bash examples/runebench/example.sh setup && bash examples/runebench/example.sh sample && bash examples/runebench/example.sh review +``` + +Open `http://127.0.0.1:8128/viewer`. Focus one agent, compare a teammate, +inspect messages and unsuccessful actions, and follow source records and game +frames. This recorded sample needs neither Docker nor AI credentials. +The [example guide](examples/runebench/README.md) covers a fresh, free scripted +run using Docker. Give Workshop the [starter prompt](examples/runebench/PROMPT.md) +to guide setup and native visual review. These game-specific views are an +example built with Workshop's shared inspector, not automatic interpretation +of arbitrary logs. + ## Connect an agent through MCP Start Workshop and find its **Data root** in Settings → About. A connection grants diff --git a/apps/synth_desktop/src-tauri/src/codex_oauth.rs b/apps/synth_desktop/src-tauri/src/codex_oauth.rs index 6483ab45..b4fb61c7 100644 --- a/apps/synth_desktop/src-tauri/src/codex_oauth.rs +++ b/apps/synth_desktop/src-tauri/src/codex_oauth.rs @@ -332,6 +332,7 @@ pub struct Manager { pending: Mutex>, failure: StateMutex>, authenticating: AtomicBool, + listener_task: Mutex>>, } impl Manager { @@ -368,6 +369,7 @@ impl Manager { pending: Mutex::new(None), failure: StateMutex::new(None), authenticating: AtomicBool::new(false), + listener_task: Mutex::new(None), } } @@ -376,6 +378,10 @@ impl Manager { if guard.is_some() { bail!("A ChatGPT subscription sign-in is already in progress"); } + if let Some(task) = self.listener_task.lock().await.take() { + task.abort(); + let _ = task.await; + } let verifier = random_urlsafe(32); let state = random_urlsafe(24); let challenge = URL_SAFE_NO_PAD.encode(Sha256::digest(verifier.as_bytes())); @@ -399,16 +405,15 @@ impl Manager { .failure .lock() .expect("OAuth failure state mutex poisoned") = None; - drop(guard); - let mode = match TcpListener::bind(("127.0.0.1", 1455)).await { Ok(listener) => { let manager = self.clone(); - tokio::spawn(async move { manager.accept_callback(listener).await }); + *self.listener_task.lock().await = Some(tokio::spawn(async move { manager.accept_callback(listener).await })); "auto" } Err(_) => "manual", }; + drop(guard); Ok(BeginResult { authorize_url: url.into(), mode: mode.into(), @@ -431,7 +436,8 @@ impl Manager { return; }; let mut bytes = vec![0; 16 * 1024]; - let read = stream.read(&mut bytes).await.unwrap_or(0); + let read = tokio::time::timeout(Duration::from_secs(10), stream.read(&mut bytes)) + .await.ok().and_then(Result::ok).unwrap_or(0); let request = String::from_utf8_lossy(&bytes[..read]); let target = request .lines() @@ -531,13 +537,19 @@ impl Manager { .map(str::to_owned), last_refresh_ms: now, }; + // A cancelled/replaced request must never reconnect an account after + // its token exchange returns. Serialize the check and save with cancel. + let mut current = self.pending.lock().await; + if current.as_ref().map(|value| value.state.as_str()) != Some(pending.state.as_str()) { + bail!("ChatGPT sign-in was cancelled or replaced; no credentials were stored"); + } let _refresh_lock = self.store.lock_refresh()?; self.store.save(&credential)?; *self .failure .lock() .expect("OAuth failure state mutex poisoned") = None; - *self.pending.lock().await = None; + *current = None; self.authenticating.store(false, Ordering::Release); self.status() } @@ -688,6 +700,10 @@ impl Manager { pub async fn cancel(&self) -> Result<()> { *self.pending.lock().await = None; self.authenticating.store(false, Ordering::Release); + if let Some(task) = self.listener_task.lock().await.take() { + task.abort(); + let _ = task.await; + } Ok(()) } diff --git a/apps/synth_desktop/src-tauri/src/model_catalog.rs b/apps/synth_desktop/src-tauri/src/model_catalog.rs index 5f6baba2..1d73fbf4 100644 --- a/apps/synth_desktop/src-tauri/src/model_catalog.rs +++ b/apps/synth_desktop/src-tauri/src/model_catalog.rs @@ -287,6 +287,16 @@ fn candidates(configured: &[OpenRouterModelConfig]) -> Vec { fn builtin_candidates() -> Vec { vec![ + builtin( + "openrouter-astra", "openai/gpt-6-astra", "GPT-6 Astra", + ModelCatalogCapabilities { + input_modalities: vec!["text".into(), "image".into()], + output_modalities: vec!["text".into()], tools: true, + reasoning_control: ModelCatalogReasoningControl::Effort, + default_reasoning: Some("medium".into()), + max_context_tokens: Some(1_050_000.0), max_completion_tokens: Some(128_000.0), + }, + ), builtin( "openrouter-luna", "openai/gpt-5.6-luna", diff --git a/apps/synth_desktop/src-tauri/src/session/codex/event_pump.rs b/apps/synth_desktop/src-tauri/src/session/codex/event_pump.rs index 76ee8ba4..da5bb41c 100644 --- a/apps/synth_desktop/src-tauri/src/session/codex/event_pump.rs +++ b/apps/synth_desktop/src-tauri/src/session/codex/event_pump.rs @@ -20,6 +20,10 @@ fn codex_child_path(binary: &Path, inherited: Option<&OsStr>) -> Result( request, persistent, } = spawn; + if matches!(request.model.as_str(), "gpt-6-astra" | "openai/gpt-6-astra") { + let mut probe = Command::new(binary); + probe.arg("--version").kill_on_drop(true); + if let Some(path) = codex_child_path(binary, std::env::var_os("PATH").as_deref())? { + probe.env("PATH", path); + } + let output = tokio::time::timeout(std::time::Duration::from_secs(20), probe.output()) + .await.context("Codex runtime version check timed out; rebuild with the pinned runtime")??; + let version = String::from_utf8_lossy(&output.stdout); + let supported = version.split_whitespace().last().and_then(|v| { + let parts: Vec = v.split('.').map(str::parse).collect::>().ok()?; + Some(parts.as_slice() >= [0, 153, 0].as_slice()) + }).unwrap_or(false); + anyhow::ensure!(output.status.success() && supported, + "Astra requires Codex 0.153.0 or newer. Rebuild Workshop or remove an outdated SYNTH_CODEX_BIN override; no alternate provider was used."); + } if persistent { return spawn_persistent_server(app, binary, session_id, home, request, pump).await; } @@ -431,6 +451,18 @@ async fn read_stdout( if persistence.codex_oauth && method == "turn/failed" { normalize_oauth_failure(&mut params); } + if method == "turn/failed" { + if let Some(record) = persistence.records.read().await.get(&session_id) { + if let Some(object) = params.as_object_mut() { + object.insert("provider".into(), json!(record.provider_title)); + object.insert("model".into(), json!(record.model)); + let message = object.get("message").and_then(Value::as_str) + .or_else(|| object.get("error").and_then(|v| v.get("message")).and_then(Value::as_str)) + .unwrap_or("Request failed; inspect provider details.").to_owned(); + object.insert("message".into(), json!(format!("{} · {}: {}", record.provider_title, record.model, message))); + } + } + } // Some app-server versions close the stream immediately after a // typed final agent message instead of sending `turn/completed`. // `phase: final_answer` is a protocol lifecycle signal, not an @@ -1019,10 +1051,17 @@ fn normalize_oauth_failure(params: &mut Value) { "codex_oauth_reauth_required", "Reconnect ChatGPT subscription in Settings → Models.", ) + } else if lower.contains("model_not_found") || lower.contains("model_not_available") + || lower.contains("not supported") || lower.contains("does not have access") { + ("codex_oauth_model_unavailable", "This model is unavailable to your ChatGPT account or workspace. Choose another ChatGPT model or ask your workspace administrator. No API provider was substituted.") } else { return; }; - *params = json!({"code": code, "message": message}); + // Keep typed turn identifiers and retry/reset metadata for recovery. + if let Some(object) = params.as_object_mut() { + object.insert("code".into(), json!(code)); + object.insert("message".into(), json!(message)); + } } diff --git a/apps/synth_desktop/src-tauri/src/session/codex/home.rs b/apps/synth_desktop/src-tauri/src/session/codex/home.rs index ab4416c1..e07ac2aa 100644 --- a/apps/synth_desktop/src-tauri/src/session/codex/home.rs +++ b/apps/synth_desktop/src-tauri/src/session/codex/home.rs @@ -915,7 +915,9 @@ pub(crate) fn validate_start(request: &CodexSessionStartRequest) -> Result<()> { } pub(crate) fn model_context_window(model: &str) -> u64 { - if model.to_ascii_lowercase().contains("laguna-xs") { + if matches!(model, "gpt-6-astra" | "openai/gpt-6-astra") { + 1_050_000 + } else if model.to_ascii_lowercase().contains("laguna-xs") { 262_144 } else if model.to_ascii_lowercase().contains("muse-spark-1.2") { 1_048_576 @@ -932,6 +934,7 @@ pub(crate) fn auto_compact_token_limit(request: &CodexSessionStartRequest) -> u6 let requested = request.auto_compact_token_limit.unwrap_or_else(|| { let model = request.model.to_ascii_lowercase(); if model.contains("laguna-s-2.1") + || matches!(model.as_str(), "gpt-6-astra" | "openai/gpt-6-astra") || model.contains("gpt-5.6-luna") || model.contains("muse-spark-1.2") { diff --git a/apps/synth_desktop/src-tauri/src/session/codex/manager.rs b/apps/synth_desktop/src-tauri/src/session/codex/manager.rs index 31f7a878..d6aeccca 100644 --- a/apps/synth_desktop/src-tauri/src/session/codex/manager.rs +++ b/apps/synth_desktop/src-tauri/src/session/codex/manager.rs @@ -102,7 +102,16 @@ impl CodexManager { approvals: Arc, ) -> Self { let root = codex_root(); - let binary = PathBuf::from(env::var("SYNTH_CODEX_BIN").unwrap_or_else(|_| "codex".into())); + let binary = env::var_os("SYNTH_CODEX_BIN").map(PathBuf::from).unwrap_or_else(|| { + // Finder launches must not depend on a shell's npm/Node PATH. + let bundled = env::current_exe().ok().and_then(|exe| { + exe.parent()?.parent().map(|contents| contents.join("Resources/runtimes/codex/bin/codex")) + }); + bundled.filter(|path| path.is_file()).unwrap_or_else(|| { + let local = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../../node_modules/@openai/codex-darwin-arm64/vendor/aarch64-apple-darwin/bin/codex"); + if cfg!(debug_assertions) && local.is_file() { local } else { PathBuf::from("codex") } + }) + }); Self::with_paths_and_approvals( crate::session::SessionPersistence::from_core(core), root, diff --git a/apps/synth_desktop/src-tauri/tauri.package.json b/apps/synth_desktop/src-tauri/tauri.package.json index c846bcf5..7f64a9fb 100644 --- a/apps/synth_desktop/src-tauri/tauri.package.json +++ b/apps/synth_desktop/src-tauri/tauri.package.json @@ -2,6 +2,7 @@ "$schema": "https://schema.tauri.app/config/2", "bundle": { "resources": { + "../../../runtime-distributions/codex": "runtimes/codex", "../../../services/laguna-daemon/laguna_daemon": "services/laguna-daemon/laguna_daemon", "../../../services/victoria-logs": "services/victoria-logs", "../../../packages/workshop-visuals/families": "visuals/families", diff --git a/apps/synth_desktop/src/renderer/src/bridge/types.ts b/apps/synth_desktop/src/renderer/src/bridge/types.ts index d6c592bf..2d071332 100644 --- a/apps/synth_desktop/src/renderer/src/bridge/types.ts +++ b/apps/synth_desktop/src/renderer/src/bridge/types.ts @@ -412,6 +412,7 @@ export type SynthBackendSettings = { }; export type SynthConfigBridge = { + chooseEnvFile?(): Promise; get(): Promise; /** Rust-owned model projection; this never parses config.toml in the renderer. */ modelCatalog(): Promise; diff --git a/apps/synth_desktop/src/renderer/src/components/ChatgptCodexSubscriptionCard.tsx b/apps/synth_desktop/src/renderer/src/components/ChatgptCodexSubscriptionCard.tsx index ebb876d8..d7ef1c3f 100644 --- a/apps/synth_desktop/src/renderer/src/components/ChatgptCodexSubscriptionCard.tsx +++ b/apps/synth_desktop/src/renderer/src/components/ChatgptCodexSubscriptionCard.tsx @@ -22,10 +22,17 @@ export function oauthErrorMessage(reason: unknown): string { export function ChatgptCodexSubscriptionCard() { const [status, setStatus] = useState(EMPTY); const [busy, setBusy] = useState(false); + const [submitting, setSubmitting] = useState(false); + const [awaitingCallback, setAwaitingCallback] = useState(false); const [manual, setManual] = useState(false); const [redirectUrl, setRedirectUrl] = useState(""); const [error, setError] = useState(null); const pollRef = useRef(null); + const generation = useRef(0); + const stopPolling = () => { + if (pollRef.current != null) window.clearInterval(pollRef.current); + pollRef.current = null; + }; const publish = (next: CodexOauthStatus) => { setStatus(next); @@ -33,53 +40,86 @@ export function ChatgptCodexSubscriptionCard() { }; useEffect(() => { - void bridges.codexOauth?.status().then(publish).catch(() => setStatus(EMPTY)); - return () => { if (pollRef.current != null) window.clearInterval(pollRef.current); }; + const attempt = generation.current; + void bridges.codexOauth?.status().then(next => { if (attempt === generation.current) publish(next); }).catch(() => undefined); + return () => { ++generation.current; stopPolling(); }; }, []); const connect = async () => { + const attempt = ++generation.current; + stopPolling(); setBusy(true); + setAwaitingCallback(false); + setRedirectUrl(""); setError(null); try { const begin = await bridges.codexOauth!.begin(); + if (attempt !== generation.current) return; + setAwaitingCallback(true); setManual(begin.mode === "manual"); + const deadline = Date.now() + 300_000; + let polling = false; if (pollRef.current != null) window.clearInterval(pollRef.current); pollRef.current = window.setInterval(() => { + if (polling || attempt !== generation.current) return; + if (Date.now() >= deadline) { + ++generation.current; stopPolling(); setBusy(false); setAwaitingCallback(false); + setError("ChatGPT sign-in timed out. Start over to try again."); + void bridges.codexOauth?.cancel().catch(() => undefined); return; + } + polling = true; void bridges.codexOauth!.status().then((next) => { + if (attempt !== generation.current) return; publish(next); if (next.state === "ready" && pollRef.current != null) { window.clearInterval(pollRef.current); pollRef.current = null; setBusy(false); + setAwaitingCallback(false); } else if (next.state === "refresh_failed" && pollRef.current != null) { window.clearInterval(pollRef.current); pollRef.current = null; setBusy(false); + setAwaitingCallback(false); setError(next.guidance); } - }); + }).catch(reason => { + if (attempt !== generation.current) return; + stopPolling(); setBusy(false); setAwaitingCallback(false); + setError(oauthErrorMessage(reason)); + }).finally(() => { polling = false; }); }, 750); } catch (reason) { + if (attempt !== generation.current) return; setBusy(false); + setAwaitingCallback(false); setError(oauthErrorMessage(reason)); } }; const completeManual = async () => { + const attempt = ++generation.current; + stopPolling(); + setSubmitting(true); setBusy(true); setError(null); try { - publish(await bridges.codexOauth!.completeManual(redirectUrl)); + const next = await bridges.codexOauth!.completeManual(redirectUrl); + if (attempt !== generation.current) return; + publish(next); + setAwaitingCallback(false); setRedirectUrl(""); setManual(false); } catch (reason) { + if (attempt !== generation.current) return; setError(oauthErrorMessage(reason)); } finally { - setBusy(false); + if (attempt === generation.current) { setBusy(false); setSubmitting(false); } } }; const disconnect = async () => { + ++generation.current; stopPolling(); setAwaitingCallback(false); setBusy(true); setError(null); try { publish(await bridges.codexOauth!.disconnect()); } @@ -88,6 +128,7 @@ export function ChatgptCodexSubscriptionCard() { }; const cancel = async () => { + ++generation.current; setSubmitting(false); setAwaitingCallback(false); setManual(false); setRedirectUrl(""); if (pollRef.current != null) window.clearInterval(pollRef.current); pollRef.current = null; await bridges.codexOauth?.cancel().catch(() => undefined); @@ -95,6 +136,7 @@ export function ChatgptCodexSubscriptionCard() { }; const restart = async () => { + ++generation.current; setSubmitting(false); setAwaitingCallback(false); if (pollRef.current != null) window.clearInterval(pollRef.current); pollRef.current = null; await bridges.codexOauth?.cancel().catch(() => undefined); @@ -119,7 +161,7 @@ export function ChatgptCodexSubscriptionCard() {

{status.guidance}

-

Uses your Codex allowance, not API credits or Platform API access.

+

Uses your Codex allowance, not API credits or Platform API access. Connected means signed in; model access still depends on your account and workspace. No automatic fallback to a paid API provider.

@@ -131,14 +173,14 @@ export function ChatgptCodexSubscriptionCard() {
{manual ?
setRedirectUrl(event.target.value)} placeholder="http://localhost:1455/auth/callback?code=…&state=…" /> - +
: null} {error ?
Couldn’t re-sync ChatGPT. {error} Use Start over to create a fresh authorization attempt.
: null} {status.canUseModels ?
-
Available in the composerChatGPT subscription · plan allowance
3 models
+
Models in the composer · access depends on your accountChatGPT subscription · plan allowance
4 models
- {[["GPT-5.6 Sol", "gpt-5.6-sol", "Fast iteration"], ["GPT-5.6 Luna", "gpt-5.6-luna", "Everyday coding"], ["GPT-5.6 Terra", "gpt-5.6-terra", "Deep reasoning"]].map(([name, id, fit]) =>
{name}{fit}{id}
)} + {[["GPT-6 Astra", "gpt-6-astra", "Complex work · account access required"], ["GPT-5.6 Sol", "gpt-5.6-sol", "Fast iteration"], ["GPT-5.6 Luna", "gpt-5.6-luna", "Everyday coding"], ["GPT-5.6 Terra", "gpt-5.6-terra", "Deep reasoning"]].map(([name, id, fit]) =>
{name}{fit}{id}
)}
: null} diff --git a/apps/synth_desktop/src/renderer/src/components/OpenRouterSetupCard.tsx b/apps/synth_desktop/src/renderer/src/components/OpenRouterSetupCard.tsx new file mode 100644 index 00000000..3b993d83 --- /dev/null +++ b/apps/synth_desktop/src/renderer/src/components/OpenRouterSetupCard.tsx @@ -0,0 +1,47 @@ +import { useEffect, useState } from "react"; +import { bridges } from "../runtime/desktopBridge"; +import { publicError } from "../runtime/publicError"; +import type { SynthBackendSettings } from "../bridge"; +import { SettingsCard } from "./SettingsCard"; + +export function OpenRouterSetupCard() { + const [settings, setSettings] = useState(null); + const [path, setPath] = useState(""); + const [busy, setBusy] = useState(false); + const [message, setMessage] = useState(""); + useEffect(() => { + let active = true; + void bridges.config?.get().then(value => { if (active) { setSettings(value); setPath(value.envFile); } }).catch(error => { if (active) setMessage(publicError(error)); }); + return () => { active = false; }; + }, []); + const save = async () => { + if (!bridges.config || !path.trim()) return; + if (!path.trim().startsWith("/") || path.includes("=") || /[\r\n]/.test(path)) { + setMessage("Choose an absolute file path, not a key or an environment assignment."); + return; + } + setBusy(true); setMessage(""); + try { + // Read current routing before saving; never overwrite it from stale UI state. + const current = await bridges.config.get(); + const next = await bridges.config.update({ profile: current.profile, backendUrl: current.backendUrl, apiKeyEnv: current.apiKeyEnv, envFile: path.trim() }); + setSettings(next); setPath(next.envFile); + window.dispatchEvent(new CustomEvent("synth:account-changed")); + setMessage(next.openrouterApiKeyConfigured ? "OpenRouter key detected. Account credit and model access are checked when you send a message." : "No OpenRouter key found. Add OPENROUTER_API_KEY to this file, then check again."); + } catch (error) { setMessage(publicError(error)); } + finally { setBusy(false); } + }; + return +

Use your own OpenRouter key. This bills OpenRouter credits, not your ChatGPT allowance. A failed ChatGPT request never switches here automatically.

+

Create a private file containing OPENROUTER_API_KEY=your-key, restrict its permissions to your user (for example chmod 600 .env), and select its path below. Never paste a key into this path field or commit the file.

+

The native host reads the file; this setup does not import anything into Keychain. This env-file setting is shared with Synth API routing: preserve any existing variables in the selected file.

+ +
+ + +
+

{settings?.openrouterApiKeyConfigured ? `Key detected · ${settings.openrouterApiKeyFingerprint ?? "configured"}` : "OpenRouter not configured"}

+

{message}

+

Then choose API → OpenRouter → GPT-6 Astra in the composer. Metadata refresh uses the public catalog and does not run a model.

+
; +} diff --git a/apps/synth_desktop/src/renderer/src/components/SettingsPage.tsx b/apps/synth_desktop/src/renderer/src/components/SettingsPage.tsx index e3155853..22c2f68a 100644 --- a/apps/synth_desktop/src/renderer/src/components/SettingsPage.tsx +++ b/apps/synth_desktop/src/renderer/src/components/SettingsPage.tsx @@ -29,6 +29,7 @@ import type { DesktopPreferences } from "../preferences"; import { ProviderMark } from "./ProviderMark"; import { bridges } from "../runtime/desktopBridge"; import { ChatgptCodexSubscriptionCard } from "./ChatgptCodexSubscriptionCard"; +import { OpenRouterSetupCard } from "./OpenRouterSetupCard"; import { ContextSettings } from "./ContextSettings"; import { SecretsSettings } from "./SecretsSettings"; import { ProjectSourcesSettings } from "./ProjectSourcesSettings"; @@ -553,6 +554,7 @@ export function SettingsPage({ + diff --git a/apps/synth_desktop/src/renderer/src/components/VisualHost.tsx b/apps/synth_desktop/src/renderer/src/components/VisualHost.tsx index 59e50830..4f2c50e7 100644 --- a/apps/synth_desktop/src/renderer/src/components/VisualHost.tsx +++ b/apps/synth_desktop/src/renderer/src/components/VisualHost.tsx @@ -93,10 +93,14 @@ function PinnedTemplate({Shell,...props}:{Shell:ComponentType}&Shell } const data=JSON.parse(JSON.stringify({values,aliases})) as {values:ShellProps;aliases:Record}; const cut=useTemplateEvidence(data); + const rendered=useMemo(()=>{ + if(!cut.value)return null; + const restored={...cut.value.values}; + for(const [key,source] of Object.entries(cut.value.aliases))restored[key]=restored[source]; + return ; + },[Shell,cut.value,ports]); if(!cut.ready||!cut.value)return
{cut.error??"Restoring pinned visual evidence…"}
; - const restored={...cut.value.values}; - for(const [key,source] of Object.entries(cut.value.aliases))restored[key]=restored[source]; - return ; + return rendered; } export function artifactFromVisualRecord(visual: VisualRecord): ArtifactRef { @@ -372,6 +376,10 @@ function TemplateVisualHost({ artifact }: { artifact: ArtifactRef }) { }), [artifact.id, artifact.visualId, artifact.revision] ); + const visualStatePort=useMemo(()=>bridges.visuals ? { + putSnapshot:(snapshot:import("@synth/visuals-protocol").VisualSnapshot)=>bridges.visuals!.putSnapshot(visualIdentity.visualId,snapshot), + putRecording:(recording:import("@synth/visuals-protocol").VisualRecording)=>bridges.visuals!.putRecording(visualIdentity.visualId,recording), + }:undefined,[visualIdentity]); const optimizerRunId = resolvedBindings.status !== "rejected" ? resolvedBindings.slots.find( (entry) => bindingInputName(entry) === "optimizer_run" && entry.kind === "optimizer_run" @@ -925,10 +933,7 @@ function TemplateVisualHost({ artifact }: { artifact: ArtifactRef }) { replayMissingTransport={replay.missingTransport} visualId={artifact.visualId ?? artifact.id} revision={typeof artifact.revision === "number" ? artifact.revision : null} - visualState={bridges.visuals ? { - putSnapshot: (snapshot: import("@synth/visuals-protocol").VisualSnapshot) => bridges.visuals!.putSnapshot(artifact.visualId ?? artifact.id, snapshot), - putRecording: (recording: import("@synth/visuals-protocol").VisualRecording) => bridges.visuals!.putRecording(artifact.visualId ?? artifact.id, recording), - } : undefined} + visualState={visualStatePort} /> ); diff --git a/apps/synth_desktop/src/renderer/src/runtime/desktopBridge.ts b/apps/synth_desktop/src/renderer/src/runtime/desktopBridge.ts index ace9094e..17c54324 100644 --- a/apps/synth_desktop/src/renderer/src/runtime/desktopBridge.ts +++ b/apps/synth_desktop/src/renderer/src/runtime/desktopBridge.ts @@ -621,6 +621,10 @@ window.synthSecrets ??= isTauri }; window.synthConfig ??= isTauri ? { + chooseEnvFile: async () => { + const path = await open({ directory: false, multiple: false, title: "Choose a private OpenRouter env file" }); + return typeof path === "string" ? path : null; + }, get: () => fromGenerated(spectaCommands.synthConfigGet()), modelCatalog: () => fromGenerated(spectaCommands.modelCatalogGet()), refreshModelCatalog: () => fromGenerated(spectaCommands.modelCatalogRefresh()), diff --git a/apps/synth_desktop/src/renderer/src/runtime/modelCapabilities.ts b/apps/synth_desktop/src/renderer/src/runtime/modelCapabilities.ts index 15ae796e..b2fbc46b 100644 --- a/apps/synth_desktop/src/renderer/src/runtime/modelCapabilities.ts +++ b/apps/synth_desktop/src/renderer/src/runtime/modelCapabilities.ts @@ -77,6 +77,12 @@ const SERVICE_TIER_OPTIONS: ModelKnobOption[] = [ ]; const BUILTIN_MODEL_CAPABILITY_REGISTRY: ModelCapabilitySpec[] = [ + { + targetId: "chatgpt-astra", + target: { kind: "remote", models: ["gpt-6-astra"] }, + knobs: [{ id: "reasoning", label: "Thinking", testId: "reasoning-effort", storageKey: "synth.models.chatgpt-astra.reasoning", defaultValue: "medium", options: LUNA_EFFORT_OPTIONS, turnStartField: "effort" }], + reasoningDisplay: "summary", inputModalities: ["text", "image"], maxContextTokens: 1_050_000 + }, { targetId: "chatgpt-luna", target: { kind: "remote", models: ["gpt-5.6-luna"] }, diff --git a/apps/synth_desktop/src/renderer/src/runtime/modelCatalog.ts b/apps/synth_desktop/src/renderer/src/runtime/modelCatalog.ts index 66f5bf68..7323d465 100644 --- a/apps/synth_desktop/src/renderer/src/runtime/modelCatalog.ts +++ b/apps/synth_desktop/src/renderer/src/runtime/modelCatalog.ts @@ -16,6 +16,7 @@ export const BROWSER_MODEL_CATALOG: ModelCatalog = { generatedAt: "browser-fixture", diagnostics: [], entries: [ + browserFixtureEntry("openrouter-astra", "openai/gpt-6-astra", "GPT-6 Astra", ["text", "image"], "effort", 1050000), browserFixtureEntry("openrouter-luna", "openai/gpt-5.6-luna", "GPT 5.6 Luna", ["text", "image"], "effort", 272000), browserFixtureEntry("openrouter-laguna-s", "poolside/laguna-s-2.1", "Laguna S 2.1", ["text"], "binary", 262144), browserFixtureEntry("openrouter-muse-spark", "meta/muse-spark-1.2", "Muse Spark 1.2", ["text", "image"], "effort", 1048576), diff --git a/apps/synth_desktop/src/renderer/src/runtime/modelPerformanceLabels.ts b/apps/synth_desktop/src/renderer/src/runtime/modelPerformanceLabels.ts index 53f5bc46..dbc3a71f 100644 --- a/apps/synth_desktop/src/renderer/src/runtime/modelPerformanceLabels.ts +++ b/apps/synth_desktop/src/renderer/src/runtime/modelPerformanceLabels.ts @@ -2,6 +2,7 @@ import { CHATGPT_LUNA_MODEL, CHATGPT_SOL_MODEL, CHATGPT_TERRA_MODEL, + CHATGPT_ASTRA_MODEL, SYNTH_CLOUD_LAGUNA_S_MODEL, SYNTH_CLOUD_MUSE_SPARK_MODEL } from "../types/landing"; @@ -11,6 +12,7 @@ import { modelCatalogEntryForModel } from "./modelCatalog"; export function performanceTargetId(summary: ModelPerformanceSummary): string | null { if (summary.provider === "local-laguna") return "local-laguna"; if (summary.provider === "openai-codex-oauth") { + if (summary.modelId === CHATGPT_ASTRA_MODEL) return "chatgpt-astra"; if (summary.modelId === CHATGPT_LUNA_MODEL) return "chatgpt-luna"; if (summary.modelId === CHATGPT_SOL_MODEL) return "chatgpt-sol"; if (summary.modelId === CHATGPT_TERRA_MODEL) return "chatgpt-terra"; diff --git a/apps/synth_desktop/src/renderer/src/runtime/nativeCodex.ts b/apps/synth_desktop/src/renderer/src/runtime/nativeCodex.ts index f5d73a38..12267bab 100644 --- a/apps/synth_desktop/src/renderer/src/runtime/nativeCodex.ts +++ b/apps/synth_desktop/src/renderer/src/runtime/nativeCodex.ts @@ -70,10 +70,13 @@ export function codexStartRequest( sessionId, workspace, baseUrl: "https://chatgpt.com/backend-api/codex", model: target.model, providerName: "openai-codex-oauth", providerTitle: "ChatGPT subscription (Codex OAuth)", providerEnvKey: "", - autoCompactTokenLimit: autoCompactTokenLimits.luna ?? 250_000, serviceTier, ...approval + autoCompactTokenLimit: target.model === "gpt-6-astra" ? autoCompactTokenLimits.astra ?? 250_000 : autoCompactTokenLimits.luna ?? 250_000, + serviceTier: target.model === "gpt-6-astra" ? "default" : serviceTier, ...approval }; } - const autoCompactTokenLimit = target.model.includes("gpt-5.6-luna") + const autoCompactTokenLimit = target.model === "openai/gpt-6-astra" + ? autoCompactTokenLimits.astra ?? 250_000 + : target.model.includes("gpt-5.6-luna") ? autoCompactTokenLimits.luna ?? 250_000 : target.model.includes("muse-spark-1.2") ? 250_000 diff --git a/apps/synth_desktop/src/renderer/src/runtime/sessionView.ts b/apps/synth_desktop/src/renderer/src/runtime/sessionView.ts index afb99ebc..29e59b9c 100644 --- a/apps/synth_desktop/src/renderer/src/runtime/sessionView.ts +++ b/apps/synth_desktop/src/renderer/src/runtime/sessionView.ts @@ -13,6 +13,7 @@ import { CHATGPT_LUNA_MODEL, CHATGPT_SOL_MODEL, CHATGPT_TERRA_MODEL, + CHATGPT_ASTRA_MODEL, SYNTH_CLOUD_LAGUNA_S_MODEL, SYNTH_CLOUD_LAGUNA_XS_B200_MODEL, SYNTH_CLOUD_LAGUNA_XS_H100_MODEL, @@ -105,6 +106,8 @@ export function targetIdToExecutionTarget(targetId: string, adapter: string | nu } switch (targetId) { + case "chatgpt-astra": + return { kind: "remote", provider: "openai-codex-oauth", model: CHATGPT_ASTRA_MODEL, adapter: remoteAdapter }; case "chatgpt-luna": case "chatgpt-sol": case "chatgpt-terra": @@ -165,7 +168,7 @@ export function executionTargetToUiId(target: ExecutionTarget): string { : "synth-cloud-laguna-s"; } if (target.provider === "openai-codex-oauth") { - return target.model === CHATGPT_SOL_MODEL ? "chatgpt-sol" : target.model === CHATGPT_TERRA_MODEL ? "chatgpt-terra" : "chatgpt-luna"; + return target.model === CHATGPT_ASTRA_MODEL ? "chatgpt-astra" : target.model === CHATGPT_SOL_MODEL ? "chatgpt-sol" : target.model === CHATGPT_TERRA_MODEL ? "chatgpt-terra" : "chatgpt-luna"; } if (target.targetId) { if (!modelCatalogEntry(target.targetId)) rememberHistoricalOpenRouterTarget(target, target.targetId); diff --git a/apps/synth_desktop/src/renderer/src/types/landing.ts b/apps/synth_desktop/src/renderer/src/types/landing.ts index d0f9a2c9..e2b3cde8 100644 --- a/apps/synth_desktop/src/renderer/src/types/landing.ts +++ b/apps/synth_desktop/src/renderer/src/types/landing.ts @@ -100,6 +100,11 @@ export function resolveDefaultTargetId( ): string { const model = preference.model.toLowerCase(); for (const provider of preference.providers) { + if (provider === "chatgpt" && availability.chatgpt && model === CHATGPT_ASTRA_MODEL) return "chatgpt-astra"; + if (provider === "openrouter" && availability.openrouter && model === CHATGPT_ASTRA_MODEL) { + const astra = targets.find(target => target.modelId === "openai/gpt-6-astra" && target.selectable !== false); + if (astra) return astra.id; + } if (provider === "chatgpt" && availability.chatgpt && model === CHATGPT_LUNA_MODEL) return "chatgpt-luna"; if (provider === "openrouter" && availability.openrouter && model === CHATGPT_LUNA_MODEL) { return targets.find((target) => target.modelId === "openai/gpt-5.6-luna" && target.selectable !== false)?.id ?? "local-laguna"; @@ -432,6 +437,7 @@ export const SYNTH_CLOUD_MUSE_SPARK_MODEL = "meta/muse-spark-1.2"; export const CHATGPT_LUNA_MODEL = "gpt-5.6-luna"; export const CHATGPT_SOL_MODEL = "gpt-5.6-sol"; export const CHATGPT_TERRA_MODEL = "gpt-5.6-terra"; +export const CHATGPT_ASTRA_MODEL = "gpt-6-astra"; export function isOpenRouterTargetId(id: string): boolean { return id.startsWith("openrouter-") || id.startsWith("openrouter:"); @@ -444,6 +450,7 @@ const BUILTIN_EXECUTION_TARGETS: ExecutionTargetOption[] = [ description: "Local · MLX · Metal · usage tracked", group: "local" }, + { id: "chatgpt-astra", label: "GPT-6 Astra", description: "ChatGPT · Codex plan allowance · account access required", group: "subscription" }, { id: "chatgpt-luna", label: "GPT-5.6 Luna", diff --git a/examples/runebench/.gitignore b/examples/runebench/.gitignore new file mode 100644 index 00000000..f3bb397c --- /dev/null +++ b/examples/runebench/.gitignore @@ -0,0 +1,19 @@ +.venv/ +.env +.annotation-capability +.run.lock +results/ +trace-v5/ +annotation-evidence/ +frame-cache/ +web/ +swarm.sqlite +swarm-data.json +trace-bindings.json +viewer.built.tsx +visual-request.json +visual-update.json +budget-*.json +__pycache__/ +docker-config/* +!docker-config/config.json diff --git a/examples/runebench/Dockerfile b/examples/runebench/Dockerfile new file mode 100644 index 00000000..ba85bb0b --- /dev/null +++ b/examples/runebench/Dockerfile @@ -0,0 +1,8 @@ +FROM ghcr.io/maxbittker/rs-agent-benchmark@sha256:baa2f071f19c9e5fffe6c2003cb444a11421af33e1a3df4e666ae5db974411ba +COPY entrypoint.sh observe.ts provision.ts save-generator.ts scenario.json engine-hook.ts smoke.ts /app/ma/ +RUN cp /app/ma/engine-hook.ts /app/server/engine/src/ma-hook.ts && \ + sed -i "/await World.start();/i import { installMaClock } from './ma-hook';\ninstallMaClock(World);" /app/server/engine/src/app.ts +RUN sed -i 's/\&fps=15/\&fps=6/' /app/server/gateway/launch-bot.ts +RUN sed -i "s/protocolTimeout: 30_000/protocolTimeout: 300_000/; s/waitUntil: 'networkidle2', timeout: 60000/waitUntil: 'domcontentloaded', timeout: 300000/" /app/server/gateway/launch-bot.ts +ENV BOT_NAMES="maa mab mba mbb" NODE_TICKRATE=300 NODE_XPRATE=1 GATEWAY_URL=ws://localhost:7780 +ENTRYPOINT ["/bin/bash", "/app/ma/entrypoint.sh"] diff --git a/examples/runebench/LICENSE.rs-sdk b/examples/runebench/LICENSE.rs-sdk new file mode 100644 index 00000000..e4597acb --- /dev/null +++ b/examples/runebench/LICENSE.rs-sdk @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Lost City + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/examples/runebench/PROMPT.md b/examples/runebench/PROMPT.md new file mode 100644 index 00000000..09fdb3fc --- /dev/null +++ b/examples/runebench/PROMPT.md @@ -0,0 +1,22 @@ +Help me try this checkout's `examples/runebench` example. Read its README and +manifest, then do the setup for me without modifying existing projects. + +Start with the bundled recorded sample when available. Open it as a native +Workshop visual using the advertised visual tools, the generated +`visual-request.json`, and this checkout's visual skill. Inspect the rendered +view before saying it works. If native visual tools are unavailable, say so +explicitly; the browser viewer is a separate fallback, not native acceptance. + +Show all four agents, focus one, compare two, inspect a message and an unsuccessful +action if one was recorded, then follow an exact source record and game frame. +Do not invent a failure or message if this sample lacks one. + +Next run `bash examples/runebench/example.sh run` for a fresh 25-second scripted +episode. Check Docker first and ask before installing a system application. +This run needs no AI credentials. Rebuild the review, refresh the same visual, +and explain the recorded outcome with three exact actor/event citations. + +Do not run any paid model policies or silently substitute an API provider. After +the scripted path works, explain the optional OpenRouter mode and its enforced +aggregate budget, then wait for my approval. My ChatGPT login does not authorize +paid game agents. Never read Keychain or paste a key into a tool argument or chat. diff --git a/examples/runebench/README.md b/examples/runebench/README.md new file mode 100644 index 00000000..61799386 --- /dev/null +++ b/examples/runebench/README.md @@ -0,0 +1,75 @@ +# Four agents, one shared world + +Workshop example: **Woodland sprint**, two teams of two scripted actors chopping +trees and communicating. This adapts RuneBench; it is not the Runite Race task +or a claim about model performance. It never connects to official game servers. + +## Start here + +From the Workshop repository after `scripts/install.sh`: + +```bash +bash examples/runebench/example.sh setup +bash examples/runebench/example.sh sample +bash examples/runebench/example.sh review +``` + +Open http://127.0.0.1:8128/viewer. The bundled, checksum-verified sample is a real +September 7 recording: four actors, zero model calls. Reviewing it needs neither +Docker nor provider credentials. It is retained evidence, not a fresh run. + +For a fresh episode, stop the review server with Ctrl-C, run +`bash examples/runebench/example.sh run`, then start `review` again. +`run` is **scripted by default**, four actors, +25 seconds after readiness, three decisions per actor, zero model calls. Image +download and startup take longer than the episode. The public AMD64 game image +is about 4 GB and uses emulation on Apple Silicon. Docker Desktop or OrbStack +must be running; setup does not install or start either without your involvement. +Setup installs an isolated, hash-locked Python environment and ffmpeg if missing. + +The inspector starts on All agents: focus one actor, compare a second, follow +messages and actions, query unsuccessful chops or same-tree attempts, and open +the exact source record and recorded game frame. Recording startup gaps are +labeled. Same-tree attempts are not proof of causal interference. + +Results are retained in `results/`, using a unique directory for each run. The +runner checks all-player readiness, cross-actor capability rejection, authoritative +cutoff, rejection of late actions, and an unchanged cutoff receipt. It stops only +its own example container. A per-checkout lock prevents overlapping runs, and +an occupied controller port fails before launch. Ctrl-C stops the review server. + +## Use from a Workshop conversation + +Give Workshop [PROMPT.md](PROMPT.md). `viewer.tsx` uses Workshop's shared trace +components. `build_web.mjs` also produces `visual-request.json`, the source and +bindings for native visual creation. Treat that generated file as local/private: +it contains a local annotation capability, not a provider credential. Never +commit it or distribute it with the sample. + +## Optional model-driven run + +Only after scripted review works, explicitly authorize the provider budget: + +```bash +bash examples/runebench/example.sh run --provider openrouter --env-file /absolute/private.env +``` + +The file must contain `OPENROUTER_API_KEY=...`; keep it mode 600 and out of Git. +ChatGPT login for the Workshop conversation does not authenticate these game +policies. This example supports Luna/Terra policy IDs, not Astra game policies. +No keys enter Docker and no Keychain importer is used. OpenRouter requests disable +provider fallbacks. Default batch reservations persist with a $5/128-call ceiling; +an unknown-cost call stops further admission. Never rename a batch to evade a cap. +No paid acceptance run is included in this example's verification. + +## Reproducibility and attribution + +`manifest.json` identifies the immutable upstream image and trace schema. +Container adapter code comes from public `synth-containers==0.4.3`, not a sibling +source checkout. Visuals come from this Workshop checkout and its npm lockfile. +No private evals framework is required. `requirements.lock` pins Python packages +and hashes. Game RNG is unseeded; repeats are descriptive, not paired-seed trials. + +Built on [RuneBench](https://github.com/MaxBittker/runebench), +[rs-sdk](https://github.com/MaxBittker/rs-sdk), and LostCity. Upstream code and +game assets retain their respective notices and terms. See `THIRD_PARTY.md`. diff --git a/examples/runebench/THIRD_PARTY.md b/examples/runebench/THIRD_PARTY.md new file mode 100644 index 00000000..18df4d09 --- /dev/null +++ b/examples/runebench/THIRD_PARTY.md @@ -0,0 +1,11 @@ +# Upstream notices + +RuneBench: https://github.com/MaxBittker/runebench (README declares MIT). +rs-sdk: https://github.com/MaxBittker/rs-sdk (MIT). +The save generator is adapted from RuneBench's generator; the game SDK and +LostCity engine/client are consumed from the pinned upstream container, not +redistributed as Workshop-owned code. Their notices remain in that image. + +This is a research emulator, not an official Jagex service. Workshop does not +claim ownership of RuneScape names or game assets. Game-asset terms are separate +from Workshop's source-code license. No official game account is required. diff --git a/examples/runebench/build_deps.mjs b/examples/runebench/build_deps.mjs new file mode 100644 index 00000000..2628a318 --- /dev/null +++ b/examples/runebench/build_deps.mjs @@ -0,0 +1,5 @@ +import { createRequire } from 'node:module'; +// Resolve from the app's locked Vite dependency, whether npm hoists it or not. +const appRequire = createRequire(new URL('../../apps/synth_desktop/package.json', import.meta.url)); +const viteRequire = createRequire(appRequire.resolve('vite')); +export const { build } = viteRequire('esbuild'); diff --git a/examples/runebench/build_traces.py b/examples/runebench/build_traces.py new file mode 100644 index 00000000..330ba88d --- /dev/null +++ b/examples/runebench/build_traces.py @@ -0,0 +1,37 @@ +"""Build V5 authority and shared viewer bindings from existing runs, without inference.""" +import json, sys +from pathlib import Path +HERE = Path(__file__).resolve().parent + +from synth_containers.tracing.adapters.runebench import runebench_projection +from synth_containers.tracing.validation.validator import validate_trace, validate_evidence +from synth_containers.tracing.annotation_store import AnnotationStore +from synth_containers.tracing.projections.rollout_inspector import rollout_inspector_from_sealed +from synth_containers.tracing.projections.visual import visual_from_sealed +from synth_containers.tracing.validation.rehydrate import trace_document_from_payload, evidence_bundle_from_payload + +def build(): + projections = {} + store = AnnotationStore(HERE / 'annotation-evidence') + output = HERE / 'trace-v5' + output.mkdir(exist_ok=True) + for run in sorted((HERE / 'results').iterdir()): + if not (run / 'episode/job_result.json').exists(): continue + document, evidence, packet = runebench_projection(run) + evidence = store.load(document, evidence) + packet = rollout_inspector_from_sealed(document, evidence) + findings = validate_trace(document) + validate_evidence(document, evidence)[0] + errors = [f.to_dict() for f in findings if str(f.severity) == 'error'] + if errors: raise ValueError(errors) + for name, record in [('trace', document), ('evidence', evidence), ('projection', packet)]: + (output / f'{run.name}.{name}.json').write_text(json.dumps(record.to_dict(), separators=(',', ':'))) + projections[run.name] = packet.visual.to_dict() + # Larger Craftax decision capture loads on demand through /annotations. + for key in ['craftax-retained']: + if (output / f'{key}.trace.json').exists(): + doc = trace_document_from_payload(json.loads((output / f'{key}.trace.json').read_text())) + base = evidence_bundle_from_payload(json.loads((output / f'{key}.evidence.json').read_text())) + projections[key] = visual_from_sealed(doc, store.load(doc, base)).to_dict() + (HERE / 'trace-bindings.json').write_text(json.dumps({'traceProjections': projections}, separators=(',', ':'))) + print(json.dumps({'runs': len(projections), 'items': sum(len(p['items']) for p in projections.values())})) +if __name__ == '__main__': build() diff --git a/examples/runebench/build_web.mjs b/examples/runebench/build_web.mjs new file mode 100644 index 00000000..7f4c8123 --- /dev/null +++ b/examples/runebench/build_web.mjs @@ -0,0 +1,42 @@ +import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { resolve, dirname } from 'node:path'; +import { gzipSync } from 'node:zlib'; +import { createHash } from 'node:crypto'; +import { execFileSync } from 'node:child_process'; +import { build } from './build_deps.mjs'; + +const root = dirname(fileURLToPath(import.meta.url)); +const bindings = JSON.parse(readFileSync(resolve(root, 'trace-bindings.json'), 'utf8')); +// Keep sealed traces untouched. Enrich only the rebuildable query projection +// with loopback source links, never absolute filesystem paths. +const raw = JSON.parse(readFileSync(resolve(root, 'swarm-data.json'), 'utf8')); +if (raw.revision !== bindings.swarmData.revision) throw new Error('Query/trace projection revisions differ; rebuild the index explicitly.'); +for (const [name, result] of Object.entries(bindings.swarmData.queries)) { + const originals = raw.queries[name].rows; + result.rows.forEach((hit, index) => { + const original = originals[index]; + if (!original || hit.run_id !== original.run_id || hit.actor !== original.actor || hit.ms !== original.ms) throw new Error('Query row identity mismatch'); + const match = String(original.ref || '').match(/\/(episode\/)?events\.jsonl:(\d+)$/); + if (match) hit.evidenceUrl = `http://127.0.0.1:8128/evidence?run=${encodeURIComponent(hit.run_id)}&scope=${match[1]?'episode':'run'}&line=${match[2]}`; + }); +} +const bytes = Buffer.from(JSON.stringify(bindings)); +// Always compile authored source; do not require a destructive index rebuild +// just to pick up a UI change. +writeFileSync(resolve(root, 'viewer.built.tsx'), readFileSync(resolve(root, 'viewer.tsx'))); +const capability = execFileSync(process.env.RUNEBENCH_PYTHON || resolve(root,'.venv/bin/python'), ['-c', 'from evidence_api import capability; print(capability())'], { cwd: root, encoding: 'utf8' }).trim(); +const data = { traceArchive: { encoding: 'gzip+base64', sha256: createHash('sha256').update(bytes).digest('hex'), data: gzipSync(bytes).toString('base64') }, annotationService: { url: 'http://127.0.0.1:8128/annotations', capability } }; +mkdirSync(resolve(root, 'web'), { recursive: true }); +writeFileSync(resolve(root, 'visual-request.json'), JSON.stringify({template_id:'sourced.visual.v1',title:'RuneBench · four-agent review',content:readFileSync(resolve(root,'viewer.tsx'),'utf8'),bindings:{schemaVersion:'synth.visual-bindings.v1',inputs:[{input:'data',kind:'inline',data}]} }), {mode:0o600}); +// Optional update payload: use only an ID and revision returned by Workshop. +if (process.argv[2]) { + const revision = Number(process.argv[3]); + if (!/^vis_[a-zA-Z0-9]+$/.test(process.argv[2]) || !Number.isSafeInteger(revision) || revision < 1) throw new Error('Expected an existing visual ID and positive revision'); + const request = JSON.parse(readFileSync(resolve(root, 'visual-request.json'), 'utf8')); + delete request.template_id; + writeFileSync(resolve(root, 'visual-update.json'), JSON.stringify({...request,visual_id:process.argv[2],expected_revision:revision}), {mode:0o600}); +} +const result = await build({ stdin: { contents: `import React from 'react'; import {createRoot} from 'react-dom/client'; import Shell from './viewer.built.tsx'; createRoot(document.getElementById('root')).render();`, resolveDir: root, loader: 'tsx' }, bundle: true, write: false, platform: 'browser', format: 'iife', jsx: 'automatic', minify: true, define: { 'process.env.NODE_ENV': '"production"' }, alias: { '@synth/visuals/components/agent_trace.v1': resolve(root,'../../packages/workshop-visuals/components/agent_trace.v1/AgentTraceInspector.tsx'), 'react-dom': resolve(root,'../../node_modules/react-dom'), react: resolve(root,'../../node_modules/react') } }); +writeFileSync(resolve(root, 'web/index.html'), `RuneBench trace review
`); +console.log('Built standalone viewer with shared components and evidence binding.'); diff --git a/examples/runebench/compose.yaml b/examples/runebench/compose.yaml new file mode 100644 index 00000000..0ef1c00f --- /dev/null +++ b/examples/runebench/compose.yaml @@ -0,0 +1,14 @@ +services: + ma: + build: . + platform: linux/amd64 + container_name: workshop-runebench-${MA_RUN_ID:?run through example.sh} + init: true + environment: + MA_RUN_ID: ${MA_RUN_ID} + MA_EVIDENCE_KIND: ${MA_EVIDENCE_KIND:-scripted} + shm_size: 2gb + ports: + - "127.0.0.1:8126:8790" + volumes: + - ${MA_ARTIFACTS:?run through example.sh}:/logs/ma diff --git a/examples/runebench/docker-config/config.json b/examples/runebench/docker-config/config.json new file mode 100644 index 00000000..99dd6c42 --- /dev/null +++ b/examples/runebench/docker-config/config.json @@ -0,0 +1 @@ +{"auths":{},"cliPluginsExtraDirs":["/opt/homebrew/lib/docker/cli-plugins","/Applications/Docker.app/Contents/Resources/cli-plugins"]} diff --git a/examples/runebench/engine-hook.ts b/examples/runebench/engine-hook.ts new file mode 100644 index 00000000..f73245de --- /dev/null +++ b/examples/runebench/engine-hook.ts @@ -0,0 +1,52 @@ +import {readFileSync,writeFileSync,appendFileSync,renameSync} from 'node:fs'; +import {performance} from 'node:perf_hooks'; +const scenario=JSON.parse(readFileSync(process.env.MA_SCENARIO_FILE||'/app/ma/scenario.json','utf8')); +const logDir=process.env.MA_LOG_DIR||'/logs/ma'; +// Imported in the engine process before World.start(). Never available to policies. +export function installMaClock(world:any) { + let phase='idle', start=0, deadline=0, baseline:any=null, last:any=null, cutoff:any=null; + const atomic=(name:string,value:any)=>{writeFileSync(`${logDir}/${name}.tmp`,JSON.stringify(value));renameSync(`${logDir}/${name}.tmp`,`${logDir}/${name}`)}; + function snapshot(){ + const players=world.players.filter(Boolean); + const actors=Object.fromEntries(scenario.actors.map(a=>{ + const p=players.find((p:any)=>p.username===a.id); + return [a.id,p?{xpTenths:p.stats[8],xp:p.stats[8]/10,x:p.x,z:p.z,level:p.level,loggedIn:true}:null]; + })); + return {tick:world.currentTick,elapsedMs:start?performance.now()-start:0,at:new Date().toISOString(),actors}; + } + function finish(reason:string){ + if(phase!=='running')return; + phase='frozen'; + const scores=Object.fromEntries(scenario.actors.map(a=>[a.id,last?.actors[a.id]&&baseline?.actors[a.id]?(last.actors[a.id].xpTenths-baseline.actors[a.id].xpTenths)/10:null])); + const teams=Object.fromEntries([...new Set(scenario.actors.map(a=>a.team))].map(t=>[t,scenario.actors.filter(a=>a.team===t).every(a=>scores[a.id]!==null)?scenario.actors.filter(a=>a.team===t).reduce((s,a)=>s+scores[a.id],0):null])); + cutoff={phase,reason,rule:'last-complete-engine-tick-at-or-before-deadline',durationMs:deadline-start,freezeElapsedMs:performance.now()-start,baseline,final:last,scores,teams,valid:reason==='deadline'&&Object.values(scores).every(v=>v!==null)}; + atomic('cutoff.json',cutoff); + } + const original=world.cycle.bind(world); + world.cycle=function(){ + if(phase==='frozen')return; + if(phase==='running'&&performance.now()>=deadline){finish('deadline');return;} + original(); + if(phase==='running'){ + const s=snapshot(); + if(performance.now()<=deadline){last=s;appendFileSync(`${logDir}/engine-states.jsonl`,JSON.stringify(s)+'\n');} + else finish('deadline'); + } + }; + return Bun.serve({hostname:'127.0.0.1',port:Number(process.env.MA_CLOCK_PORT??8792),fetch:async req=>{ + const path=new URL(req.url).pathname; + if(path==='/status')return Response.json({phase,remainingMs:phase==='running'?Math.max(0,deadline-performance.now()):0,snapshot:snapshot(),cutoff}); + if(req.method==='POST'&&path==='/arm'){ + if(phase!=='idle')return new Response('Already armed',{status:409}); + const b=await req.json();const seconds=b.duration_seconds; + if(!Number.isInteger(seconds)||seconds<10||seconds>600)return new Response('Duration must be 10–600 seconds',{status:400}); + baseline=snapshot();if(Object.values(baseline.actors).some(v=>!v))return new Response('Players not ready',{status:409}); + // The pinned engine uses level/10 in its XP curve, not modern RS level/7. + if(scenario.actors.some(a=>baseline.actors[a.id].x!==a.position.x||baseline.actors[a.id].z!==a.position.z||baseline.actors[a.id].xp!==980))return new Response('Scenario save mismatch: expected start positions and level-10 Woodcutting (980 XP in this engine)',{status:409}); + start=performance.now();deadline=start+seconds*1000;baseline.elapsedMs=0;last=baseline;phase='running'; + atomic('baseline.json',baseline);return Response.json({phase,durationMs:seconds*1000,baseline}); + } + if(req.method==='POST'&&path==='/stop'){finish('operator-stop');return Response.json(cutoff);} + return new Response('Not found',{status:404}); + }}); +} diff --git a/examples/runebench/entrypoint.sh b/examples/runebench/entrypoint.sh new file mode 100644 index 00000000..94c27723 --- /dev/null +++ b/examples/runebench/entrypoint.sh @@ -0,0 +1,26 @@ +#!/bin/bash +set -euo pipefail +mkdir -p /logs/ma +cd /app +bun-svc run ma/provision.ts +(cd /app/server/engine && exec bun-svc run src/app.ts) >/logs/ma/engine.log 2>&1 & +engine=$! +wait_http() { + for ((i=0;i<600;i++)); do + kill -0 "$engine" || exit 1 + if curl -fsS "$1" >/dev/null 2>&1; then return; fi + sleep 1 + done + echo "Readiness timeout: $1" >&2; exit 1 +} +wait_http http://localhost:8888 +(cd /app/server/gateway && exec bun-svc run gateway.ts) >/logs/ma/gateway.log 2>&1 & +wait_http http://localhost:7780 +idx=99 +for name in $BOT_NAMES; do + Xvfb :$idx -screen 0 800x600x24 -ac >/logs/ma/display-$name.log 2>&1 & + sleep 1 + (cd /app/server/gateway && DISPLAY=:$idx BOT_NAME=$name exec bun-svc run launch-bot.ts) >/logs/ma/client-$name.log 2>&1 & + idx=$((idx+1)) +done +exec bun-svc run /app/ma/observe.ts diff --git a/examples/runebench/evidence_api.py b/examples/runebench/evidence_api.py new file mode 100644 index 00000000..3882dd4d --- /dev/null +++ b/examples/runebench/evidence_api.py @@ -0,0 +1,39 @@ +"""Loopback demo adapter for Containers' canonical annotation evidence store.""" +import json +import os +import secrets +import sys +from pathlib import Path + +HERE = Path(__file__).resolve().parent + +from synth_containers.tracing.annotation_store import AnnotationStore +from synth_containers.tracing.validation.rehydrate import rehydrate_trace, evidence_bundle_from_payload +from synth_containers.tracing.projections.visual import visual_from_sealed + +STORE = AnnotationStore(HERE / 'annotation-evidence') + +def capability(): + path = HERE / '.annotation-capability' + if not path.exists(): + try: + fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + with os.fdopen(fd, 'w') as f: f.write(secrets.token_urlsafe(32)) + except FileExistsError: pass + return path.read_text().strip() + +def sources(run): + if not run or any(c not in 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-' for c in run): + raise ValueError('Invalid run') + directory = HERE / 'trace-v5' + document = rehydrate_trace(json.loads((directory / f'{run}.trace.json').read_text())) + base = evidence_bundle_from_payload(json.loads((directory / f'{run}.evidence.json').read_text())) + return document, base + +def evidence(run, request=None): + document, base = sources(run) + if request is None: + bundle = STORE.load(document, base) + else: + bundle, _ = STORE.append(document, base, request) + return {'projection': visual_from_sealed(document, bundle).to_dict(), 'evidence_digest': bundle.content_digest} diff --git a/examples/runebench/example.sh b/examples/runebench/example.sh new file mode 100644 index 00000000..9e10b70a --- /dev/null +++ b/examples/runebench/example.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +set -euo pipefail +example_root="$(cd "$(dirname "$0")" && pwd)" +repo_root="$(cd "$example_root/../.." && pwd)" +cd "$example_root" +source "$repo_root/scripts/local-toolchain-env.sh" +export DOCKER_CONFIG="$example_root/docker-config" +case "${1:-help}" in + setup) + command -v uv >/dev/null || { echo 'Run scripts/install.sh --bootstrap first.' >&2; exit 1; } + node build_deps.mjs >/dev/null || { echo 'Run scripts/install.sh first.' >&2; exit 1; } + [[ -x .venv/bin/python ]] || uv venv --python 3.12 .venv + uv pip sync --python .venv/bin/python --require-hashes requirements.lock + if ! command -v ffmpeg >/dev/null; then + command -v brew >/dev/null || { echo 'Install ffmpeg before replaying game frames.' >&2; exit 1; } + brew install ffmpeg + fi + ;; + run) + [[ -x .venv/bin/python ]] || { echo 'Run bash examples/runebench/example.sh setup first.' >&2; exit 1; } + docker info >/dev/null || { echo 'Start Docker Desktop or OrbStack and retry.' >&2; exit 1; } + docker compose version >/dev/null + shift + .venv/bin/python run.py --duration 25 --calls 3 "$@" + .venv/bin/python query.py --build + node build_web.mjs + echo 'Run finished. Start review with: bash examples/runebench/example.sh review' + ;; + sample) + [[ -x .venv/bin/python ]] || { echo 'Run setup first.' >&2; exit 1; } + .venv/bin/python sample.py + .venv/bin/python query.py --build + node build_web.mjs + echo 'Sample ready. Run: bash examples/runebench/example.sh review' + ;; + rebuild) + .venv/bin/python query.py --build + node build_web.mjs + ;; + review) + [[ -f web/index.html ]] || { echo 'No review built yet. Run sample or run first.' >&2; exit 1; } + command -v ffmpeg >/dev/null || { echo 'Install ffmpeg for video frames (brew install ffmpeg).' >&2; exit 1; } + echo 'Open http://127.0.0.1:8128/viewer — Ctrl-C stops only this review server.' + .venv/bin/python serve.py + ;; + *) + echo 'Usage: bash examples/runebench/example.sh setup | sample | run [--scripted] | rebuild | review' + echo 'Default run: four scripted actors, 25 seconds, zero model calls.' + echo 'Optional paid mode: run --provider openrouter --env-file /private/path/.env' + ;; +esac diff --git a/examples/runebench/manifest.json b/examples/runebench/manifest.json new file mode 100644 index 00000000..670c5431 --- /dev/null +++ b/examples/runebench/manifest.json @@ -0,0 +1,23 @@ +{ + "schema": "workshop.example.v1", + "id": "runebench-woodland-sprint", + "upstream": "https://github.com/MaxBittker/runebench", + "sdk": "https://github.com/MaxBittker/rs-sdk", + "image": "ghcr.io/maxbittker/rs-agent-benchmark@sha256:baa2f071f19c9e5fffe6c2003cb444a11421af33e1a3df4e666ae5db974411ba", + "platform": "linux/amd64", + "actors": 4, + "defaultMode": "scripted", + "defaultDurationSeconds": 25, + "traceSchema": "synth.trace.v5", + "containersPackage": "synth-containers==0.4.3", + "viewer": "http://127.0.0.1:8128/viewer", + "visualSource": "viewer.tsx", + "visualDataBuilder": "build_web.mjs", + "sample": { + "file": "sample.tar.gz", + "sha256": "d2d2da454fce87bb827fc927020824a717fb4c996f6b001ab2d4231ac11fbca6", + "run": "20260907T182716Z-scripted", + "kind": "scripted", + "modelCalls": 0 + } +} diff --git a/examples/runebench/observe.ts b/examples/runebench/observe.ts new file mode 100644 index 00000000..744c7710 --- /dev/null +++ b/examples/runebench/observe.ts @@ -0,0 +1,101 @@ +import {BotSDK} from '/app/sdk/index'; +import {BotActions} from '/app/sdk/actions'; +import {appendFileSync,writeFileSync} from 'node:fs'; +import {randomBytes} from 'node:crypto'; +import scenario from './scenario.json'; +scenario.evidence_kind=process.env.MA_EVIDENCE_KIND||'scripted'; +const names=scenario.actors.map(a=>a.id), observers=new Map(), controllers=new Map(); +const tokens=new Map(names.map(n=>[n,randomBytes(24).toString('hex')])); +const busy=new Set(), recorders:any[]=[]; +let latest:any={ready:false,bots:{}}, clock:any={phase:'idle'}, startAt=0, sequence=0, started=false; +const seen=new Set(); +function event(kind:string,actor:string|null,payload:any){ + appendFileSync('/logs/ma/events.jsonl',JSON.stringify({schema_version:'evals.event-stream.v1',kind:'trace.raw',source:'local',occurred_at:new Date().toISOString(),run_id:process.env.MA_RUN_ID||'local-ma',sequence:String(++sequence),actor_id:actor,payload:{kind,elapsedMs:startAt?Date.now()-startAt:0,...payload}})+'\n'); +} +async function engine(path:string,body?:any){ + const r=await fetch('http://127.0.0.1:8792'+path,{method:body?'POST':'GET',headers:{'Content-Type':'application/json'},body:body?JSON.stringify(body):undefined}); + if(!r.ok)throw Error(await r.text());return r.json(); +} +function privateView(name:string){ + const s=latest.bots[name];if(!s)return null; + return {actor:scenario.actors.find(a=>a.id===name),remainingMs:clock.remainingMs,phase:clock.phase,tick:s.tick,player:s.player,skills:s.skills,inventory:s.inventory,nearbyPlayers:s.nearbyPlayers,nearbyLocs:s.nearbyLocs?.slice(0,45),gameMessages:s.gameMessages?.slice(-18),menu:s.menu,dialog:s.dialog}; +} +Bun.serve({port:8790,idleTimeout:120,fetch:async req=>{ + try{ + const path=new URL(req.url).pathname; + if(req.method==='GET'&&path==='/health')return Response.json({ready:latest.ready,names,phase:clock.phase},{status:latest.ready?200:503}); + if(req.method==='GET'&&path==='/state')return Response.json({...latest,clock,scenario}); + if(req.method==='POST'&&path==='/arm'){ + if(started||!latest.ready)return new Response('Not ready or already started',{status:409}); + started=true; + for(const name of names){ + const sdk=new BotSDK({botUsername:name,password:'test',gatewayUrl:'ws://localhost:7780',connectionMode:'control',autoLaunchBrowser:false,autoReconnect:false,actionTimeout:20000}); + await sdk.connect();controllers.set(name,{sdk,bot:new BotActions(sdk)}); + } + const b=await req.json();const result=await engine('/arm',{duration_seconds:b.duration_seconds});scenario.duration_seconds=b.duration_seconds;startAt=Date.now();clock=await engine('/status'); + event('episode.started',null,{scenario,baseline:result.baseline}); + for(const [i,name] of names.entries()){ + const recorder=Bun.spawn(['ffmpeg','-nostdin','-y','-f','x11grab','-framerate','2','-video_size','800x600','-i',`:${99+i}`,'-an','-c:v','libx264','-threads','1','-preset','ultrafast','-crf','27','-pix_fmt','yuv420p','-movflags','+frag_keyframe+empty_moov+default_base_moof',`/logs/ma/${name}.mp4`],{stdout:'ignore',stderr:Bun.file(`/logs/ma/record-${name}.log`)}); + recorders.push(recorder);event('media.started',name,{path:`${name}.mp4`,offsetMs:Date.now()-startAt}); + } + return Response.json({scenario,...result,tokens:Object.fromEntries(tokens)}); + } + if(req.method==='POST'&&path==='/stop'){const result=await engine('/stop',{});return Response.json(result);} + const match=path.match(/^\/actors\/([a-z0-9]+)\/(observation|action)$/); + if(match){ + const [,name,op]=match; + if(!tokens.has(name)||req.headers.get('authorization')!==`Bearer ${tokens.get(name)}`)return new Response('Wrong actor capability',{status:403}); + if(op==='observation'&&req.method==='GET')return Response.json(privateView(name)); + if(op==='action'&&req.method==='POST'){ + clock=await engine('/status'); + if(clock.phase!=='running'||clock.remainingMs<=0)return new Response('Episode closed',{status:409}); + if(busy.has(name))return new Response('Actor busy',{status:409}); + const a=await req.json(); + if(!['say','chop','walk','drop','wait'].includes(a.type))return new Response('Unsupported action',{status:400}); + const c=controllers.get(name)!; + const s=c.sdk.getState(); + if(a.type==='say'&&(typeof a.text!=='string'||a.text.length>80))return new Response('Chat must be at most 80 characters',{status:400}); + if(a.type==='walk'&&(!Number.isInteger(a.x)||!Number.isInteger(a.z)||Math.abs(a.x-s.player.worldX)>32||Math.abs(a.z-s.player.worldZ)>32))return new Response('Walk within 32 tiles',{status:400}); + if(a.type==='drop'&&(!Number.isInteger(a.slot)||!s.inventory.some((i:any)=>i.slot===a.slot&&/logs/i.test(i.name))))return new Response('Only logs may be dropped',{status:400}); + let tree:any; + if(a.type==='chop'){ + tree=a.x!==undefined?s.nearbyLocs.find((l:any)=>/^tree$/i.test(l.name)&&l.x===a.x&&l.z===a.z):c.sdk.findNearbyLoc(/^tree$/i); + if(!tree)return Response.json({success:false,message:'No normal tree at requested location'}); + } + busy.add(name);event('action.started',name,{action:a,target:tree?{x:tree.x,z:tree.z,id:tree.id}:null,privateObservation:privateView(name)}); + try{ + const result=a.type==='say'?await c.sdk.say(a.text):a.type==='walk'?await c.bot.walkTo(a.x,a.z,1):a.type==='chop'?await c.bot.chopTree(tree):a.type==='drop'?await c.sdk.sendDropItem(a.slot):await c.sdk.sendWait(2); + event('action.completed',name,{action:a,result});return Response.json(result); + }finally{busy.delete(name);} + } + } + return new Response('RuneBench MA: /health /state',{status:path==='/'?200:404}); + }catch(e){return Response.json({error:String(e)},{status:500});} +}}); +await Promise.all(names.map(async name=>{ + const sdk=new BotSDK({botUsername:name,password:'test',gatewayUrl:'ws://localhost:7780',connectionMode:'observe',autoLaunchBrowser:false,autoReconnect:true}); + await sdk.connect();observers.set(name,sdk); +})); +let sealed=false; +async function sample(){ + try{ + clock=await engine('/status'); + const snapshots=Object.fromEntries(names.map(name=>[name,observers.get(name)?.getState()??null])); + latest={timestamp:new Date().toISOString(),elapsedMs:startAt?Date.now()-startAt:0,ready:names.every(n=>snapshots[n]?.inGame),bots:snapshots}; + if(clock.phase==='running'){ + appendFileSync('/logs/ma/states.jsonl',JSON.stringify(latest)+'\n'); + for(const [name,s] of Object.entries(snapshots))for(const m of s?.gameMessages??[]){ + if(!m.sender)continue; + const key=`${name}|${m.sender}|${m.tick}|${m.type}|${m.text}`;if(seen.has(key))continue;seen.add(key); + event('message.observed',name,{sender:m.sender,text:m.text,clientTick:m.tick,messageType:m.type,visibility:'actor-observation'}); + } + } + writeFileSync('/logs/ma/latest.json',JSON.stringify(latest)); + if(clock.phase==='frozen'&&!sealed){ + sealed=true;event('episode.frozen',null,{cutoff:clock.cutoff}); + for(const p of recorders)p.kill('SIGINT'); + await Promise.all([...controllers.values()].map(c=>c.sdk.disconnect())); + } + }catch(e){console.error('Observer:',String(e));} +} +await sample();setInterval(sample,1000); diff --git a/examples/runebench/provision.ts b/examples/runebench/provision.ts new file mode 100644 index 00000000..43770c3d --- /dev/null +++ b/examples/runebench/provision.ts @@ -0,0 +1,13 @@ +import { generateSave, Items } from './save-generator'; +import { mkdirSync, writeFileSync } from 'node:fs'; +import scenario from './scenario.json'; +// A clean upstream image has no player directory yet. Create the engine's +// actual save root before the upstream generator chooses its fallback path. +mkdirSync('/app/server/engine/data/players/main', {recursive:true}); +for (const [i,actor] of scenario.actors.entries()) { + const name=actor.id; + if (!/^[a-z][a-z0-9]{1,11}$/.test(name)) throw Error('Invalid bot name'); + await generateSave(name, { position: actor.position, skills:{Woodcutting:10}, inventory:[{id:Items.BRONZE_AXE,count:1},{id:Items.TINDERBOX,count:1}], varps:{281:1000}, appearance:{colors:[i, i<2?4:8,0,0,0]} }); + mkdirSync(`/app/bots/${name}`,{recursive:true}); + writeFileSync(`/app/bots/${name}/bot.env`, `BOT_USERNAME=${name}\nPASSWORD=test\nSERVER=localhost\nSHOW_CHAT=true\n`); +} diff --git a/examples/runebench/query.py b/examples/runebench/query.py new file mode 100644 index 00000000..0138829e --- /dev/null +++ b/examples/runebench/query.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +"""Rebuildable swarm index and a read-only query path shared by viewer and analysis.""" +import sys +import argparse, datetime as dt, hashlib, json, sqlite3, re, subprocess +from pathlib import Path +HERE=Path(__file__).resolve().parent +OUT=HERE/'results' +DB=HERE/'swarm.sqlite' +QUERIES={ + 'all':"SELECT id AS run_id, evidence_kind, status, actors, score FROM episodes ORDER BY id DESC", + 'failed_chops':"SELECT run_id, actor, ms, text, ref FROM events WHERE kind='action.completed' AND action_type='chop' AND success=0 ORDER BY run_id,ms", + 'heard_teammate':"SELECT e.run_id,e.actor,e.sender,e.ms,e.text,e.ref FROM events e JOIN actors a ON a.run_id=e.run_id AND a.id=e.actor JOIN actors s ON s.run_id=e.run_id AND s.id=lower(e.sender) WHERE e.kind='message.observed' AND e.actor<>lower(e.sender) AND a.team=s.team ORDER BY e.run_id,e.ms", + 'same_tree':"SELECT a.run_id,a.actor,b.actor AS other,a.ms,a.x,a.z,a.ref FROM events a JOIN events b ON a.run_id=b.run_id AND a.actor=3900: + frames.append({'ms':round(ms),'inventory':[{k:i.get(k) for k in ['slot','id','name','count']} for i in v.get('inventory',[])],'messages':[{k:m.get(k) for k in ['sender','text','tick']} for m in v.get('gameMessages',[])[-4:]]}) + if not baseline: + for m in v.get('gameMessages',[]): + if not m.get('sender'):continue + key=(a,m.get('sender'),m.get('tick'),m.get('text')) + if key in seen:continue + seen.add(key);add_event(a,ms,'message.observed',m,f'{job}/states.jsonl:{n}') + r['events'].sort(key=lambda e:e['ms']);r['samples'].sort(key=lambda s:s['ms']) + r['durationMs']=r['durationMs'] or max([e['ms'] for e in r['events']]+[s['ms'] for s in r['samples']]+[1]) + r['media']={a['id']:{'url':f'http://127.0.0.1:8128/media/{run.name}/{a["id"]}.mp4','offsetMs':next((e['ms'] for e in r['events'] if e['kind']=='media.started' and e['actor']==a['id']),0)} for a in actors if (job/f'{a["id"]}.mp4').exists()} + for actor,media in r['media'].items(): + log=job/f'record-{actor}.log' + m=re.search(r'Duration: N/A, start: ([0-9.]+)',log.read_text(errors='replace')) if log.exists() else None + origin=result.get('cutoff',{}).get('baseline',{}).get('at') + if m and origin: + media['offsetMs']=round(float(m[1])*1000-dt.datetime.fromisoformat(origin).timestamp()*1000) + media['clockBasis']='ffmpeg-first-input-wall-time-minus-engine-baseline' + else:media['clockBasis']='spawn-time-estimate' + runs.append(r) + db.commit();db.close() + metadata={'revision':revision.hexdigest(),'runs':runs,'queries':{k:query(v) for k,v in QUERIES.items()},'querySql':QUERIES} + (HERE/'swarm-data.json').write_text(json.dumps(metadata,separators=(',',':'))) + # Keep raw filesystem references in SQL/data; the pane only needs stable event keys. + def pane_projection(value): + if isinstance(value,dict):return {('timeline' if k=='events' else k):pane_projection(v) for k,v in value.items() if k not in ['ref','source']} + if isinstance(value,list):return [pane_projection(v) for v in value] + return value + for r in metadata['runs']: + for e in r['events']: + ref=e['ref'];file,line=ref.rsplit(':',1) + if file.endswith('events.jsonl'): + scope='episode' if Path(file).parent.name=='episode' else 'run' + e['evidence']=f'http://127.0.0.1:8128/evidence?run={r["id"]}&scope={scope}&line={line}' + pane=pane_projection(metadata) + for r in pane['runs']: + r['timeline']=[{k:v for k,v in e.items() if k not in ['result','target'] and v is not None} for e in r['timeline'] if e['kind'] in ['policy.action','policy.result','action.started','action.completed','message.observed']] + for actor,frames in r['frames'].items(): + compact=[] + for f in frames: + if not compact or f['ms']-compact[-1]['ms']>=14900: + compact.append({'ms':f['ms'],'inventory':[{k:v for k,v in i.items() if k!='id'} for i in f['inventory']],'messages':f['messages'][-2:]}) + r['frames'][actor]=compact + if r.get('cutoff'): + final=r['cutoff']['final'] + for actor,v in final['actors'].items(): + if v:r['samples'].append({'actor':actor,'ms':round(final['elapsedMs']),'xp':r['scores'][actor],'x':v['x'],'z':v['z'],'authority':'engine'}) + bindings=json.loads((HERE/'trace-bindings.json').read_text());bindings['swarmData']=pane + (HERE/'trace-bindings.json').write_text(json.dumps(bindings,separators=(',',':'))) + source=(HERE/'viewer.tsx').read_text() + if len(source.encode())>262144:raise ValueError('Pane exceeds source budget; full evidence remains in SQL') + (HERE/'viewer.built.tsx').write_text(source) + print(json.dumps({'episodes':len(runs),'events':sum(len(r['events']) for r in runs),'revision':metadata['revision'],'queries':{k:len(v['rows']) for k,v in metadata['queries'].items()}})) + +def query(sql): + db=sqlite3.connect(f'file:{DB}?mode=ro',uri=True);db.execute('PRAGMA query_only=ON');db.row_factory=sqlite3.Row + # Hard CPU budget even for user-authored read queries. + steps=0 + def guard(): + nonlocal steps;steps+=1;return 1 if steps>10000 else 0 + db.set_progress_handler(guard,1000) + allowed={sqlite3.SQLITE_SELECT,sqlite3.SQLITE_READ,sqlite3.SQLITE_FUNCTION,sqlite3.SQLITE_RECURSIVE} + db.set_authorizer(lambda op,*args:sqlite3.SQLITE_OK if op in allowed else sqlite3.SQLITE_DENY) + try: + rows=[dict(r) for r in db.execute(sql).fetchmany(1000)] + return {'queryId':hashlib.sha256(sql.encode()).hexdigest()[:12],'sql':sql,'rows':rows,'limit':1000} + finally:db.close() +if __name__=='__main__': + p=argparse.ArgumentParser();p.add_argument('--build',action='store_true');p.add_argument('--sql');p.add_argument('--query',choices=QUERIES);a=p.parse_args() + if a.build:build() + else:print(json.dumps(query(a.sql or QUERIES[a.query or 'all']),indent=2)) diff --git a/examples/runebench/requirements.lock b/examples/runebench/requirements.lock new file mode 100644 index 00000000..3b9f8f24 --- /dev/null +++ b/examples/runebench/requirements.lock @@ -0,0 +1,460 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile examples/runebench/requirements.txt --generate-hashes -o examples/runebench/requirements.lock +annotated-doc==0.0.5 \ + --hash=sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101 \ + --hash=sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb + # via fastapi +annotated-types==0.8.0 \ + --hash=sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7 \ + --hash=sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0 + # via pydantic +anyio==4.15.1 \ + --hash=sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101 \ + --hash=sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94 + # via + # httpx + # starlette +certifi==2026.7.22 \ + --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \ + --hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55 + # via + # httpcore + # httpx + # synth-containers +click==8.5.0 \ + --hash=sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360 \ + --hash=sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34 + # via uvicorn +fastapi==0.141.1 \ + --hash=sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3 \ + --hash=sha256:e8822fc40db1e1858054d7a949a888695bc9bdce70139178e33bd2871a453ca1 + # via synth-containers +h11==0.16.0 \ + --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ + --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 + # via + # httpcore + # uvicorn +httpcore==1.0.9 \ + --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \ + --hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8 + # via httpx +httpx==0.28.1 \ + --hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \ + --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad + # via synth-containers +idna==3.19 \ + --hash=sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15 \ + --hash=sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4 + # via + # anyio + # httpx + # synth-containers +pydantic==2.13.5 \ + --hash=sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73 \ + --hash=sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08 + # via + # fastapi + # synth-containers +pydantic-core==2.46.5 \ + --hash=sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942 \ + --hash=sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821 \ + --hash=sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5 \ + --hash=sha256:0bddb4020d8f04175865ccd17eff3040874fc11fb593f424edb452653b4b947c \ + --hash=sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184 \ + --hash=sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2 \ + --hash=sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc \ + --hash=sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5 \ + --hash=sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0 \ + --hash=sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931 \ + --hash=sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e \ + --hash=sha256:1e449def1945a462c464331254e5a44fca7c3b4f9aedf59ec2f50f8066dd8e25 \ + --hash=sha256:1e5aad1220a1192c42341c8fd4a8686657e73ab2a920c970bdc4de334fe3193d \ + --hash=sha256:200aa3dc9f8d54f0754f43247c0bad0999fdcfbfd2488384dd44f37279271fe6 \ + --hash=sha256:2471fd51c61c610e1dcf7de44d7299283661654d11264ab4802b303368d69c47 \ + --hash=sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038 \ + --hash=sha256:28a6a556cd3b6066bea827857f9d9cce027c96f776e512f544a581f9e42161f8 \ + --hash=sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b \ + --hash=sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a \ + --hash=sha256:2d330aaba8621b1edcec8ae2c4050f63b84ccf6d98723a8f212e9684713abf0e \ + --hash=sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074 \ + --hash=sha256:337639ba62a11acde6ef3aeb08c8ea755f8ef1fe5e513356c0f36a2b0d7568b0 \ + --hash=sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433 \ + --hash=sha256:356c8368cbc321050b169595683a2e1d63413b1e0e2868b330af9fc14c616d3f \ + --hash=sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034 \ + --hash=sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21 \ + --hash=sha256:3a3e26b6a8274211bddee2d0e4d0d42778f17a34510f49d2ec44b58abfc41736 \ + --hash=sha256:3aa166e99c4f2985407fb8714aebede877ecb5455cf321b606adca926d30d5a0 \ + --hash=sha256:3d2652072b2d774947ba5cf78a9e59644ac62ee572daf6dd2e1dfe905e15b2b7 \ + --hash=sha256:40375c2d05acec10323e45dfe2077ac44bc74659008614af5069034e2cfc781c \ + --hash=sha256:413a717a410d0c817ef5b786a059415550b3794e1d0c2abffd9efb93a3d9f7b4 \ + --hash=sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c \ + --hash=sha256:49776eab08766a08dfff7012f8b422dcd7e25e43b316eedf0477c24fcfa84b7c \ + --hash=sha256:4d44cf99ddebf875f9b68cc267aa684c99b7b44fe63ee1cac4ec163807290069 \ + --hash=sha256:4dedce55295becb61921e386b99d4f2706045306e7fa52249a33004c837379fb \ + --hash=sha256:4f8507560a9284e1370bb048ed4282012fbef4e8d109875b95e884d228552061 \ + --hash=sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29 \ + --hash=sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3 \ + --hash=sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa \ + --hash=sha256:53feb344243bb9510a9dec7bf3cf1b64d88a98af5dc7872a5160465f8b198c8e \ + --hash=sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38 \ + --hash=sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f \ + --hash=sha256:5cb482e9e84c851f4e623fe4acc1ced89168cf1fe18f7089db4548c8f5bbb65b \ + --hash=sha256:5e81740c09e310f5aa5cbd3e434a01c154d4bef93241c7877b39f211d2b78ba8 \ + --hash=sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e \ + --hash=sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c \ + --hash=sha256:5f93c5fe914d75fbec9a49209b00da5f08e9e467d69da2b1510c81940cfd10be \ + --hash=sha256:657b40d6240c0a7b6a64b30f22d1e3aa631c7e846c621b0c0f6d1d75e2e15ea6 \ + --hash=sha256:6d30e1a4f138b8951063e9a394752a9179b51da288ffa507b1e659222f4c1793 \ + --hash=sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1 \ + --hash=sha256:701b2e04b560eeb4bddf7a25ab8ca476176e34fdbd9a0e18196f0d12d4685f0b \ + --hash=sha256:771cf63ae0b1b50dd22e5f3e3549fab5f3f4ff1635d352a9e1a97fe01c7b2e64 \ + --hash=sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f \ + --hash=sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761 \ + --hash=sha256:7b0fc826b16c55e561e5d2a0c5c77b051ba1d92808118c4e4b5390f5e0cf191d \ + --hash=sha256:7c6be839a5a8312626b32029a415644a0846b420bc8b52b95b28cd92da162168 \ + --hash=sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869 \ + --hash=sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111 \ + --hash=sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5 \ + --hash=sha256:850a08d167dde16db8702c274f320c7be9d7da6f6dff2b58b18f9e815bd94f5b \ + --hash=sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7 \ + --hash=sha256:892a881d5f68c2b9ea304b7a6c2c60d9343df578a311b0f86b94bc8f1ffe8129 \ + --hash=sha256:895395f8918627b04efb1ad2a4cf605387143300ba03304cd1dfa6d03f5e095e \ + --hash=sha256:8b10e3e8fd7ddc2bd915848a2768e44c15b22936f1cc54c462ad1164deb02655 \ + --hash=sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c \ + --hash=sha256:8feeac04b5794e513e710af2f9c87d49f31a6dc47967bb264a1fed61a8989bec \ + --hash=sha256:9432f3598db432cb51c5b37fdbf29a60fcccc79e30d37a05022776a6bc4ab689 \ + --hash=sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f \ + --hash=sha256:978e7b97d4824b5be09c69fb70507cbde3b0323fc147332ca40a94d9a6a0ebbf \ + --hash=sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea \ + --hash=sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9 \ + --hash=sha256:9b68938dd5b0c783d88ff8e2dcc69451b5eb936fe212d516b21b9d5567f6d464 \ + --hash=sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519 \ + --hash=sha256:9f47b8a949e60f027f0aa0a6f6c7b7e9c55cbf4380d10b344e282fa4e7ab1e1b \ + --hash=sha256:a1dee1b804ff4d11c663636cf15d2ea47e9f79cd56c033fb1cbf08924842a48f \ + --hash=sha256:a2468d93d181667a7abd66e1b64bb9f76f361b0fef8faddf687456453576f5ee \ + --hash=sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a \ + --hash=sha256:a39ac25a9a2fa4072efdb429833c4a4c8009a51ff9eea3eeae131713cd27991e \ + --hash=sha256:a445486499897b88a7d6c310c88ed64dd37b1b59bfd7ae9107490bbb362f47d6 \ + --hash=sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2 \ + --hash=sha256:ab4b66edffb32d9e951efb3814bd104b8367a7501b81b955cacb5726d897389f \ + --hash=sha256:aca6c767f552b21b10f774aeac128e828eafb796adfa1b666a18bf6321453c3a \ + --hash=sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f \ + --hash=sha256:b10ec717381bdbfafef34607824db4c91de69ff085e4fca3b2af91b4fa17e68a \ + --hash=sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47 \ + --hash=sha256:b6acfb46a814762367fb7ba0828b0a17d441b92ce249a0e007474c9072662dda \ + --hash=sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0 \ + --hash=sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4 \ + --hash=sha256:b9fe6fb92520e3fd61f2e49000b6911b188824f089b75973ea06d6267f0b476d \ + --hash=sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3 \ + --hash=sha256:c14ad3bdc85ee7f318742c457ca3968a92126d144b15721c759033bfb06296c2 \ + --hash=sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355 \ + --hash=sha256:c3471e5c4a949c26ec00a77f01df59096aa9495877de76fd60a980f8ee6be461 \ + --hash=sha256:c583b927a8838dab890706a6fa7573fbb8b70e24000ef9f7238e2d6f6435a5ed \ + --hash=sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f \ + --hash=sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5 \ + --hash=sha256:cd5214352ae68f3b5e9af7768bdc5253695ee069675db3480518420b3be881f2 \ + --hash=sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829 \ + --hash=sha256:cdc8b74ecc48c0cb1e9607a05ec4e9e88db60a19ffcc9a1d5f9088ede40c8dc0 \ + --hash=sha256:d0a24b40877af2de4950252be9d21eaf7fb07660f3c2cae1f56c6b599ada5266 \ + --hash=sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575 \ + --hash=sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290 \ + --hash=sha256:d625a186a65201c23a9e3b8ed9c47e90a026e03256608cc91851c6709096844f \ + --hash=sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62 \ + --hash=sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f \ + --hash=sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a \ + --hash=sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7 \ + --hash=sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed \ + --hash=sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f \ + --hash=sha256:eb7d8d0e5886a89a55d2eef490e272fa965a9d57c6b29a5b5088a7997ec2cad1 \ + --hash=sha256:ecb42011e12ee19cafbc312887cbf3546959fe02fbad44f272d4be5baa997615 \ + --hash=sha256:ef3fbbf161dc9351a2fe0422e51b129f9e97e42385bd0320b309c15f7d287dd8 \ + --hash=sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3 \ + --hash=sha256:f077d0b97ab11fa7dcc633fca53515f290bca8a8a633e966d5b6d1879d9ed01a \ + --hash=sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff \ + --hash=sha256:f7b0ec93a2893de856652154d73b7ba622f26fa97726487dcac373de5f4c6084 \ + --hash=sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0 \ + --hash=sha256:fc5d783bd4a2387e97b8a2d5ec781cfb92b3d893bf82370548e99db5915935d3 \ + --hash=sha256:fc8515076c11f3cfdf4fb142dcca0fe384b1230a3b5415458ac84f3e0903ec13 \ + --hash=sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9 + # via pydantic +starlette==1.6.0 \ + --hash=sha256:a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c \ + --hash=sha256:d4e3ac5e546444960c710297a3c9fc3f7ebae1b7e963f3d36173b49da535be9b + # via + # fastapi + # synth-containers +synth-containers==0.4.3 \ + --hash=sha256:d0bab327439a93fc90c77f19c464a14b9f9cefb589ae76ba56bd66b07ea4efa7 \ + --hash=sha256:eaff16ec40b6e2c9a569751f415912178f3aa0ac63396749466ec92e1d734bf5 + # via -r examples/runebench/requirements.txt +typing-extensions==4.16.0 \ + --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \ + --hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5 + # via + # anyio + # fastapi + # pydantic + # pydantic-core + # typing-inspection +typing-inspection==0.4.4 \ + --hash=sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47 \ + --hash=sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147 + # via + # fastapi + # pydantic +uvicorn==0.52.4 \ + --hash=sha256:73acfee47a0b133c5de13d219492d62d8a31e935f4fe6e41a232451a15379f86 \ + --hash=sha256:f86e41a149d7d05a9969337e3946a9c171c06a5d42680896daaba624aeac8da1 + # via synth-containers +websockets==17.1 \ + --hash=sha256:0014eaff8ad5b3b43feda2279f9d34bf2eaae040720b9fbbb55944b10f40b14d \ + --hash=sha256:00679b7468b4c2b12b0757118174e8eabac56bb2f579a928a104d9554a56e098 \ + --hash=sha256:00bf34b64501e3477e81fc281532ff3cbf4da26633c10b63979d5085d46602d3 \ + --hash=sha256:01dcb47deebc40b38fd4a493b9b9f4d0a704b7bec6f35e4d34085b329abce71a \ + --hash=sha256:020e271205f8ab3406d7a59cd00de6dec722315924411c421bd00642f18bad86 \ + --hash=sha256:0340bbef6bfbe16da888b3983d666a4db4954ac3253c38f13bc7aba0c7db5a2f \ + --hash=sha256:054c28db2dcec0e857e3b705d8c28012613e555b38c765d6a4f75340a4fc06a0 \ + --hash=sha256:073c5c3f7e127041fa9d34a9e29ceefee8c3cafbd267ed2927318f425144380d \ + --hash=sha256:07fa3e7c30e2c577928d359b56bf872a3e0cbcc15553eaa0907c1ee86344b56f \ + --hash=sha256:0c84bdef916556cbe1d5a43b423398be4dd3cba6522b463e53d848578b920695 \ + --hash=sha256:0c863507ada5805517ca6dff1c524dcd42942efe6304dacf06700878398d21a6 \ + --hash=sha256:0c9982938980e086da59f70d05f9418cd143401a601a0faac10fa48f7bb1cd3e \ + --hash=sha256:0de501b7f2db11e83739ac20e2d33d46da4604b829f506c24be80e7def069391 \ + --hash=sha256:0f4f50dfe2cc810fc4e2de979b35e83bf8bb4bccdc6fe472d93762ea7b1d5927 \ + --hash=sha256:1036189bd34b0bc1b10a4679321e2c7968af317efe6e8e4c1c5141c4254fb5bb \ + --hash=sha256:10ecb38ffc05e1841b619d99c725307a223ef9ad58e7b1ed33311d472dc43918 \ + --hash=sha256:12c8e2b25df59755954a04dfa09c990b96691025aaf7eafd19ed6da24b09c18d \ + --hash=sha256:12fe8984a32dbfd084e0603f1a8d740c0180cb85b3174585c54a80d2455a8394 \ + --hash=sha256:17aa424ab61620aad21b36b2240efc87b500cc496e7d0e999a5c2ae99395e886 \ + --hash=sha256:1a20136d61f9ca3a31493732762661fafc2c20e8861930214e21afc6a8a692a2 \ + --hash=sha256:1a5cf4e7bbe3ca499e6a289206cb4fcb7444b09919e129bd517f57d5fa192c13 \ + --hash=sha256:1c7ac77401227212dc6e849182feee50d57cf456ec6329ffd6979c94bb136c5c \ + --hash=sha256:1fce0f43e0d41422e0b2cad6561e1970df22f212f4c7e884967df7cf591b031c \ + --hash=sha256:259f45358c76d3b18489e3e80636cdbe807e05ecf1b10fdf1a779106d23d0c8e \ + --hash=sha256:2786cbd273ab69c22612db8a41229ddf2c158060b17b5928884bf388d07887f3 \ + --hash=sha256:29176d8b429cfa0fa443c473878d37a5c06cfd0cb36b71ba4314accc71e05906 \ + --hash=sha256:2a0162a6372110a5601cb5c9fd826635cedf69f3e110c545dd19774e040b970e \ + --hash=sha256:2a4dc6ef83f4559e0d05f313a375cb38f63c986096a9da99fe94fdd779d313e5 \ + --hash=sha256:2afb58c7ba48b329d56769f8dfd89f394efe587b65ef806bae810a484d6d3608 \ + --hash=sha256:2cce251f3e2469b99b6802b55435bcdd07123b41870f54c87b336183af9d7e68 \ + --hash=sha256:2d72879819f5145a342d0030c418702496c65a4b913ef81f5ae944dd91dd50f6 \ + --hash=sha256:32a2a68d989d6e5b74a9d5095415c51189ebae29fceb7cf2b64a1c0318a81256 \ + --hash=sha256:33e45c7ea38428e740a7f233555d71df0b875cef7fc080acebc9654475e35335 \ + --hash=sha256:34879e19bb0a3c44f9317679435aea5327fac993933a704cbf353bf1234b10c7 \ + --hash=sha256:3546ef55b3a074494106508bc6505c73825970d2d9505f7bf53882b3e88b0d1e \ + --hash=sha256:3709a1ab30b4b922027d22f68d2b61a0656a91680ac894a537624e6be7dd7f7c \ + --hash=sha256:4031152769179ab8dcdeafc7b0e58052a49117560a28671700b47b2c7b717aad \ + --hash=sha256:406b8107943a43ef4649b1e0cb0cdc052bbf08fe1c8905a623c4af9586e5cebb \ + --hash=sha256:416b4bc8789a1865a3ff643ec4ee073a5f52402d0dbeafd27b1798d5dd6b6a51 \ + --hash=sha256:43bd0c1ceb924d67f5c1a5254d8361dd9d94246e6331a726064dfa2917880780 \ + --hash=sha256:455ffeea0879d313205df1e745e5883e1feb7f31ecd26be882f5f0babd3db04f \ + --hash=sha256:469355ab1af100b9380f1afb09985019f4a4b94fa1dd0e9396db4361626d7ab8 \ + --hash=sha256:46c0331c9eaaf73a559f3a9e388466be0df96eb83d40f06f1ca6ab6613b35c82 \ + --hash=sha256:46e7a10bf04318c7b0c0273791925ae5e1cbe4a11e34aa934d2ef27862058a80 \ + --hash=sha256:47a62d6045c6eaa0d8f97bc2fb68b8cf90077a0cbfd4e83d6f2d2145611ee134 \ + --hash=sha256:4af784f3e436f65b355c117c6497320f2b5cf6a559295cb1c4c7338e335d45cc \ + --hash=sha256:4e8ffcb486c8490a34a4cef5e4409d8da5a1cb1681e5bf7d786ce5e84aa8540d \ + --hash=sha256:50903d335bfda026c2fa11dd9aed09d8cbee0c451e3a85122a9acb041b7dc69b \ + --hash=sha256:57b39dc8541cf7ed3f639da82bf7451060483967f9e733da1f8173e4095f0642 \ + --hash=sha256:581fa678ef46f4277cc8491312468e582f8ad609dbab907ba6096a08c6a0ff98 \ + --hash=sha256:5a74531ce81af587f906ab42f194032388fcff8fc7938402e5917c9147a39441 \ + --hash=sha256:5aefe78e6a3077fe22b5e64b04666a85a3eb8b934d40e8595a693adcbceb6f11 \ + --hash=sha256:5c27336b1a0ac56569493e858497870347854372395f50483725f8cdacc5a45c \ + --hash=sha256:5d4724255fb8398acd9e583b97eb2279cec20e0bd0f9a94bf75f6056ef9f13da \ + --hash=sha256:5f051f8030a51815dc00e24bd2e5f1435af095c1cc111d747ac6e2a3620d7641 \ + --hash=sha256:615746b12b26a3fd4077bc6fbeb277a1c192a45dd57b531d07ad9ed5c52a9a7a \ + --hash=sha256:617243e19a0992095956f406ee9cd3bc4ba92862d83cb1d83bb59ce574412bec \ + --hash=sha256:655a8e28010f09fd6fa317e857afab3af7647f33e41dee88fa421e92086d1090 \ + --hash=sha256:65be6bda2b537fefa4b3a5ccd6ab386533ce39dd8fe62433ec90901fdc81752d \ + --hash=sha256:67258b00302a5aaf0b267771c7014b13429abd7ea17eebc4c55bd935ff101555 \ + --hash=sha256:677014a073bcb1fbaa7e21144786864f16c08f856d66834f611eceb9006cbab8 \ + --hash=sha256:6a8e768a048c2220697477ce2e67e4345dc9f693d0ee6af53945b5e30227c6a7 \ + --hash=sha256:6da17fc94bd270f5987b10bee113461ac36a36a98b0481ddcc98056e5a90001a \ + --hash=sha256:6de9acef07e3a78e9567fcd26c29011a4da8f050b13004bbf880a0fd82a6eea5 \ + --hash=sha256:6e63c01803be425ff062b7f7fc201a74def1d49fc94a2410dd17375df75936e9 \ + --hash=sha256:6f42912fa9eb4cb7c7ec9fde9b3332ba339eb8a8811981043d4029599f3d950b \ + --hash=sha256:722ec21717eec6477bce582147a28acdfe034e604239466a6a95daedb863e774 \ + --hash=sha256:764cf7bfa149365f32b7a0fd9fed32debdac29dd06295d5635cde1745b446cd8 \ + --hash=sha256:76dd004f59115087c7b700474cb18f01325e37250032e19396c08ae41448e4b3 \ + --hash=sha256:77b37cceca17291897c3c73bd30a7c7c7909593554b5da574ec852af83c1742a \ + --hash=sha256:7a72efa3bf4fa3a6669a54420a472ad056da3973d827f10e3a536da463f926c2 \ + --hash=sha256:7e724f843fa6a0614aece65a7c73e51d0f4412ca41dccac13c3caf98e69536bb \ + --hash=sha256:8196d217eeca52b9235ee1f8a684a09885a5f953d5a31e80ef915bf2c5c94f9d \ + --hash=sha256:829dba1bc049779de9b332088c1a6a9858e96bd67e50b6b644a95e02b67836bc \ + --hash=sha256:84f25e099fdfe3b09f953d84698f729a1f7d1e99101b2787d7a28ed77b323750 \ + --hash=sha256:87f0d5e77548b0c40c8464cdb6108792e7e53f487c6400028a4ec28a8afbe5ab \ + --hash=sha256:880069d21cc33a558dcf180924a546d1ecf8ada5be3e4e70acee87019d706a24 \ + --hash=sha256:882af300d2c6a092b93767d5de03c7bb56dfb06314140c8e872d3f48e09f7b74 \ + --hash=sha256:88381602e379165b66244b2ebc29f9b23ea0851fbe63ae157f91ca324f072d6f \ + --hash=sha256:88b882764ef65147a7a5ae13168dedbe225a04e2ff4858fe543f2c402f093e9c \ + --hash=sha256:88bc5138e53903a85c354e59df7ba73ce306f7b09724cef74dba121e60a88ce2 \ + --hash=sha256:8d1b108bd8f5f6a8b90801f6db3b3858d5deca889acfdb8ac497bbb24e4b0edf \ + --hash=sha256:8f6c38cdcaf98a911d7acc25577f2f9e710f3a2fc2bde1563556784320196b51 \ + --hash=sha256:8fbf28e639544503b7d1c96452a5e5e043e4108d89b1f3fa02910603622d19db \ + --hash=sha256:96abdecbaae746851b87c3a36cb4a661df93ca3d92f114270f79228bf1d00de6 \ + --hash=sha256:98a5b2589a56a4b4f098b0a958099a4356ab904a7844f1da3841efca469af7e9 \ + --hash=sha256:9ae55d24241fc055f22aea3ac924559069848bd0ad4ea065fdd72d2194685fe8 \ + --hash=sha256:9f4a08ff7cb68c27b18e09223cc6304e01d0f82d5a240d251266dfd2e6e44729 \ + --hash=sha256:9f4c0377a83e163a303514fdfab501dbe379bdc13e5b9312a91d112658b29dce \ + --hash=sha256:9fa1ffa08c81a4f809cdab6129f8e55bee4650b9d6d3461019dda73aacd146b6 \ + --hash=sha256:a06f3b5085176763182449559e20391d7ce616a8972a9f7a33deda87ea6d4f3c \ + --hash=sha256:a62d8c424383c9dc769ff3672018df822603117e32686e567d452ed035b6fb2e \ + --hash=sha256:a7b35181a14cbfcae163b4de545d22abfd07d06c2c41ca69cfcd99251d6888ab \ + --hash=sha256:a9fe648abd1d9b89aebfa30407bfdd08a0271ec5dc7d44a4c6ccd1ce22cf562a \ + --hash=sha256:acfea4c20bf54384883ea33b1240fc1db4f52e190823a4e2b334bc3e8bfca96a \ + --hash=sha256:ad68c28a27246fed109a4409393d677b7e1388345cbbd2f5aee5c182d8506110 \ + --hash=sha256:aec00f018d34c67500ff0438dc314b40277be4a1b983cbacbf53ccf7db63e257 \ + --hash=sha256:b1bc819c6db90e8f91a38250a1ab4c058261871aa52d2fe36382eddedf146dee \ + --hash=sha256:b1c323fc3be1dc3f87f6c59458cb7d9e13dcbbf971d6c3f3e2bbaf58d3bfcdfe \ + --hash=sha256:b3b451fd2723ad3191a209afe6f3f4bc86c83e9a85bdc255353b91803ee6aa66 \ + --hash=sha256:b65d5fe48219dc2d5e158de9e6514e75600f379cc7e37108d35f31764c155566 \ + --hash=sha256:bb0efe019480a1c93e168ce96479273aaebd672fc8c350d5eed1e507ababb1b8 \ + --hash=sha256:be3f0129c5654517b2abf07dcb75bb1d9479759a4ccfb569e8293579e9fc029a \ + --hash=sha256:c3241d684a76eaaef8b2dc789afde4343cd3aad55ea81e4e8ab3605b529bae51 \ + --hash=sha256:c65da239a5ad553619804c1f9d65c1a0b3005381c6158ee14da2c7444cbd0c78 \ + --hash=sha256:c89406fa3dcd4aa8662c6406cc5c0de1790e9614d2c3aaf03ca53a8a8ccf3405 \ + --hash=sha256:ccbf3f4a9890d50b3a08ee04029fde30a03bfdeffaa19977628bf17251764e60 \ + --hash=sha256:ce0305b702b20d1e1d60a9aaace6bc89970e1753565543f310d549eab22c2435 \ + --hash=sha256:ce13c7d233239e739600a57d4a73c1192ad8259e655a4d55aa1a454242bc809d \ + --hash=sha256:cec1bb8f22abccc8d20f8ca63df9be41600c26c190f4b97ee86c675fd4a863a6 \ + --hash=sha256:d1e2f5fa2b6d01f0d85b4f223fea7ed1d504be282a02a81bd2be4817ef7a2f03 \ + --hash=sha256:d334d11398086bb5559606cb42d51c013ea7c061c7db701521392373d3c087f5 \ + --hash=sha256:d411ea5ca18ac1b12c0c94be88b60c18ca641ac43bcdfdf1c9f79d46cdbe1603 \ + --hash=sha256:d41ef69d5416fbc1d98cf96c37be6192d10fd101c3e0f8b3ddc36e09432b3c08 \ + --hash=sha256:d58159af7835fde09c462394293c0d7aaf8fb4557d8f8e5699f5e722ccae013d \ + --hash=sha256:d7b349265fad6244013eecd99df8d83c12bf3013943e431f4fadd5bffc37db42 \ + --hash=sha256:d8e83333385cac6030a5167fd18bf96cc6c58b914c308e683f05b0cf94bc8dd0 \ + --hash=sha256:d9d01e8ede41fea4f5a847dad9d628355f74905f437a5b6856d67aa66d193800 \ + --hash=sha256:d9fc873e239c5abeb150bc24dbd1a7af23a9254526383ce0a077f5e20adbeb19 \ + --hash=sha256:db9d7ee47f3ba531e278be539af39e2c7c7d28fb94897b6cd1120d63b0ef5922 \ + --hash=sha256:dc053f9e95a76213c5eb7ed95779f7daf0d2bf0e4e03073629ebfa43a033f151 \ + --hash=sha256:dc2b79afc074d2f3e64b26539350f697fe1b85ea1c49ea24eb588f247b053ce1 \ + --hash=sha256:dc5789e5ea182b77a38881383ada5347202a6c66f4857d054e075290e80b604b \ + --hash=sha256:dcb8d5f7edef7a399d322cf28d4c4e6f98dab64d301c8f50581a1080e5198142 \ + --hash=sha256:e4bd7eacb87d8cf3ed70d6392c770a0d92441f05d7d2a3efafb5bc171d5e3067 \ + --hash=sha256:e552e0037230ac16e5f568de7012041344d1b18c9feed30ec2891b8eba55af81 \ + --hash=sha256:e5f5c7a893507d0e83a80b88aefd6522f7e882cd53f9722c6f23f5a020c9557c \ + --hash=sha256:e74e41f0ad12ff1e8983e349daef79d37cc8280c743ce9d134d6c74c18dab5d6 \ + --hash=sha256:e78fd4b7b2c5086a38671c9c882c1e643385eccea360b5b1fda4a105e590087e \ + --hash=sha256:e8dc3fa6d6b7ead3f9de57895f41b116a28787548e066365d9d90f7356bcaad2 \ + --hash=sha256:ea0ed9373b880115911d9d39634bccc95b8ce590c9c42e8589f5cacc3ef3cee2 \ + --hash=sha256:edadce7a22052056fd4384543019856b34850363c9d387929f677ae01d79709c \ + --hash=sha256:eec113a5b41d124ef42ff56b0d74a6da3fd986400038eab9e58ee42a4024e837 \ + --hash=sha256:f221081107b8c48184d99f7019604486376e7ef826037e70aad6b02540732c23 \ + --hash=sha256:f3a1d577e081667dda7f8e5b4796e6e32f9713c93e2a3d930669519840a3c623 \ + --hash=sha256:f47aafd92aa28b941180e6da8a42b0f711851b14b81a5b6bb28dbbb1fa35152c \ + --hash=sha256:f4c45ee2512d3757b5e6c67c5a34e435143f2ecb7df3324f9fd888688c45c0f4 \ + --hash=sha256:f58f58b4b29bbea2a3635e2c56eff4a3adab011fe383802a9e542e31b97085fc \ + --hash=sha256:f612dc57f00c07cf4aa2673f7cbceabd654ad2457b7e639f061b794d6e11f9fd \ + --hash=sha256:f62114a54117e4948a1e414e89521f7fe1e3c2f83f2a571a06a4fc6718b0900a \ + --hash=sha256:f64e001bb7fa89b9f32cfa600bf8e9ac8ca26759d9b92ae01453ee303d9cd7b4 \ + --hash=sha256:f7233eaf441a345a5943a929fd4b5ea3278f11aed35a9ed0f3106b8cb3ca846a \ + --hash=sha256:f78a3ffb1994304db2c0c4588e4d1a518079b557054fa3bb985a6f5e50ff49a3 \ + --hash=sha256:f8e822efd54137d8cc8310eb64635ab827a4a6c72ff08691f38aa624776d8ecb \ + --hash=sha256:f98bf378d7a5be047a044a1a27c987a8f355e10e3b5754617dbe756248cbc5ce \ + --hash=sha256:fb88076df585b69c5761c387c0081aa87d7b9eb1b205a6535ca4777e25650d81 \ + --hash=sha256:fd8f47dbf2e8adb15c847215f83436de3fdb120b51fdae0fbbdf69fd97a3ad80 \ + --hash=sha256:ff3e2ba7a9f0a110b0555452e9b5a03a34e11662544e01beea15f144b48ba7b7 + # via synth-containers +zstandard==0.25.0 \ + --hash=sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64 \ + --hash=sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a \ + --hash=sha256:05353cef599a7b0b98baca9b068dd36810c3ef0f42bf282583f438caf6ddcee3 \ + --hash=sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f \ + --hash=sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6 \ + --hash=sha256:07b527a69c1e1c8b5ab1ab14e2afe0675614a09182213f21a0717b62027b5936 \ + --hash=sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431 \ + --hash=sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250 \ + --hash=sha256:106281ae350e494f4ac8a80470e66d1fe27e497052c8d9c3b95dc4cf1ade81aa \ + --hash=sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f \ + --hash=sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851 \ + --hash=sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3 \ + --hash=sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9 \ + --hash=sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6 \ + --hash=sha256:19796b39075201d51d5f5f790bf849221e58b48a39a5fc74837675d8bafc7362 \ + --hash=sha256:1cd5da4d8e8ee0e88be976c294db744773459d51bb32f707a0f166e5ad5c8649 \ + --hash=sha256:1f3689581a72eaba9131b1d9bdbfe520ccd169999219b41000ede2fca5c1bfdb \ + --hash=sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5 \ + --hash=sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439 \ + --hash=sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137 \ + --hash=sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa \ + --hash=sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd \ + --hash=sha256:25f8f3cd45087d089aef5ba3848cd9efe3ad41163d3400862fb42f81a3a46701 \ + --hash=sha256:2b6bd67528ee8b5c5f10255735abc21aa106931f0dbaf297c7be0c886353c3d0 \ + --hash=sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043 \ + --hash=sha256:3756b3e9da9b83da1796f8809dd57cb024f838b9eeafde28f3cb472012797ac1 \ + --hash=sha256:37daddd452c0ffb65da00620afb8e17abd4adaae6ce6310702841760c2c26860 \ + --hash=sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611 \ + --hash=sha256:3b870ce5a02d4b22286cf4944c628e0f0881b11b3f14667c1d62185a99e04f53 \ + --hash=sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b \ + --hash=sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088 \ + --hash=sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e \ + --hash=sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa \ + --hash=sha256:4b14abacf83dfb5c25eb4e4a79520de9e7e205f72c9ee7702f91233ae57d33a2 \ + --hash=sha256:4b6d83057e713ff235a12e73916b6d356e3084fd3d14ced499d84240f3eecee0 \ + --hash=sha256:4d441506e9b372386a5271c64125f72d5df6d2a8e8a2a45a0ae09b03cb781ef7 \ + --hash=sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf \ + --hash=sha256:51526324f1b23229001eb3735bc8c94f9c578b1bd9e867a0a646a3b17109f388 \ + --hash=sha256:53e08b2445a6bc241261fea89d065536f00a581f02535f8122eba42db9375530 \ + --hash=sha256:53f94448fe5b10ee75d246497168e5825135d54325458c4bfffbaafabcc0a577 \ + --hash=sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902 \ + --hash=sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc \ + --hash=sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98 \ + --hash=sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a \ + --hash=sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097 \ + --hash=sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea \ + --hash=sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09 \ + --hash=sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb \ + --hash=sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7 \ + --hash=sha256:75ffc32a569fb049499e63ce68c743155477610532da1eb38e7f24bf7cd29e74 \ + --hash=sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b \ + --hash=sha256:78228d8a6a1c177a96b94f7e2e8d012c55f9c760761980da16ae7546a15a8e9b \ + --hash=sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b \ + --hash=sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91 \ + --hash=sha256:81dad8d145d8fd981b2962b686b2241d3a1ea07733e76a2f15435dfb7fb60150 \ + --hash=sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049 \ + --hash=sha256:89c4b48479a43f820b749df49cd7ba2dbc2b1b78560ecb5ab52985574fd40b27 \ + --hash=sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a \ + --hash=sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00 \ + --hash=sha256:9174f4ed06f790a6869b41cba05b43eeb9a35f8993c4422ab853b705e8112bbd \ + --hash=sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072 \ + --hash=sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c \ + --hash=sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c \ + --hash=sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065 \ + --hash=sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512 \ + --hash=sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1 \ + --hash=sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f \ + --hash=sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2 \ + --hash=sha256:a51ff14f8017338e2f2e5dab738ce1ec3b5a851f23b18c1ae1359b1eecbee6df \ + --hash=sha256:a5a419712cf88862a45a23def0ae063686db3d324cec7edbe40509d1a79a0aab \ + --hash=sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7 \ + --hash=sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b \ + --hash=sha256:ab85470ab54c2cb96e176f40342d9ed41e58ca5733be6a893b730e7af9c40550 \ + --hash=sha256:b9af1fe743828123e12b41dd8091eca1074d0c1569cc42e6e1eee98027f2bbd0 \ + --hash=sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea \ + --hash=sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277 \ + --hash=sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2 \ + --hash=sha256:c2ba942c94e0691467ab901fc51b6f2085ff48f2eea77b1a48240f011e8247c7 \ + --hash=sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778 \ + --hash=sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859 \ + --hash=sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d \ + --hash=sha256:d8c56bb4e6c795fc77d74d8e8b80846e1fb8292fc0b5060cd8131d522974b751 \ + --hash=sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12 \ + --hash=sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2 \ + --hash=sha256:e05ab82ea7753354bb054b92e2f288afb750e6b439ff6ca78af52939ebbc476d \ + --hash=sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0 \ + --hash=sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3 \ + --hash=sha256:e59fdc271772f6686e01e1b3b74537259800f57e24280be3f29c8a0deb1904dd \ + --hash=sha256:e7360eae90809efd19b886e59a09dad07da4ca9ba096752e61a2e03c8aca188e \ + --hash=sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f \ + --hash=sha256:ea9d54cc3d8064260114a0bbf3479fc4a98b21dffc89b3459edd506b69262f6e \ + --hash=sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94 \ + --hash=sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708 \ + --hash=sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313 \ + --hash=sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4 \ + --hash=sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c \ + --hash=sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344 \ + --hash=sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551 \ + --hash=sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01 + # via synth-containers diff --git a/examples/runebench/requirements.txt b/examples/runebench/requirements.txt new file mode 100644 index 00000000..677bfb3d --- /dev/null +++ b/examples/runebench/requirements.txt @@ -0,0 +1 @@ +synth-containers==0.4.3 diff --git a/examples/runebench/run.py b/examples/runebench/run.py new file mode 100644 index 00000000..c926ce0d --- /dev/null +++ b/examples/runebench/run.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +"""Bounded, concurrent MA runner. Policies receive only typed actor-local tools.""" +import argparse, concurrent.futures, datetime as dt, fcntl, hashlib, json, os, socket, subprocess, threading, time, urllib.request, urllib.error +from pathlib import Path +HERE=Path(__file__).resolve().parent +OUT=HERE/'results' +BASE='http://127.0.0.1:8126' +MODEL='openai/gpt-5.6-luna' +LOCK=threading.Lock() + +def write(p,data): + p.write_text(json.dumps(data,indent=2)) +def call(path,body=None,token=None,timeout=45): + headers={'Content-Type':'application/json'} + if token:headers['Authorization']='Bearer '+token + r=urllib.request.Request(BASE+path,data=json.dumps(body).encode() if body is not None else None,headers=headers) + with urllib.request.urlopen(r,timeout=timeout) as f:return json.load(f) +def compose(*args,env=None): + subprocess.run(['docker','compose','-p','workshop-rune-'+hashlib.sha256(str(HERE).encode()).hexdigest()[:12],'-f',str(HERE/'compose.yaml'),*args],env=env,check=True,stdout=subprocess.DEVNULL) +def utc():return dt.datetime.now(dt.timezone.utc).isoformat() +def credential(): + # Project-local env only. No Keychain, shell expansion, or credential copying into Docker. + for line in CREDENTIAL_FILE.read_text().splitlines(): + if line.strip().startswith('OPENROUTER_API_KEY='):return line.split('=',1)[1].strip().strip('"').strip("'") + raise RuntimeError('Project-local OPENROUTER_API_KEY unavailable') +def reserve(batch, model): + p=HERE/f'budget-{batch}.json' + with p.open('a+') as f: + fcntl.flock(f,fcntl.LOCK_EX);f.seek(0);s=f.read();b=json.loads(s) if s else {'maxUsd':5,'maxCalls':128,'reservedUsd':0,'calls':0,'actualUsd':0,'unknownCostCalls':0} + # Bounded 20k-byte input and 8192 output; include cache-write pricing in reservations. + amount=.20 if model.endswith('terra') else .02 + if b.get('unknownCostCalls',0) or b['calls']>=b['maxCalls'] or round(b['reservedUsd']+amount,8)>b['maxUsd']:raise RuntimeError('Aggregate budget exhausted') + b['calls']+=1;b['reservedUsd']=round(b['reservedUsd']+amount,8);f.seek(0);f.truncate();json.dump(b,f);f.flush() + return p + +def settle(p,usage): + with p.open('r+') as f: + fcntl.flock(f,fcntl.LOCK_EX);b=json.load(f) + if usage.get('cost') is None:b['unknownCostCalls']+=1 + else:b['actualUsd']+=float(usage['cost']) + f.seek(0);f.truncate();json.dump(b,f);f.flush() + +def main(): + ap=argparse.ArgumentParser();ap.add_argument('--provider',choices=['openrouter']);ap.add_argument('--env-file',type=Path);ap.add_argument('--scripted',action='store_true');ap.add_argument('--duration',type=int,default=150);ap.add_argument('--calls',type=int,default=10);ap.add_argument('--batch',default='demo-20260907');ap.add_argument('--effort',choices=['low','medium','high'],default='low');ap.add_argument('--model',choices=['luna','terra'],default='luna');args=ap.parse_args();args.scripted = args.scripted or args.provider is None + global CREDENTIAL_FILE + CREDENTIAL_FILE=args.env_file + if not args.scripted and CREDENTIAL_FILE is None:ap.error('--provider openrouter requires an explicit --env-file; never paste keys into chat') + model='openai/gpt-5.6-'+args.model + if not 10<=args.duration<=600 or not 1<=args.calls<=16:raise ValueError('Invalid duration or call cap') + if not args.batch.replace('-','').isalnum():raise ValueError('Invalid batch') + with socket.socket() as port_check: + port_check.bind(('127.0.0.1',8126)) + stamp=dt.datetime.now(dt.timezone.utc).strftime('%Y%m%dT%H%M%SZ')+('-scripted' if args.scripted else '-'+args.model+'-'+args.effort) + run=OUT/stamp;job=run/'episode';job.mkdir(parents=True,exist_ok=False) + scenario=json.loads((HERE/'scenario.json').read_text());scenario['duration_seconds']=args.duration + kind='scripted' if args.scripted else 'provider-backed' + scenario['evidence_kind']=kind + manifest={'schema_version':'runebench.ma.manifest.v1','config':{'id':'runebench-ma','scenario':scenario,'evidence_kind':kind,'model':None if args.scripted else model,'reasoning_effort':args.effort,'max_completion_tokens':8192,'max_calls_per_actor':args.calls,'batch':args.batch},'head_commit':subprocess.check_output(['git','rev-parse','HEAD'],cwd=HERE,text=True).strip(),'tree_clean':False,'source_sha256':{p.name:hashlib.sha256(p.read_bytes()).hexdigest() for p in HERE.iterdir() if p.suffix in ['.ts','.py','.json','.yaml'] and not p.name.startswith('budget-')},'evals_authority':{'head_commit':subprocess.check_output(['git','rev-parse','HEAD'],cwd=HERE,text=True).strip(),'working_tree_tracked_clean':False},'created_at':utc(),'randomization':'Fixed starting saves; engine RNG is not seeded. Repeats are not paired-seed causal trials.'} + write(run/'matrix_run_manifest.json',manifest) + env=dict(os.environ,MA_ARTIFACTS=str(job),MA_RUN_ID=stamp,MA_EVIDENCE_KIND=kind) + container_name='workshop-runebench-'+stamp + seq=0;armed_at=None + def event(kind,actor,payload): + nonlocal seq + with LOCK: + seq+=1 + record={'schema_version':'evals.event-stream.v1','kind':'trace.raw','source':'local','occurred_at':utc(),'run_id':stamp,'sequence':str(seq),'actor_id':actor,'payload':{'kind':kind,'elapsedMs':(time.monotonic()-armed_at)*1000 if armed_at else 0,**payload}} + with (run/'events.jsonl').open('a') as f:f.write(json.dumps(record)+'\n') + key=None if args.scripted else credential() + usages=[];errors=[];answered=set() + def policy(actor,token): + name=actor['id'];history=[] + prompt=f'''You control {name}, {actor['role']} on team {actor['team']} in a local RuneScape benchmark. Your team competes to gain the most total Woodcutting XP before a hard wall deadline. All players start with level 10 Woodcutting and bronze axes. XP multiplier 1, server tick 300ms. Both lead and scout can chop: lead prioritize throughput; scout locate free trees, report useful locations to your teammate, then chop too. Communicate briefly in public chat when useful, but do not spend the whole race chatting. Only your observation is supplied; other players' private state is unavailable. Nearby normal Trees are legal; do not target oaks. Crowded trees may be felled by others. If inventory is full drop logs. A chop can continue automatically until a tree is depleted. Avoid needless interruptions. Return exactly one JSON object with type (say/chop/walk/drop/wait), optional text (max 80 chars), x,z (observed tree coordinates for chop or within 32 tiles for walk), slot (logs only for drop), and reason (one concise sentence). No code. Tools: chop attempts one normal tree and waits for a log; walk navigates; wait waits 2 game ticks; say sends public chat; drop discards one inventory slot of logs. Prefer a useful action now. Teammate roster: {json.dumps([a for a in scenario['actors'] if a['team']==actor['team']])}.''' + for turn in range(args.calls): + try: + obs=call(f'/actors/{name}/observation',token=token) + if obs['phase']!='running' or obs['remainingMs']<3000:break + # Bound request size using a compact projection, retaining actor-local provenance. + compact={**obs,'nearbyLocs':obs.get('nearbyLocs',[])[:24],'gameMessages':obs.get('gameMessages',[])[-10:]} + decision_id=f'{name}-{turn}' + if args.scripted: + if turn==0:action={'type':'say','text':f'{actor["team"]} {actor["role"]} chopping nearby trees','reason':'Scripted communication probe'} + else:action={'type':'chop','reason':'Scripted lifecycle probe'} + else: + messages=[{'role':'system','content':prompt},*history[-4:],{'role':'user','content':json.dumps(compact,separators=(',',':'))}] + body={'model':model,'messages':messages,'max_completion_tokens':8192,'reasoning':{'effort':args.effort},'response_format':{'type':'json_object'},'provider':{'max_price':{'prompt':2 if args.model=='terra' else .2,'completion':12 if args.model=='terra' else 1.2},'allow_fallbacks':False}} + data=json.dumps(body).encode() + if len(data)>20000:raise RuntimeError('Request exceeds 20k-byte input bound') + bp=reserve(args.batch,model) + event('model.requested',name,{'decisionId':decision_id,'observation':compact,'inputBytes':len(data),'model':model,'reasoningEffort':args.effort,'messages':messages}) + req=urllib.request.Request('https://openrouter.ai/api/v1/chat/completions',data=data,headers={'Authorization':'Bearer '+key,'Content-Type':'application/json'}) + response=None + try: + with urllib.request.urlopen(req,timeout=min(65,max(1,obs['remainingMs']/1000))) as f:response=json.load(f) + usage=response.get('usage',{});settle(bp,usage) + except Exception: + settle(bp,{}) + raise + with LOCK:usages.append(usage);answered.add(name) + message=response['choices'][0]['message'];content=message.get('content') or '' + event('model.completed',name,{'decisionId':decision_id,'responseId':response.get('id'),'model':response.get('model'),'content':content,'reasoningEffort':args.effort,'reasoning':message.get('reasoning'),'reasoning_details':message.get('reasoning_details'),'usage':usage,'finishReason':response['choices'][0].get('finish_reason')}) + action=json.loads(content) + history.extend([{'role':'user','content':f'Turn {turn} observation received.'},{'role':'assistant','content':content}]) + action['decisionId']=decision_id + event('policy.action',name,{'decisionId':decision_id,'action':action,'evidence_kind':kind}) + result=call(f'/actors/{name}/action',action,token,timeout=40) + if args.scripted: + with LOCK:answered.add(name) + event('policy.result',name,{'decisionId':decision_id,'result':result}) + history.append({'role':'user','content':'Action result: '+json.dumps(result)[:1500]}) + time.sleep(.3) + except urllib.error.HTTPError as e: + detail=e.read(1200).decode(errors='replace') + if key:detail=detail.replace(key,'[redacted]') + event('policy.error',name,{'turn':turn,'status':e.code,'detail':detail}) + if e.code==409:break + errors.append({'actor':name,'turn':turn,'error':detail});break + except Exception as e: + message=str(e).replace(key,'[redacted]') if key else str(e) + event('policy.error',name,{'turn':turn,'error':message});errors.append({'actor':name,'turn':turn,'error':message});break + event('policy.finished',name,{}) + started=time.monotonic();result=None + try: + compose('up','-d','--build','--force-recreate',env=env) + ready_until=time.monotonic()+900 + while True: + try: + if call('/health',timeout=3)['ready']:break + except Exception:pass + state=subprocess.run(['docker','inspect','--format','{{.State.Status}}',container_name],capture_output=True,text=True,timeout=10) + if state.returncode or state.stdout.strip() in ['exited','dead']:raise RuntimeError('MA container exited before readiness; inspect episode engine/client logs') + if any('Fatal error:' in p.read_text(errors='replace') for p in job.glob('client-*.log')): + raise RuntimeError('A game browser exited before readiness; inspect episode/client-*.log') + if time.monotonic()>ready_until:raise RuntimeError('All-player readiness timed out') + time.sleep(2) + receipt=call('/arm',{'duration_seconds':args.duration},timeout=180);armed_at=time.monotonic() + isolated=False + try:call('/actors/mab/observation',token=receipt['tokens']['maa']) + except urllib.error.HTTPError as e:isolated=e.code==403 + if not isolated:raise RuntimeError('Actor capability isolation failed') + event('eval.started',None,{'evidence_kind':kind,'scenario':scenario}) + # Four trusted policy loops; models can emit only typed actions, never shell/code. + with concurrent.futures.ThreadPoolExecutor(max_workers=len(scenario['actors'])) as pool: + futures=[pool.submit(policy,a,receipt['tokens'][a['id']]) for a in scenario['actors']] + end=time.monotonic()+args.duration+10 + while time.monotonic() 9_000_000: + raise ValueError('Sample exceeds the small public-example size budget') + policy = json.loads((HERE / 'manifest.json').read_text()) + policy['sample'] = {'file': archive.name, 'sha256': hashlib.sha256(archive.read_bytes()).hexdigest(), 'run': run.name, 'kind': 'scripted', 'modelCalls': 0} + (HERE / 'manifest.json').write_text(json.dumps(policy, indent=2) + '\n') + print(f'Packaged {run.name}: {archive.stat().st_size} bytes') + +def restore(): + sample = json.loads((HERE / 'manifest.json').read_text()).get('sample') + if not sample: + raise ValueError('This checkout has no packaged sample yet') + archive = HERE / sample['file'] + if hashlib.sha256(archive.read_bytes()).hexdigest() != sample['sha256']: + raise ValueError('Sample checksum mismatch') + destination = HERE / 'results' + if (destination / sample['run']).exists(): + raise ValueError('Sample already restored; use review or rebuild instead of overwriting it') + with tarfile.open(archive, 'r:gz') as tar: + expected = {f"{sample['run']}/{name}" for name in ALLOWED} + members = tar.getmembers() + if len(members) != len(expected) or {m.name for m in members} != expected or any(not m.isfile() for m in members): + raise ValueError('Unexpected sample members') + destination.mkdir(exist_ok=True) + tar.extractall(destination, filter='data') + print(f"Restored verified sample {sample['run']}") + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--package', type=Path) + args = parser.parse_args() + package(args.package) if args.package else restore() diff --git a/examples/runebench/sample.tar.gz b/examples/runebench/sample.tar.gz new file mode 100644 index 00000000..6a679b48 Binary files /dev/null and b/examples/runebench/sample.tar.gz differ diff --git a/examples/runebench/save-generator.ts b/examples/runebench/save-generator.ts new file mode 100644 index 00000000..25940d26 --- /dev/null +++ b/examples/runebench/save-generator.ts @@ -0,0 +1,750 @@ +/** + * Save File Generator + * Creates pre-configured save files for testing specific scenarios. + * + * Usage: + * import { generateSave, TestPresets } from './utils/save-generator'; + * + * // Create a save file with custom config + * await generateSave('mybot', { + * position: { x: 3285, z: 3365 }, // SE Varrock mine + * skills: { Mining: 15 }, + * inventory: [{ id: 1265, count: 1 }], // Bronze pickaxe + * }); + * + * // Or use a preset + * await generateSave('miner1', TestPresets.MINER_AT_VARROCK); + */ + +import * as fs from 'fs'; +import * as path from 'path'; + +// Constants from PlayerLoading +const SAV_MAGIC = 0x2004; +const SAV_VERSION = 6; + +// Common inventory type IDs (from game config) +export const InvTypes = { + INV: 93, // Main inventory (28 slots) + WORN: 94, // Equipment (14 slots) + BANK: 95, // Bank (496 slots) +}; + +// Worn-equipment slot indices (engine ObjType.getWearPosId) +export const WearSlots = { + HAT: 0, + CAPE: 1, + AMULET: 2, + WEAPON: 3, // righthand + TORSO: 4, + SHIELD: 5, // lefthand + ARMS: 6, + LEGS: 7, + HANDS: 9, + FEET: 10, + RING: 12, + AMMO: 13, +}; + +// Common item IDs +export const Items = { + // Tools + BRONZE_AXE: 1351, + BRONZE_PICKAXE: 1265, + TINDERBOX: 590, + SMALL_FISHING_NET: 303, + HAMMER: 2347, + CHISEL: 1755, + KNIFE: 946, + // Uncut gems (cut with a chisel for Crafting XP; higher gems need higher levels) + UNCUT_OPAL: 1625, + UNCUT_SAPPHIRE: 1623, + UNCUT_EMERALD: 1621, + UNCUT_RUBY: 1619, + UNCUT_DIAMOND: 1617, + + // Weapons + BRONZE_SWORD: 1277, + BRONZE_DAGGER: 1205, + WOODEN_SHIELD: 1171, + SHORTBOW: 841, + BRONZE_ARROW: 882, + // "Dragonfire shield" in-game — the anti-dragonfire shield (engine + // debugname antidragonbreathshield); caps KBD fiery breath at 15. + ANTI_DRAGON_SHIELD: 1540, + + // Resources + LOGS: 1511, + OAK_LOGS: 1521, + RAW_SHRIMPS: 317, + SHRIMPS: 315, + COPPER_ORE: 436, + TIN_ORE: 438, + BRONZE_BAR: 2349, + IRON_BAR: 2351, + STEEL_BAR: 2353, + MITHRIL_BAR: 2359, + ADAMANTITE_BAR: 2361, + RUNITE_BAR: 2363, + + // Staves (elemental staves substitute for their rune) + STAFF_OF_FIRE: 1387, + + // Runes + AIR_RUNE: 556, + MIND_RUNE: 558, + WATER_RUNE: 555, + EARTH_RUNE: 557, + FIRE_RUNE: 554, + BODY_RUNE: 559, + NATURE_RUNE: 561, + LAW_RUNE: 563, + + // Crafting materials + WOOL: 1737, + BALL_OF_WOOL: 1759, + FLAX: 1779, + BOW_STRING: 1777, + LEATHER: 1741, + HARD_LEATHER: 1743, + GREEN_DRAGONHIDE: 1745, + BLUE_DRAGONHIDE: 2505, + RED_DRAGONHIDE: 2507, + BLACK_DRAGONHIDE: 2509, + SOFT_CLAY: 1761, + NEEDLE: 1733, + THREAD: 1734, + + // Herblore + GUAM_LEAF: 249, // Clean guam leaf + EYE_OF_NEWT: 221, // Secondary ingredient for attack potion + VIAL_OF_WATER: 227, // Base for unfinished potions + GUAM_POTION_UNF: 91, // Unfinished guam potion + ATTACK_POTION_3: 121, // 3-dose attack potion + + // Runecrafting + RUNE_ESSENCE: 1436, // Basic essence for crafting runes + PURE_ESSENCE: 7936, // Pure essence (for higher runes) + AIR_TALISMAN: 1438, // Talisman to enter air altar + + // Clothing (role uniforms for team/market tasks) + BLUE_WIZARD_HAT: 579, // bluewizhat (hat slot) + BLUE_WIZARD_ROBE: 577, // wizards_robe (torso slot) + BLUE_SKIRT: 1011, // blue_skirt (legs slot) — the wizard robe bottom + BROWN_APRON: 1757, // brown_apron (torso slot) + WHITE_APRON: 1005, // white_apron (torso slot) + + // Other + COINS: 995, + BUCKET: 1925, + POT: 1931, + BREAD: 2309, + BONES: 526, + BIG_BONES: 532, +}; + +// Spell component IDs (from content/pack/interface.pack) +export const Spells = { + // Combat spells + WIND_STRIKE: 1152, + CONFUSE: 1153, + WATER_STRIKE: 1154, + ENCHANT_LVL1: 1155, // Sapphire + EARTH_STRIKE: 1156, + WEAKEN: 1157, + FIRE_STRIKE: 1158, + WIND_BOLT: 1160, + CURSE: 1161, + LOW_ALCHEMY: 1162, + WATER_BOLT: 1163, + VARROCK_TELEPORT: 1164, + ENCHANT_LVL2: 1165, // Emerald + EARTH_BOLT: 1166, + LUMBRIDGE_TELEPORT: 1167, + FIRE_BOLT: 1169, + FALADOR_TELEPORT: 1170, + WIND_BLAST: 1172, + SUPERHEAT: 1173, + CAMELOT_TELEPORT: 1174, + WATER_BLAST: 1175, + ENCHANT_LVL3: 1176, // Ruby + EARTH_BLAST: 1177, + HIGH_ALCHEMY: 1178, + ENCHANT_LVL4: 1180, // Diamond + FIRE_BLAST: 1181, + WIND_WAVE: 1183, + WATER_WAVE: 1185, + ENCHANT_LVL5: 1187, // Dragonstone + EARTH_WAVE: 1188, + FIRE_WAVE: 1189, + BIND: 1572, +}; + +// Skill indices +export const Skills = { + ATTACK: 0, + DEFENCE: 1, + STRENGTH: 2, + HITPOINTS: 3, + RANGED: 4, + PRAYER: 5, + MAGIC: 6, + COOKING: 7, + WOODCUTTING: 8, + FLETCHING: 9, + FISHING: 10, + FIREMAKING: 11, + CRAFTING: 12, + SMITHING: 13, + MINING: 14, + HERBLORE: 15, + AGILITY: 16, + THIEVING: 17, + RUNECRAFT: 20, +}; + +// Known locations +export const Locations = { + LUMBRIDGE_CASTLE: { x: 3222, z: 3218 }, + LUMBRIDGE_SPINNING_WHEEL: { x: 3209, z: 3213, level: 2 }, // 2nd floor of castle + LUMBRIDGE_SHOP: { x: 3212, z: 3246 }, + ALKHARID_FISHING: { x: 3267, z: 3148 }, // Safe shrimp spot near Al Kharid palace + DRAYNOR_FISHING: { x: 3086, z: 3230 }, // WARNING: Near dark wizards! + VARROCK_SE_MINE: { x: 3285, z: 3365 }, + VARROCK_SQUARE: { x: 3213, z: 3424 }, // Varrock town square (fountain) + VARROCK_TEA_STALL: { x: 3269, z: 3410 }, // SE Varrock, near tea stall + ALKHARID_MINE: { x: 3300, z: 3310 }, + ALKHARID_FURNACE: { x: 3274, z: 3186 }, + VARROCK_WEST_BANK: { x: 3185, z: 3436 }, + FALADOR_MINE: { x: 3045, z: 9780, level: 0 }, // Dwarven mine + EDGEVILLE_DUNGEON: { x: 3132, z: 9912, level: 0 }, // Edgeville dungeon entrance area + ZANARIS: { x: 3220, z: 9592, level: 0 }, // Lost City (Zanaris) + GNOME_STRONGHOLD_AGILITY: { x: 2474, z: 3436 }, // Gnome Agility Course start + FALADOR_CENTER: { x: 2964, z: 3378 }, // Falador town center + CATHERBY_BEACH: { x: 2836, z: 3432 }, // Catherby fishing spots (on shore) + SEERS_VILLAGE: { x: 2725, z: 3484 }, // Seers Village center + EDGEVILLE_BANK: { x: 3094, z: 3491 }, // Edgeville bank (Wilderness staging) +}; + +// Character-design kit ids from the engine's idk.pack, demon/disabled +// variants excluded. body order: [hair, jaw, torso, arms, hands, legs, feet]; +// female kits live in the 45+ id range and use jaw -1 (none). +const kitRange = (from: number, to: number): number[] => + Array.from({ length: to - from + 1 }, (_, i) => from + i); +const MALE_KITS: number[][] = [ + kitRange(0, 8), // hair + kitRange(10, 17), // jaw + kitRange(18, 25), // torso + kitRange(26, 31), // arms + kitRange(33, 34), // hands + kitRange(36, 40), // legs + kitRange(42, 43), // feet +]; +const FEMALE_KITS: number[][] = [ + kitRange(45, 54), // hair + [-1], // jaw (none) + kitRange(56, 60), // torso + kitRange(61, 65), // arms + kitRange(67, 68), // hands + kitRange(70, 77), // legs + kitRange(79, 80), // feet +]; + +// Palette sizes from the engine's Player.DESIGN_BODY_COLORS: +// [hair, torso, legs, feet, skin] +export const DESIGN_COLOR_COUNTS = [12, 16, 16, 6, 8]; + +/** Random valid character look (gender, body kits, palette colors). */ +export function randomAppearance( + rng: () => number = Math.random +): { gender: number; body: number[]; colors: number[] } { + const pick = (arr: number[]): number => arr[Math.floor(rng() * arr.length)]!; + const gender = rng() < 0.5 ? 0 : 1; + const kits = gender === 0 ? MALE_KITS : FEMALE_KITS; + return { + gender, + body: kits.map(pick), + colors: DESIGN_COLOR_COUNTS.map(n => Math.floor(rng() * n)), + }; +} + +export interface SaveConfig { + position?: { x: number; z: number; level?: number }; + skills?: Record; // Skill name -> level + inventory?: Array<{ id: number; count: number }>; + equipment?: Array<{ id: number; count: number; slot: number }>; + bank?: Array<{ id: number; count: number }>; // Bank items (up to 496 slots) + coins?: number; + varps?: Record; // Varp ID -> value (281=tutorial progress) + appearance?: { + body?: number[]; + colors?: number[]; + gender?: number; + }; +} + +// CRC32 calculation (matches engine's Packet.getcrc) +const CRC32_TABLE = new Int32Array(256); +const CRC32_POLYNOMIAL = 0xedb88320; + +// Initialize CRC table +for (let i = 0; i < 256; i++) { + let crc = i; + for (let j = 0; j < 8; j++) { + if ((crc & 1) === 1) { + crc = (crc >>> 1) ^ CRC32_POLYNOMIAL; + } else { + crc = crc >>> 1; + } + } + CRC32_TABLE[i] = crc; +} + +function getCRC32(data: Uint8Array, offset: number, length: number): number { + let crc = 0xffffffff; + for (let i = offset; i < length; i++) { + const tableIndex = (crc ^ (data[i] ?? 0)) & 0xff; + crc = (crc >>> 8) ^ (CRC32_TABLE[tableIndex] ?? 0); + } + return ~crc; +} + +// Calculate XP for a given level (RS formula) +// Must match engine's levelExperience calculation: Math.floor(acc / 4) * 10 +function getExpByLevel(level: number): number { + let xp = 0; + for (let i = 1; i < level; i++) { + xp += Math.floor(i + 300 * Math.pow(2, i / 10)); + } + return Math.floor(xp / 4) * 10; // ×10 to match engine format +} + +// Binary writer helper +class BinaryWriter { + private buffer: Uint8Array; + private view: DataView; + public pos: number = 0; + + constructor(size: number = 4096) { + this.buffer = new Uint8Array(size); + this.view = new DataView(this.buffer.buffer); + } + + p1(value: number): void { + this.buffer[this.pos++] = value & 0xff; + } + + p2(value: number): void { + this.view.setUint16(this.pos, value, false); // Big-endian + this.pos += 2; + } + + p4(value: number): void { + this.view.setInt32(this.pos, value, false); // Big-endian + this.pos += 4; + } + + p8(value: bigint): void { + this.view.setBigInt64(this.pos, value, false); // Big-endian + this.pos += 8; + } + + getData(): Uint8Array { + return this.buffer.subarray(0, this.pos); + } +} + +/** + * Generate a save file with the given configuration + */ +export function createSaveData(config: SaveConfig): Uint8Array { + const writer = new BinaryWriter(8192); + + // Header + writer.p2(SAV_MAGIC); + writer.p2(SAV_VERSION); + + // Position (default: Lumbridge) + const pos = config.position ?? Locations.LUMBRIDGE_CASTLE; + writer.p2(pos.x); + writer.p2(pos.z); + const posLevel = (pos as { level?: number }).level ?? 0; + writer.p1(posLevel); + + // Appearance - body parts (7 values) + const body = config.appearance?.body ?? [0, 10, 18, 26, 33, 36, 42]; + for (let i = 0; i < 7; i++) { + const val = body[i] ?? -1; + writer.p1(val === -1 ? 255 : val); + } + + // Appearance - colors (5 values) + const colors = config.appearance?.colors ?? [0, 0, 0, 0, 0]; + for (let i = 0; i < 5; i++) { + writer.p1(colors[i] ?? 0); + } + + // Gender + writer.p1(config.appearance?.gender ?? 0); + + // Run energy (max = 10000) + writer.p2(10000); + + // Playtime (ticks) + writer.p4(1000); + + // Skills (21 total) + // Build a case-insensitive skill name lookup + const skillLevels: Record = {}; + if (config.skills) { + for (const [name, level] of Object.entries(config.skills)) { + skillLevels[name.toUpperCase()] = level; + } + } + + for (let i = 0; i < 21; i++) { + let level = 1; + let xp = 0; + + // Find skill by index (case-insensitive lookup) + for (const [skillName, skillIndex] of Object.entries(Skills)) { + if (skillIndex === i && skillLevels[skillName]) { + level = skillLevels[skillName]; + xp = getExpByLevel(level); + break; + } + } + + // Hitpoints defaults to 10 + if (i === Skills.HITPOINTS && level === 1) { + level = 10; + xp = getExpByLevel(10); + } + + writer.p4(xp); // Experience + writer.p1(level); // Current level + } + + // Variables (varps) - must cover all quest varps (highest is troll_quest at 317) + // Set tutorial progress (varp 281) to 1000 to mark tutorial complete + const varpCount = 320; + const varps = new Array(varpCount).fill(0); + varps[281] = 1000; // Tutorial complete + + // Apply custom varps if specified + if (config.varps) { + for (const [id, value] of Object.entries(config.varps)) { + const idx = parseInt(id); + if (idx >= 0 && idx < varpCount) { + varps[idx] = value; + } + } + } + + writer.p2(varpCount); + for (let i = 0; i < varpCount; i++) { + writer.p4(varps[i]); + } + + // Inventories + // Build inventory items array + const invItems: Array<{ id: number; count: number } | null> = new Array(28).fill(null); + const wornItems: Array<{ id: number; count: number } | null> = new Array(14).fill(null); + + // Add coins if specified + if (config.coins && config.coins > 0) { + // Find first empty slot + for (let i = 0; i < 28; i++) { + if (!invItems[i]) { + invItems[i] = { id: Items.COINS, count: config.coins }; + break; + } + } + } + + // Add inventory items + if (config.inventory) { + let slot = 0; + for (const item of config.inventory) { + // Find next empty slot + while (slot < 28 && invItems[slot]) slot++; + if (slot >= 28) break; + invItems[slot] = item; + slot++; + } + } + + // Add equipment items + if (config.equipment) { + for (const item of config.equipment) { + if (item.slot >= 0 && item.slot < 14) { + wornItems[item.slot] = { id: item.id, count: item.count }; + } + } + } + + // Build bank items array (496 slots) + const BANK_SIZE = 496; + const bankItems: Array<{ id: number; count: number } | null> = new Array(BANK_SIZE).fill(null); + if (config.bank) { + for (let i = 0; i < config.bank.length && i < BANK_SIZE; i++) { + bankItems[i] = config.bank[i] ?? null; + } + } + + // Count non-empty inventories + const hasInv = invItems.some(i => i !== null); + const hasWorn = wornItems.some(i => i !== null); + const hasBank = bankItems.some(i => i !== null); + const invCount = (hasInv ? 1 : 0) + (hasWorn ? 1 : 0) + (hasBank ? 1 : 0); + + writer.p1(invCount); + + // Write main inventory + if (hasInv) { + writer.p2(InvTypes.INV); // Type ID + writer.p2(28); // Capacity + for (let i = 0; i < 28; i++) { + const item = invItems[i]; + if (!item) { + writer.p2(0); // Empty slot + } else { + writer.p2(item.id + 1); // Item ID (1-indexed) + if (item.count >= 255) { + writer.p1(255); + writer.p4(item.count); + } else { + writer.p1(item.count); + } + } + } + } + + // Write worn equipment + if (hasWorn) { + writer.p2(InvTypes.WORN); // Type ID + writer.p2(14); // Capacity + for (let i = 0; i < 14; i++) { + const item = wornItems[i]; + if (!item) { + writer.p2(0); // Empty slot + } else { + writer.p2(item.id + 1); // Item ID (1-indexed) + if (item.count >= 255) { + writer.p1(255); + writer.p4(item.count); + } else { + writer.p1(item.count); + } + } + } + } + + // Write bank + if (hasBank) { + writer.p2(InvTypes.BANK); // Type ID + writer.p2(BANK_SIZE); // Capacity + for (let i = 0; i < BANK_SIZE; i++) { + const item = bankItems[i]; + if (!item) { + writer.p2(0); // Empty slot + } else { + writer.p2(item.id + 1); // Item ID (1-indexed) + if (item.count >= 255) { + writer.p1(255); + writer.p4(item.count); + } else { + writer.p1(item.count); + } + } + } + } + + // AFK zones (empty) + writer.p1(0); // Count + writer.p2(0); // Last AFK zone + + // Chat modes (packed byte: public << 4 | private << 2 | trade) + writer.p1(0); + + // Last login time + writer.p8(BigInt(Date.now())); + + // Get data without CRC + const dataWithoutCRC = writer.getData(); + + // Calculate and append CRC + const crc = getCRC32(dataWithoutCRC, 0, dataWithoutCRC.length); + writer.p4(crc); + + return writer.getData(); +} + +/** + * Generate and save a save file for a bot + */ +export async function generateSave( + username: string, + config: SaveConfig, + profile: string = 'main' +): Promise { + const saveData = createSaveData(config); + + // Determine save path - check both possible engine locations + const serverEnginePath = path.join(process.cwd(), 'server/engine/data/players', profile); + const rootEnginePath = path.join(process.cwd(), 'engine/data/players', profile); + const baseDir = fs.existsSync(path.join(process.cwd(), 'server/engine/data/players')) + ? serverEnginePath + : rootEnginePath; + const savePath = path.join(baseDir, `${username.toLowerCase()}.sav`); + + // Ensure directory exists + const dir = path.dirname(savePath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + // Write save file + fs.writeFileSync(savePath, saveData); + console.log(`[SaveGenerator] Created save file: ${savePath}`); + + return savePath; +} + +/** + * Pre-configured test presets + * + * These are user-defined starting constraints for scripts. + * Agents must use one of these presets - they cannot create custom saveConfigs. + * The preset is a fixed constraint (like the goal), not something to optimize. + */ +export const TestPresets = { + // Standard post-tutorial Lumbridge spawn - all skills level 1 + LUMBRIDGE_SPAWN: { + position: Locations.LUMBRIDGE_CASTLE, + inventory: [ + { id: Items.BRONZE_AXE, count: 1 }, + { id: Items.TINDERBOX, count: 1 }, + { id: Items.SMALL_FISHING_NET, count: 1 }, + { id: Items.SHRIMPS, count: 1 }, + { id: Items.BUCKET, count: 1 }, + { id: Items.POT, count: 1 }, + { id: Items.BREAD, count: 1 }, + { id: Items.BRONZE_PICKAXE, count: 1 }, + { id: Items.BRONZE_DAGGER, count: 1 }, + { id: Items.BRONZE_SWORD, count: 1 }, + { id: Items.WOODEN_SHIELD, count: 1 }, + { id: Items.SHORTBOW, count: 1 }, + { id: Items.BRONZE_ARROW, count: 25 }, + { id: Items.AIR_RUNE, count: 25 }, + { id: Items.MIND_RUNE, count: 15 }, + { id: Items.WATER_RUNE, count: 6 }, + { id: Items.EARTH_RUNE, count: 4 }, + { id: Items.BODY_RUNE, count: 2 }, + ], + } as SaveConfig, + + // Miner at SE Varrock mine with pickaxe + MINER_AT_VARROCK: { + position: Locations.VARROCK_SE_MINE, + skills: { Mining: 1 }, + inventory: [ + { id: Items.BRONZE_PICKAXE, count: 1 }, + ], + } as SaveConfig, + + // Miner at Al-Kharid mine (good for mining -> smithing flow) + MINER_AT_ALKHARID: { + position: Locations.ALKHARID_MINE, + skills: { Mining: 1, Smithing: 1 }, + inventory: [ + { id: Items.BRONZE_PICKAXE, count: 1 }, + { id: Items.HAMMER, count: 1 }, + ], + } as SaveConfig, + + // Fisher at Al Kharid with net (safe from enemies) + FISHER_AT_ALKHARID: { + position: Locations.ALKHARID_FISHING, + skills: { Fishing: 1 }, + inventory: [ + { id: Items.SMALL_FISHING_NET, count: 1 }, + ], + } as SaveConfig, + + // Fisher at Draynor with net (WARNING: near dark wizards!) + FISHER_AT_DRAYNOR: { + position: Locations.DRAYNOR_FISHING, + skills: { Fishing: 1 }, + inventory: [ + { id: Items.SMALL_FISHING_NET, count: 1 }, + ], + } as SaveConfig, + + // Fisher+Cook at Al-Kharid (SAFE!) - cook at range, bank fish + FISHER_COOK_AT_DRAYNOR: { + position: Locations.ALKHARID_FISHING, // Safe from dark wizards + skills: { Fishing: 1, Cooking: 1 }, + inventory: [ + { id: Items.SMALL_FISHING_NET, count: 1 }, + ], + } as SaveConfig, + + // Woodcutter at Lumbridge with axe and tinderbox + WOODCUTTER_AT_LUMBRIDGE: { + position: Locations.LUMBRIDGE_CASTLE, + skills: { Woodcutting: 1, Firemaking: 1 }, + inventory: [ + { id: Items.BRONZE_AXE, count: 1 }, + { id: Items.TINDERBOX, count: 1 }, + ], + } as SaveConfig, + + // Combat trainer with basic gear + COMBAT_TRAINER: { + position: Locations.LUMBRIDGE_CASTLE, + skills: { Attack: 1, Strength: 1, Defence: 1 }, + inventory: [ + { id: Items.BRONZE_SWORD, count: 1 }, + { id: Items.WOODEN_SHIELD, count: 1 }, + { id: Items.BREAD, count: 10 }, + ], + } as SaveConfig, + + // Thief at Varrock tea stall (level 5 required) + THIEF_AT_VARROCK: { + position: Locations.VARROCK_TEA_STALL, + skills: { Thieving: 5 }, + } as SaveConfig, + + // Agility trainer at Gnome Stronghold course + GNOME_AGILITY: { + position: Locations.GNOME_STRONGHOLD_AGILITY, + skills: { Agility: 1 }, + } as SaveConfig, +}; + +/** Type for a single test preset (value from TestPresets) */ +export type TestPreset = SaveConfig; + +// CLI usage +if (import.meta.main) { + const args = process.argv.slice(2); + if (args.length < 2) { + console.log('Usage: bun run test/utils/save-generator.ts '); + console.log('Presets:', Object.keys(TestPresets).join(', ')); + process.exit(1); + } + + const username = args[0]!; + const presetName = args[1]!; + const preset = TestPresets[presetName as keyof typeof TestPresets]; + + if (!preset) { + console.error(`Unknown preset: ${presetName}`); + console.log('Available presets:', Object.keys(TestPresets).join(', ')); + process.exit(1); + } + + generateSave(username, preset); +} diff --git a/examples/runebench/scenario.json b/examples/runebench/scenario.json new file mode 100644 index 00000000..cfc59347 --- /dev/null +++ b/examples/runebench/scenario.json @@ -0,0 +1,15 @@ +{ + "id": "woodcutting-team-sprint-v1", + "title": "Woodland sprint", + "evidence_kind": "provider-backed", + "duration_seconds": 150, + "tick_ms": 300, + "xp_multiplier": 1, + "score": "Sum of each teammate's Woodcutting XP gained since arm, using the last complete engine tick at or before the wall deadline.", + "actors": [ + {"id":"maa","team":"moss","role":"lead","position":{"x":3200,"z":3243},"color":"#3d876a"}, + {"id":"mab","team":"moss","role":"scout","position":{"x":3201,"z":3243},"color":"#74b49b"}, + {"id":"mba","team":"ember","role":"lead","position":{"x":3200,"z":3245},"color":"#be633e"}, + {"id":"mbb","team":"ember","role":"scout","position":{"x":3201,"z":3245},"color":"#df9c65"} + ] +} diff --git a/examples/runebench/serve.py b/examples/runebench/serve.py new file mode 100644 index 00000000..f090a0fd --- /dev/null +++ b/examples/runebench/serve.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +"""Local query/media service plus capability-scoped append-only evidence writes.""" +import html, json, mimetypes, re, subprocess, threading +FRAME_LOCK=threading.Lock() +from http.server import BaseHTTPRequestHandler,ThreadingHTTPServer +from urllib.parse import urlparse,parse_qs,unquote +from query import OUT,HERE,query,QUERIES +from evidence_api import capability, evidence +class Handler(BaseHTTPRequestHandler): + def cors(self): + origin=self.headers.get('Origin','') + if origin in ('http://127.0.0.1:8128','http://localhost:8128','http://127.0.0.1:14338','http://localhost:14338','tauri://localhost','http://tauri.localhost','https://tauri.localhost'): + self.send_header('Access-Control-Allow-Origin',origin);self.send_header('Vary','Origin') + def do_OPTIONS(self): + self.send_response(204);self.cors();self.send_header('Access-Control-Allow-Headers','Content-Type, X-Annotation-Capability');self.send_header('Access-Control-Allow-Methods','GET, POST, OPTIONS');self.end_headers() + def do_POST(self): + try: + u=urlparse(self.path) + if u.path!='/annotations':return self.send_error(404) + if not __import__('hmac').compare_digest(self.headers.get('X-Annotation-Capability',''),capability()):return self.json({'error':'Annotation capability required'},403) + length=int(self.headers.get('Content-Length','0')) + if not 01000000:raise ValueError('Invalid evidence reference') + p=OUT/run/({'episode':'episode/events.jsonl','run':'events.jsonl','engine':'episode/engine-states.jsonl'}[scope]) + if not p.resolve().is_relative_to(OUT.resolve()) or not p.is_file():return self.send_error(404) + with p.open() as f: + for n,row in enumerate(f,1): + if n==line: + record={'source':str(p.relative_to(OUT)),'line':line,'record':json.loads(row)} + if 'text/html' not in self.headers.get('Accept',''):return self.json(record) + content=html.escape(json.dumps(record,indent=2)) + b=('RuneBench source record

Exact source record

'+content+'
').encode() + self.send_response(200);self.send_header('Content-Type','text/html; charset=utf-8');self.send_header('Content-Length',str(len(b)));self.end_headers();self.wfile.write(b);return + return self.send_error(404) + if u.path=='/data':return self.json(json.loads((HERE/'swarm-data.json').read_text())) + if u.path.startswith('/frame/'): + parts=unquote(u.path).split('/');q=parse_qs(u.query) + if len(parts)!=4 or not re.fullmatch(r'[a-zA-Z0-9-]+',parts[2]) or not re.fullmatch(r'[a-z0-9]+\.jpg',parts[3]):raise ValueError('Invalid frame path') + ms=int(q.get('ms',['0'])[0]) + if not 0<=ms<=600000:raise ValueError('Invalid frame timestamp') + ms=ms//500*500;actor=parts[3][:-4];p=OUT/parts[2]/'episode'/f'{actor}.mp4' + if not p.is_file():return self.send_error(404) + cache=HERE/'frame-cache'/parts[2];cache.mkdir(parents=True,exist_ok=True);frame=cache/f'{actor}-{ms}.jpg' + with FRAME_LOCK: + if not frame.exists(): + result=subprocess.run(['ffmpeg','-loglevel','error','-ss',str(ms/1000),'-i',str(p),'-frames:v','1','-q:v','3','-f','image2pipe','-vcodec','mjpeg','-'],capture_output=True,timeout=15) + if result.returncode or not result.stdout:raise ValueError('Frame unavailable at selected timestamp') + frame.write_bytes(result.stdout) + b=frame.read_bytes();self.send_response(200);self.cors();self.send_header('Content-Type','image/jpeg');self.send_header('Content-Length',str(len(b)));self.end_headers();self.wfile.write(b);return + if u.path.startswith('/media/'): + parts=unquote(u.path).split('/') + if len(parts)!=4 or not re.fullmatch(r'[a-zA-Z0-9-]+',parts[2]) or not re.fullmatch(r'[a-z0-9]+\.mp4',parts[3]):raise ValueError('Invalid media path') + p=(OUT/parts[2]/'episode'/parts[3]).resolve() + if not p.is_relative_to(OUT.resolve()) or not p.is_file():return self.send_error(404) + size=p.stat().st_size;start=0;end=size-1;partial=False + if self.headers.get('Range'): + m=re.fullmatch(r'bytes=(\d+)-(\d*)',self.headers['Range']) + if not m:return self.send_error(416) + start=int(m[1]);end=min(end,int(m[2]) if m[2] else end);partial=True + if start>end:return self.send_error(416) + self.send_response(206 if partial else 200);self.send_header('Content-Type','video/mp4');self.send_header('Accept-Ranges','bytes');self.send_header('Content-Length',str(end-start+1)) + if partial:self.send_header('Content-Range',f'bytes {start}-{end}/{size}') + self.end_headers() + with p.open('rb') as f: + f.seek(start);left=end-start+1 + while left>0: + b=f.read(min(left,65536));self.wfile.write(b);left-=len(b) + return + self.send_error(404) + except (BrokenPipeError,ConnectionResetError):pass + except Exception as e:self.json({'error':str(e)},400) + def json(self,data,status=200): + b=json.dumps(data).encode();self.send_response(status);self.cors();self.send_header('Content-Type','application/json');self.send_header('Content-Length',str(len(b)));self.send_header('Cache-Control','no-store');self.end_headers();self.wfile.write(b) +if __name__=='__main__':ThreadingHTTPServer(('127.0.0.1',8128),Handler).serve_forever() diff --git a/examples/runebench/smoke.ts b/examples/runebench/smoke.ts new file mode 100644 index 00000000..e7eb675a --- /dev/null +++ b/examples/runebench/smoke.ts @@ -0,0 +1,16 @@ +import {BotSDK} from '/app/sdk/index'; +import {writeFileSync} from 'node:fs'; +const names=process.env.BOT_NAMES!.split(/\s+/); +const bots:BotSDK[]=[]; +try { + for(const name of names){ + const sdk=new BotSDK({botUsername:name,password:'test',gatewayUrl:'ws://localhost:7780',connectionMode:'control',autoLaunchBrowser:false,autoReconnect:false}); + await sdk.connect();await sdk.waitForCondition(s=>s.inGame,60000);bots.push(sdk); + } + const token=`ma probe ${Date.now().toString().slice(-6)}`; + const actions=[]; + for(let i=0;i<2;i++) actions.push(await bots[i].say(`${token} ${i}`)); + await bots[2].waitForCondition(s=>[0,1].every(i=>s.gameMessages?.some(m=>m.text.toLowerCase().includes(`${token} ${i}`))),30000); + const result={kind:'local-scripted-ma-smoke',passed:true,timestamp:new Date().toISOString(),names,token,actions,witness:bots[2].getState()}; + writeFileSync('/logs/ma/smoke.json',JSON.stringify(result,null,2));console.log(JSON.stringify({passed:true,names,token})); +} finally {await Promise.all(bots.map(b=>b.disconnect()));} diff --git a/examples/runebench/viewer.tsx b/examples/runebench/viewer.tsx new file mode 100644 index 00000000..7d1296ee --- /dev/null +++ b/examples/runebench/viewer.tsx @@ -0,0 +1,165 @@ +import {useState,useEffect,useRef} from 'react'; +import {AgentTraceInspector,useTraceArchive,runeBenchTraceExtension,craftaxTraceExtension,useTraceEvidence,AnnotationEditor,TraceResearchPanel,useTraceViewState} from '@synth/visuals/components/agent_trace.v1'; +const clock=(ms:number)=>`${Math.floor(ms/60000)}:${String(Math.floor(ms/1000)%60).padStart(2,'0')}`; +const names:any={all:'All episodes',failed_chops:'Unsuccessful chops',heard_teammate:'Teammate messages heard',same_tree:'Same tree within 5 seconds'}; +const modelName=(run:any)=>run.model?.endsWith('gpt-5.6-terra')?'Terra':run.model?.endsWith('gpt-5.6-luna')?'Luna':run.model||'Unknown model'; +const armLabel=(run:any)=>`${modelName(run)} · ${run.effort}`; +const compareArms=(a:any,b:any)=>modelName(a).localeCompare(modelName(b))||(['low','medium','high'].indexOf(a.effort)-['low','medium','high'].indexOf(b.effort)); +function AggregateDifferences({runs}:any){ + runs=[...runs].sort(compareArms); + const arms=[...new Set(runs.filter((r:any)=>r.effort&&r.status==='evaluated').map((r:any)=>JSON.stringify([r.model,r.effort])))]; + const groups=arms.map(arm=>runs.filter((r:any)=>r.status==='evaluated'&&JSON.stringify([r.model,r.effort])===arm)); + if(groups.length<2)return null; + const sum=(group:any[],get:(r:any)=>number)=>group.reduce((total,r)=>total+get(r),0); + const metrics=[ + {label:'Total XP',get:(g:any[])=>sum(g,r=>Object.values(r.teams).reduce((a:any,b:any)=>a+b,0) as number),format:(n:number)=>n.toLocaleString()}, + {label:'Mean response time',get:(g:any[])=>sum(g,r=>r.comparison.meanResponseMs*r.calls)/sum(g,r=>r.calls),format:(n:number)=>`${(n/1000).toFixed(2)}s`}, + {label:'Reasoning tokens',get:(g:any[])=>sum(g,r=>r.comparison.reasoningTokens),format:(n:number)=>n.toLocaleString()}, + {label:'Output tokens',get:(g:any[])=>sum(g,r=>r.comparison.completionTokens),format:(n:number)=>n.toLocaleString()}, + {label:'Cost',get:(g:any[])=>sum(g,r=>r.cost),format:(n:number)=>`$${n.toFixed(4)}`}, + {label:'Model calls',get:(g:any[])=>sum(g,r=>r.calls),format:(n:number)=>n.toLocaleString()}, + ]; + return
+

Aggregate differences

{runs.filter((r:any)=>r.effort&&r.policyErrors?.length).map((r:any)=>

{armLabel(r)}: {r.calls} completed calls; {r.policyErrors.length} policy error(s): {r.policyErrors.map((e:any)=>`${e.actor}: ${e.error}`).join("; ")}. Call counts differ; totals are descriptive.

)}
{groups.map(group=>`${armLabel(group[0])}: ${group.length} episode${group.length===1?'':'s'}`).join(' · ')}. Changes relative to {armLabel(groups[0][0])}. Output tokens include reasoning tokens.
+
{['Metric',...groups.map(group=>armLabel(group[0]))].map(h=>)}{metrics.map(metric=>{const values=groups.map(metric.get);const baseline=values[0];return {values.map((value,index)=>{const difference=value-baseline;return })}})}
{h}
{metric.label}{metric.format(value)}{index>0&&
{difference===0?'No change':`${difference>0?'+':'−'}${metric.format(Math.abs(difference))}${baseline?` (${difference>0?'+':''}${(difference/baseline*100).toFixed(1)}%)`:''}`}
}
+
; +} +function PlayerVideo({media,ms}:{media:any,ms:number}){ + if(msNative recording begins at {clock(media.offsetMs)}. Earlier observations remain available below.; + const timestamp=Math.floor(Math.max(0,(ms-media.offsetMs))/500)*500; + const frame=media.url.replace('/media/','/frame/').replace('.mp4','.jpg')+'?ms='+timestamp; + return
Frame extracted from this player's native recording at the replay cursor
Original recording frame · nearest 0.5 seconds · Open video ↗
; +} +function RunComparison({DATA,data,traces,onBack,traceResearch}:any){ + const options=DATA.runs.filter((r:any)=>r.effort&&r.status==='evaluated').sort(compareArms); + const [ids,setIds]=useTraceViewState('runebench-comparison:ids',[options.find((r:any)=>modelName(r)==='Luna'&&r.effort==='low')?.id||options[0].id,options.find((r:any)=>r.effort==='medium')?.id||options[1].id]); + const runs=ids.map((id:string,i:number)=>options.find((r:any)=>r.id===id)||options[i]); + const [mode,setMode]=useTraceViewState('runebench-comparison:alignment','independent'); + const [positions,setPositions]=useTraceViewState('runebench-comparison:positions',[{ms:0,actor:'all'},{ms:0,actor:'all'}]); + const [filter,setFilter]=useTraceViewState('runebench-comparison:events','failures'); + const [actorFilter,setActorFilter]=useTraceViewState('runebench-comparison:actor','all'); + const [term,setTerm]=useTraceViewState('runebench-comparison:search',''); + const [focus,setFocus]=useTraceViewState('runebench-comparison:focus',[null,null]); + function seek(side:number,ms:number,actor?:string,decision?:string){ + setPositions(previous=>previous.map((p,i)=>i===side?{ms,actor:actor||p.actor}:mode==='elapsed'?{...p,ms:Math.min(ms,runs[i].durationMs)}:mode==='decision'&&decision?{ms:runs[i].timeline.find((e:any)=>e.kind==='policy.action'&&e.decisionId===decision)?.ms??p.ms,actor:actor||p.actor}:p)); + } + function jump(side:number,event:any){seek(side,event.ms,event.actor,event.decisionId);setFocus(previous=>previous.map((x,i)=>i===side?{event,revision:(x?.revision||0)+1}:x));} + const hits=runs.flatMap((r:any,side:number)=>r.timeline.filter((e:any)=> + (filter==='failures'?e.kind==='policy.result'&&e.success===false:filter==='results'?e.kind==='policy.result':filter==='messages'?e.kind==='message.observed':e.kind==='policy.action')&& + (actorFilter==='all'||e.actor===actorFilter)&&(!term||JSON.stringify(e).toLowerCase().includes(term.toLowerCase()))).map((event:any)=>({side,event}))).sort((a:any,b:any)=>a.event.ms-b.event.ms); + return
+ +

Compare rollouts

Select runs, inspect an actor’s contribution, then open the recorded calls and results. Each column retains its own trace and annotations.

+
Query saved eval jobs · Workshop backendr.id)} stateKey="runebench-research" onInspect={(selection:any)=>{ + const index=options.findIndex((r:any)=>r.traceDigest===selection.selector?.trace_digest); + if(index<0)throw new Error('This query result has no matching retained RuneBench replay. Its exact source remains readable.'); + const run=options[index];const side=ids[1]===run.id?1:0; + setIds((v:string[])=>v.map((id,i)=>i===side?run.id:id)); + setFocus((v:any[])=>v.map((x,i)=>i===side?{selector:selection.selector,revision:(x?.revision||0)+1}:x)); + setPositions((v:any[])=>v.map((x,i)=>i===side?{...x,actor:'all'}:x)); + }}/>
{ids.map((id,side)=>)}
+

{mode==='decision'?'Selecting a decision aligns the same actor and decision number in the other run, when present. These are ordinal matches, not equivalent game states.':mode==='elapsed'?'Moving either cursor seeks both runs to the same elapsed time.':'Each replay cursor moves independently.'} One episode per arm; engine RNG is unseeded.

+
Actor contribution differences{runs.map((r:any)=>)}{runs[0].actors.map((a:any)=>{runs.map((r:any)=>)})}
Actor{armLabel(r)} XPChange (right − left)Evidence
{a.id} · {a.team} · {a.role}{r.scores[a.id]??'—'}{runs[1].scores[a.id]-runs[0].scores[a.id]}
+
setTerm(e.target.value)}/>{hits.length} matching events across these two runs
+
{hits.map(({side,event}:any,i:number)=>)}{!hits.length&&

No matching recorded events.

}
+
{runs.map((r:any,side:number)=>seek(side,ms,actor,decision)}/>)}
+
; +} +function ComparisonPane({side,run:r,data,fallback,position,focus,onSeek}:any){ + const evidence=useTraceEvidence(data?.annotationService,r.id,fallback); + const [edit,setEdit]=useState(null); + const projection=evidence.projection; + const actor=position.actor; + const selected=focus?.selector ? (focus.selector.trace_digest===projection?.trace_digest?projection?.items.find((item:any)=>item.source_selector?.entity_id===focus.selector.entity_id):undefined) : focus?.event?.evidence&&projection?.items.find((item:any)=>{const source=item.detail?.source;if(!source)return false;const url=new URL(focus.event.evidence);return source.line===Number(url.searchParams.get('line'))&&source.file===(url.searchParams.get('scope')==='episode'?'episode/events.jsonl':'events.jsonl');}); + useEffect(()=>{if(selected&&focus?.selector){const actor=selected.detail?.native_actor_id;const ms=selected.detail?.elapsed_ms??selected.detail?.elapsedMs;if(ms!=null)onSeek(ms,actor);}},[selected?.item_id,focus?.revision]); + const sourceUrl=(item:any)=>{const source=item.detail?.source;if(!source)return;const scope=source.file==='events.jsonl'?'run':source.file==='episode/events.jsonl'?'episode':source.file==='episode/engine-states.jsonl'?'engine':null;return scope?`http://127.0.0.1:8128/evidence?run=${r.id}&scope=${scope}&line=${source.line}`:undefined;}; + return
+

{armLabel(r)}

{r.id} · {r.calls} calls · {Object.values(r.teams).reduce((a:any,b:any)=>a+b,0) as any} XP +
onSeek(Number(e.target.value))}/>{clock(position.ms)} / {clock(r.durationMs)} · {actor==='all'?'All agents':actor}
+
Game replay · {actor==='all'?'all agents':actor}
{r.actors.filter((a:any)=>actor==='all'||a.id===actor).map((a:any)=>
{a.id} · {a.team} · {a.role}{r.media[a.id]?:

No recording.

}
)}
+ {evidence.error&&

{evidence.error}

}{edit&&setEdit(null)}/>} + {projection?{const a=r.actors.find((a:any)=>a.id===lane.display_name);return a?{...lane,color:a.color,display_name:`${a.id} · ${a.team}`}:lane;})}} initialView="runebench" extensions={[runeBenchTraceExtension]} actorId={actor==='all'?'all':projection.lanes.find((l:any)=>l.display_name===actor)?.actor_id} cursorMs={position.ms} selection={selected?{itemId:selected.item_id,revision:focus.revision}:undefined} onActorChange={(id:string)=>onSeek(position.ms,id==='all'?'all':projection.lanes.find((l:any)=>l.actor_id===id)?.display_name)} onSelectEvent={(item:any,time:any)=>onSeek(time??position.ms,actor==='all'?'all':item.detail?.native_actor_id||actor,item.detail?.decisionId||item.detail?.action?.decisionId)} sourceUrl={sourceUrl} onAnnotate={evidence.ready?(target:any)=>setEdit({target}):undefined} onOpenAnnotation={(note:any)=>setEdit({target:note.target,note})}/>:

Loading trace…

} +
; +} + +function CraftaxReview({data,projections,onBack}:any){ + const [source,setSource]=useState('craftax-calls'); + const evidence=useTraceEvidence(data?.annotationService,source,projections?.[source]); + const [edit,setEdit]=useState(null); + useEffect(()=>setEdit(null),[source]); + return
+

Craftax · decisions and outcomes

+ +

{source==='craftax-calls'?'10 independent single-agent episodes · 66 retained prompts and replies, with executed actions and rewards.':'10 recorded runs · game frames and terminal summaries. Model messages and tool-call text were not retained in this source.'}

+ {evidence.error&&

{evidence.error}

} + {edit&&setEdit(null)}/>} + {evidence.projection?setEdit({target}):undefined} onOpenAnnotation={(note:any)=>setEdit({target:note.target,note})}/>:

Loading retained Craftax trace…

} +
; +} +export function Shell({data,traceResearch}:any){ + const traces=useTraceArchive(data?.traceArchive); + if(!traces.data?.swarmData)return

{traces.error||'Loading rollouts…'}

; + return ; +} +function Explorer({data,traces,DATA,traceResearch}:any){ +const firstRun=DATA.runs.find((r:any)=>r.model?.endsWith('luna')&&r.effort==='medium'&&r.status==='evaluated')||DATA.runs.find((r:any)=>r.model?.endsWith('terra')&&r.status==='evaluated')||DATA.runs.find((r:any)=>r.effort==='low'&&r.status==='evaluated')||DATA.runs.find((r:any)=>r.kind==='provider-backed'&&r.status==='evaluated'); +const firstMoment=firstRun?.timeline.find((e:any)=>e.kind==='action.completed'&&e.success===false)?.ms||15000; + + const mainRef=useRef(null); + const [environment,setEnvironment]=useState('rune'); + const [query,setQuery]=useState('all'),[selected,setSelected]=useState(Math.max(0,DATA.runs.findIndex((r:any)=>r.id===firstRun?.id))),[ms,setMs]=useState(firstMoment),[playing,setPlaying]=useState(false),[actor,setActor]=useState('all'),[showSql,setShowSql]=useState(false); + const tourCaption=''; + const [hitLimit,setHitLimit]=useState(25),[selectedHit,setSelectedHit]=useState(null); + useEffect(()=>{setHitLimit(25);setSelectedHit(null)},[query]); + const hitSource=(hit:any)=>{ + if(hit.evidenceUrl)return hit.evidenceUrl; + const match=String(hit.ref||'').match(/\/(episode\/)?events\.jsonl:(\d+)$/); + return match?`http://127.0.0.1:8128/evidence?run=${encodeURIComponent(hit.run_id)}&scope=${match[1]?'episode':'run'}&line=${match[2]}`:null; + }; + const r=DATA.runs[selected]||DATA.runs[0]; + const evidence=useTraceEvidence(data?.annotationService,r.id,traces.data?.traceProjections?.[r.id]); + const projection=evidence.projection; + const [edit,setEdit]=useState(null); + useEffect(()=>setEdit(null),[r.id]); + const rows=DATA.queries[query].rows; + const matching=new Set(rows.map((x:any)=>x.run_id)); + const cohort=DATA.runs.filter((x:any)=>matching.has(x.id)).sort((a:any,b:any)=>Number(b.kind==='provider-backed')-Number(a.kind==='provider-backed')); + const chosen=r.actors.find((a:any)=>a.id===actor)||r.actors[0]; + const moments=r.timeline.filter((e:any)=>e.actor===chosen.id&&e.ms<=ms); + const latest=moments.at(-1); + const policy=moments.filter((e:any)=>e.kind==='policy.action').at(-1); + const frame=(r.frames[chosen.id]||[]).filter((f:any)=>f.ms<=ms).at(-1); + const own=r.samples.filter((s:any)=>s.actor===chosen.id&&s.ms<=ms).at(-1); + const activeMessages=r.timeline.filter((e:any)=>e.kind==='message.observed'&&e.actor===chosen.id&&e.ms<=ms&&e.ms>ms-30000); + useEffect(()=>{if(!playing)return;const timer=setInterval(()=>setMs(v=>{if(v>=r.durationMs){setPlaying(false);return r.durationMs}return Math.min(r.durationMs,v+500)}),100);return()=>clearInterval(timer)},[playing,r.durationMs]); + const select=(id:string,time=0)=>{setSelected(DATA.runs.findIndex((x:any)=>x.id===id));setMs(time);setPlaying(false)}; + const scored=DATA.runs.filter((x:any)=>x.kind==='provider-backed'&&x.status==='evaluated').length; + const failed=DATA.runs.filter((x:any)=>x.status==='error').length; + const gameContext=(actorId:string)=>{const chosen=r.actors.find((a:any)=>a.id===projection?.lanes.find((l:any)=>l.actor_id===actorId)?.display_name);if(!chosen)return

No game recording for this lane.

;const own=r.samples.filter((s:any)=>s.actor===chosen.id&&s.ms<=ms).at(-1);const frame=(r.frames[chosen.id]||[]).filter((f:any)=>f.ms<=ms).at(-1);return

{chosen.id} · {chosen.team} team · {chosen.role} · replay

{r.media[chosen.id]?:
Observed positions · not a reconstructed map{r.actors.map((a:any,i:number)=>{const p=r.samples.filter((s:any)=>s.actor===a.id&&s.ms<=ms).at(-1);return {a.id}{p?`${p.x}, ${p.z}`:'not sampled'}})}
}
Retained state {own?clock(own.ms):'—'} · position {own?`${own.x}, ${own.z}`:'not sampled'} · XP gained {own?.xp??'unscored'}
Player-local inventory and messages
Nearest retained observation: {frame?clock(frame.ms):'unavailable'}. Inventory snapshots are sampled about every 15 seconds.
{JSON.stringify(frame?{inventory:frame.inventory,messages:frame.messages}:null,null,2)}
;}; + if(environment==='compare'&&DATA.runs.filter((x:any)=>x.effort&&x.status==='evaluated').length>=2)return setEnvironment('rune')}/>; + if(environment==='craftax')return setEnvironment('rune')}/>; + return
+ + +
RuneBench · agent & swarm analysis

Decisions, actions, and consequences

{scored} model episodes · {DATA.runs.filter((x:any)=>x.kind==='scripted'&&x.status==='evaluated').length} scripted episodes · {failed} incomplete attempts
+ {data?.annotationService&&!evidence.ready&&

{evidence.error||'Loading annotation evidence before native review…'}

} +
{r.actors.map((a:any)=>
{r.media[a.id]?:

No recording for this agent.

}
)}
+ {tourCaption&&
{tourCaption}
Scripted navigation through retained evidence
} + {DATA.runs.filter((x:any)=>x.effort&&x.status==='evaluated').length>=2&&

Model comparison · RuneBench

Same starting saves, 150-second deadline and decision cap. Engine RNG is unseeded; compare these episodes descriptively.

{DATA.runs.filter((x:any)=>x.effort).sort(compareArms).map((x:any)=>)}
} +
+
{showSql&&
{DATA.querySql[query]}
}
{r.effort?`${armLabel(r)} reasoning · ${r.title}`:r.title}
Rollout {r.id} · {r.actors.length} agents in this episode
{r.kind} · {r.model||'Scripted SDK controllers'} · {r.calls} model calls{r.cost!=null?` · $${r.cost.toFixed(5)}`:''}
{Object.entries(r.teams).map(([t,v])=>{t}: {v as any} XP)}
+ {r.status==='error'&&r.samples.length===0?
This attempt failed before a scored episode. It is retained in the cohort as an infrastructure failure; there is no score or gameplay to inspect.
:<> +
Team timeline · {r.actors.length} agents
Solid: action · small marks: observed chat
+ {r.actors.map((a:any)=>
{r.timeline.filter((e:any)=>e.actor===a.id&&(e.kind==='action.started'||e.kind==='message.observed')).map((e:any,i:number)=>{const end=e.kind==='action.started'?r.timeline.find((v:any)=>v.kind==='action.completed'&&v.actor===a.id&&v.decisionId===e.decisionId)?.ms:e.ms+500;return
)} +
{setMs(Number(e.target.value));setPlaying(false)}}/>
{clock(ms)}/ {clock(r.durationMs)}
One episode, {r.actors.length} interacting actors
+ +

Decision transcript{r.effort?` · ${modelName(r)} ${r.effort}`:""}

{evidence.error&&

{evidence.error}

}{edit&&setEdit(null)}/>}{projection ? {const a=r.actors.find((a:any)=>a.id===lane.display_name);return a?{...lane,color:a.color,display_name:`${a.id} · ${a.team} team`}:lane;})}} initialView="runebench" extensions={[runeBenchTraceExtension]} renderEnvironment={(id:any)=>id?gameContext(id):

All agents · same episode

{r.actors.map((a:any)=>
{a.id} · {a.team} team · {a.role}{r.media[a.id]?:

No recording for this agent.

}
)}
} onAnnotate={data?.annotationService && evidence.ready ? (target:any)=>setEdit({target}) : undefined} onOpenAnnotation={(note:any)=>setEdit({target:note.target,note})} actorId={actor==='all'?'all':projection.lanes.find((l:any)=>l.display_name===chosen.id)?.actor_id} cursorMs={ms} onActorChange={(id:any)=>{if(id==='all'){setActor('all');return}const lane=projection.lanes.find((l:any)=>l.actor_id===id);if(lane&&r.actors.some((a:any)=>a.id===lane.display_name))setActor(lane.display_name)}} onSelectEvent={(item:any,time:any)=>{if(time!=null)setMs(time);if(actor!=='all'&&item.detail?.native_actor_id&&r.actors.some((a:any)=>a.id===item.detail.native_actor_id))setActor(item.detail.native_actor_id);setPlaying(false)}} sourceUrl={(item:any)=>{const source=item.detail?.source;if(!source)return;const scope=source.file==='events.jsonl'?'run':source.file==='episode/events.jsonl'?'episode':source.file==='episode/engine-states.jsonl'?'engine':null;return scope?`http://127.0.0.1:8128/evidence?run=${r.id}&scope=${scope}&line=${source.line}`:undefined}}/>:

{traces.error || (traces.loading ? 'Loading trace evidence…' : 'Trace projection unavailable. Rebuild and bind this episode’s V5 evidence.')}

}
+ +

Team contributions · retrospective engine state

{r.actors.map((a:any)=>{const s=r.samples.filter((s:any)=>s.actor===a.id&&s.ms<=ms).at(-1);return
{a.id} · {a.team}{s?.xp??'—'} XP
{a.role} · {r.timeline.filter((e:any)=>e.actor===a.id&&e.kind==='action.started'&&e.ms<=ms).length} actions started
})}
+ {r.cutoff&&
Score uses the last complete engine tick before cutoff: {clock(r.cutoff.final.elapsedMs)}. Late actions rejected; score receipt frozen. Message observations show delivery, not proof that the recipient used the information.
} + } +
{r.kind==='scripted'?'Scripted evidence; no model-performance claim. ':''}Episodes are the unit of comparison; players within an episode are not independent trials. Same-tree queries flag attempts within 5 seconds, not proven causal interference. Raw evidence and read-only SQL remain available for inspection.
; +} +export default Shell; diff --git a/package-lock.json b/package-lock.json index 022a8ffa..eb59b7b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "playwright": "1.62.1" }, "devDependencies": { + "@openai/codex": "0.153.0", "turbo": "^2.10.9" }, "engines": { @@ -1074,6 +1075,135 @@ "node": "^22.20 || ^24.12 || >=25" } }, + "node_modules/@openai/codex": { + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.153.0.tgz", + "integrity": "sha512-k55kUZaclNi5ceUStSVuyW834ruA6AEdzTK7Xi3M1mOyXokUmq1sJLXm1RJ3XD2S7bRPeF1EXNsYB5Qxwus0mw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "codex": "bin/codex.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@openai/codex-darwin-arm64": "npm:@openai/codex@0.153.0-darwin-arm64", + "@openai/codex-darwin-x64": "npm:@openai/codex@0.153.0-darwin-x64", + "@openai/codex-linux-arm64": "npm:@openai/codex@0.153.0-linux-arm64", + "@openai/codex-linux-x64": "npm:@openai/codex@0.153.0-linux-x64", + "@openai/codex-win32-arm64": "npm:@openai/codex@0.153.0-win32-arm64", + "@openai/codex-win32-x64": "npm:@openai/codex@0.153.0-win32-x64" + } + }, + "node_modules/@openai/codex-darwin-arm64": { + "name": "@openai/codex", + "version": "0.153.0-darwin-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.153.0-darwin-arm64.tgz", + "integrity": "sha512-C9F3HVEYekVJC3WhiGSztcxItWBwd7Y6hTWoDqa9Z9aLWsYywzqChABB59n7G5F5jCil9wxW3+u2L6uiVUydmw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@openai/codex-darwin-x64": { + "name": "@openai/codex", + "version": "0.153.0-darwin-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.153.0-darwin-x64.tgz", + "integrity": "sha512-DO5u+X/eL8pObrV0WDErOjS9DeuDvVuOL5oHR5y5Yklp0NGoGj3oOHsmaSXjGesiIiu3IX8l4pzS6dbeQXJAvQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@openai/codex-linux-arm64": { + "name": "@openai/codex", + "version": "0.153.0-linux-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.153.0-linux-arm64.tgz", + "integrity": "sha512-N2zLgmuslhMoC2RFC2aDkcCWk3iapZmBlSiESpjXFF+UMNxIpNxWJ5qQtQrCp5Md7kYbF4/EFy8RqJ8tL8UTeA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@openai/codex-linux-x64": { + "name": "@openai/codex", + "version": "0.153.0-linux-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.153.0-linux-x64.tgz", + "integrity": "sha512-sFF+g7p1o6sZVL6PHd9JTYrP2j3xao3Qeu5AngcrD5brWrCc+w9ifBJCJuRSM728WfgjXML9PQPdHPaZSeHAEA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@openai/codex-win32-arm64": { + "name": "@openai/codex", + "version": "0.153.0-win32-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.153.0-win32-arm64.tgz", + "integrity": "sha512-Ro+/2oAQnOObxuw3hpr+anv+j9hFmql/2iDxd7v0m46ctZy8x6ygW/Ud4f9CB1UXlRRsDhW+Tu9UAd5X9vTL1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@openai/codex-win32-x64": { + "name": "@openai/codex", + "version": "0.153.0-win32-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.153.0-win32-x64.tgz", + "integrity": "sha512-eXvmitT1Hmr6d8n9r5HVjTuSgdHtxFSyXh0c3rRnaTcJA0ef8JnI5LY0TJci/QHYLNJGaEMBZ5R4dEdB2d87Lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, "node_modules/@playwright/test": { "version": "1.62.1", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz", diff --git a/package.json b/package.json index 1b929469..bff43a6b 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "playwright": "1.62.1" }, "devDependencies": { + "@openai/codex": "0.153.0", "turbo": "^2.10.9" } } diff --git a/packages/visuals-react/src/captureBarrier.ts b/packages/visuals-react/src/captureBarrier.ts index a0f17775..59a575c4 100644 --- a/packages/visuals-react/src/captureBarrier.ts +++ b/packages/visuals-react/src/captureBarrier.ts @@ -44,6 +44,23 @@ const frame=()=>new Promise((resolve,reject)=>{ const timeout=setTimeout(()=>{cancelAnimationFrame(id);reject(new Error("Capture paint frame timed out; ensure the native window is visible"));},2000); const id=requestAnimationFrame(()=>{clearTimeout(timeout);resolve();}); }); +/** Window resizing and queued React effects may commit after native layout has + * acknowledged the viewport. Wait for a bounded quiet interval BEFORE freezing + * images and opening the pixel cut. Mutations during the cut still invalidate it. */ +async function settleRoots(roots:HTMLElement[],signal:AbortSignal){ + await new Promise((resolve,reject)=>{ + let quiet:ReturnType; + const observer=new MutationObserver(()=>arm()); + const finish=(error?:Error)=>{clearTimeout(quiet);clearTimeout(deadline);observer.disconnect();signal.removeEventListener('abort',abort);error?reject(error):resolve();}; + const abort=()=>finish(new Error('Capture preparation cancelled')); + const arm=()=>{clearTimeout(quiet);quiet=setTimeout(()=>finish(),250);}; + const deadline=setTimeout(()=>finish(new Error('Capture surface did not settle before pixel freeze')),2000); + signal.addEventListener('abort',abort,{once:true}); + if(signal.aborted){abort();return;} + for(const root of roots)observer.observe(root,{subtree:true,attributes:true,childList:true,characterData:true}); + arm(); + }); +} function release(){ if(!active)return; active.closed=true;active.observer?.disconnect(); @@ -97,6 +114,8 @@ function begin(visualId:string,revision:number,stateVersion:number){ await Promise.all(barrier.adapters.map(adapter=>adapter.prepare(barrier.abort.signal,{nativeSnapshotPaint}))); if(barrier.closed)return; await document.fonts.ready; + await settleRoots(roots,barrier.abort.signal); + if(barrier.closed)return; await Promise.all(roots.flatMap(root=>Array.from(root.querySelectorAll("img"))).map(async image=>{ if(!image.complete)await image.decode(); if(!image.naturalWidth)throw new Error("Capture contains an unavailable image"); diff --git a/packages/visuals-react/src/session.ts b/packages/visuals-react/src/session.ts index bc12d783..a604c765 100644 --- a/packages/visuals-react/src/session.ts +++ b/packages/visuals-react/src/session.ts @@ -31,8 +31,15 @@ export function useVisualSessionSnapshot() { * outside this hook. Unhosted fixtures retain ordinary local React behavior. */ export function useVisualState(id: string, initial: T | (() => T), options: Partial> = {}): [T, Dispatch>] { const client = useVisualSessionClient(); - const snapshot = useVisualSessionSnapshot(); const [fallback, setFallback] = useState(initial); + // A control subscribes to its value, not scene publications or unrelated + // controls. Otherwise read-only host updates re-render every input and React + // temporarily clears input names even while the presentation is unchanged. + const readValue = useCallback(() => { + const snapshot=client?.getSnapshot(); + return snapshot && Object.hasOwn(snapshot.state.values,id) ? snapshot.state.values[id] : fallback; + },[client,id,fallback]); + const value=useSyncExternalStore(client?.subscribe ?? emptySubscribe,readValue,readValue); const defaultValue = useRef(fallback); const controlRef = useRef(null); if (!controlRef.current || controlRef.current.id !== id) { @@ -50,7 +57,6 @@ export function useVisualState(id: string, initial: T | (() => T), options: P return JSON.parse(JSON.stringify(next ?? null)) as JsonValue; }).catch(() => {}); // The shared session surface displays persistence errors. }, [client, id]); - const value = snapshot && Object.hasOwn(snapshot.state.values, id) ? snapshot.state.values[id] : fallback; return [(value === null && defaultValue.current === undefined ? undefined : value) as T, set]; } diff --git a/packages/workshop-visuals/components/agent_trace.v1/EvidenceClient.tsx b/packages/workshop-visuals/components/agent_trace.v1/EvidenceClient.tsx index f1a9f0a4..e401a88a 100644 --- a/packages/workshop-visuals/components/agent_trace.v1/EvidenceClient.tsx +++ b/packages/workshop-visuals/components/agent_trace.v1/EvidenceClient.tsx @@ -1,12 +1,11 @@ import { useEffect, useState } from 'react'; import type { TraceSelector, TraceVisual } from './model.ts'; import type { TraceAnnotation } from './annotations.ts'; +import { evidenceEndpoint } from './evidenceEndpoint.ts'; export type EvidenceService = { url: string; capability: string }; function endpoint(service: EvidenceService, runId: string) { - // This capability is intentionally limited to the local demo evidence service. - if (service.url !== 'http://127.0.0.1:8118/annotations') throw new Error('Unsupported evidence service'); - return `${service.url}?run=${encodeURIComponent(runId)}`; + return evidenceEndpoint(service.url, runId); } export function useTraceEvidence(service: EvidenceService | undefined, runId: string, fallback?: TraceVisual) { const [state, setState] = useState<{ runId: string; projection: TraceVisual; digest: string } | null>(null); diff --git a/packages/workshop-visuals/components/agent_trace.v1/README.md b/packages/workshop-visuals/components/agent_trace.v1/README.md index bbea8907..3b672fac 100644 --- a/packages/workshop-visuals/components/agent_trace.v1/README.md +++ b/packages/workshop-visuals/components/agent_trace.v1/README.md @@ -20,6 +20,6 @@ The Codex view handles projected native app-server item payloads and existing pr RuneBench now registers its extension and supplies synchronized game context. `cursorMs` highlights recorded events and the Follow replay control governs scrolling. Common markers support failure/message/reward/tool/annotation navigation; unknown execution times remain unset. -`useTraceEvidence` and `AnnotationEditor` connect the local demo to Containers' append-only evidence store. The client intentionally accepts only the local demo endpoint at port 8118. Writes require its bound capability and an expected evidence digest. Production remote transport, text-selection annotation authoring, and live stream assembly remain separate work. +`useTraceEvidence` and `AnnotationEditor` connect the local demo to Containers' append-only evidence store. The client intentionally accepts only the exact loopback `/annotations` endpoints at ports 8118 (maintainer demo) and 8128 (portable RuneBench example). Writes require its bound capability and an expected evidence digest. Production remote transport, text-selection annotation authoring, and live stream assembly remain separate work. `craftaxTraceExtension` proves reuse on retained real frames/terminal summaries. It does not manufacture model/tool messages missing from that source. diff --git a/packages/workshop-visuals/components/agent_trace.v1/evidenceEndpoint.ts b/packages/workshop-visuals/components/agent_trace.v1/evidenceEndpoint.ts new file mode 100644 index 00000000..bc4c47f6 --- /dev/null +++ b/packages/workshop-visuals/components/agent_trace.v1/evidenceEndpoint.ts @@ -0,0 +1,8 @@ +/** Only the two local demo services may receive annotation capabilities. */ +export function evidenceEndpoint(serviceUrl: string, runId: string): string { + if (![ + 'http://127.0.0.1:8118/annotations', + 'http://127.0.0.1:8128/annotations', + ].includes(serviceUrl)) throw new Error('Unsupported evidence service'); + return `${serviceUrl}?run=${encodeURIComponent(runId)}`; +} diff --git a/resources/codex/LICENSE b/resources/codex/LICENSE new file mode 100644 index 00000000..4606e72e --- /dev/null +++ b/resources/codex/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2025 OpenAI + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/resources/codex/NOTICE b/resources/codex/NOTICE new file mode 100644 index 00000000..c137c5d1 --- /dev/null +++ b/resources/codex/NOTICE @@ -0,0 +1,6 @@ +OpenAI Codex +Copyright 2025 OpenAI + +This project includes code derived from [Ratatui](https://github.com/ratatui/ratatui), licensed under the MIT license. +Copyright (c) 2016-2022 Florian Dehau +Copyright (c) 2023-2025 The Ratatui Developers diff --git a/scripts/bootstrap-deps.sh b/scripts/bootstrap-deps.sh new file mode 100644 index 00000000..de186392 --- /dev/null +++ b/scripts/bootstrap-deps.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Invoked explicitly by install.sh --bootstrap, never by --check/--dry-run. +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +[[ "$(uname -s)" == Darwin && "$(uname -m)" == arm64 ]] || { + echo '[bootstrap] Requires an Apple Silicon Mac.' >&2; exit 1; +} +macos_major="$(sw_vers -productVersion | cut -d. -f1)" +[[ "$macos_major" =~ ^[0-9]+$ ]] && (( macos_major >= 14 )) || { + echo '[bootstrap] Requires macOS 14 or newer.' >&2; exit 1; +} +if ! xcode-select -p >/dev/null 2>&1; then + xcode-select --install || true + echo '[bootstrap] Finish the Apple command-line tools installer, then rerun the same command.' >&2 + exit 1 +fi +swift_major="$(swift --version 2>/dev/null | sed -nE 's/.*Swift version ([0-9]+).*/\1/p' | head -n 1)" || swift_major="" +[[ "$swift_major" =~ ^[0-9]+$ ]] && (( swift_major >= 6 )) || { + echo '[bootstrap] Update Apple command-line tools or select Xcode 16+ (Swift 6 required), then rerun. Apple installation/license prompts must be completed by you.' >&2 + exit 1 +} +source "$ROOT/scripts/local-toolchain-env.sh" +if ! command -v brew >/dev/null 2>&1; then + echo '[bootstrap] Installing Homebrew using its official interactive installer (may ask for administrator approval).' + installer="$(mktemp "${TMPDIR:-/tmp}/workshop-homebrew.XXXXXX")" + trap 'rm -f "$installer"' EXIT + curl --fail --show-error --silent --location https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh -o "$installer" + /bin/bash "$installer" + source "$ROOT/scripts/local-toolchain-env.sh" +fi +echo '[bootstrap] Installing build tools via Homebrew; existing formula installations are reused.' +brew install node@20 python@3.12 rustup uv jq ripgrep git +source "$ROOT/scripts/local-toolchain-env.sh" +# Install the repository's stable toolchain without changing the global default. +(cd "$ROOT" && rustup toolchain install stable --profile minimal) +echo '[bootstrap] Build tools ready. Continuing with locked npm dependencies.' diff --git a/scripts/build-tier.sh b/scripts/build-tier.sh index e028beb9..820bdfbd 100755 --- a/scripts/build-tier.sh +++ b/scripts/build-tier.sh @@ -30,8 +30,9 @@ python3 -c 'import sys; sys.exit(0 if sys.version_info >= (3, 11) else 1)' || { exit 1 } -# Shared packaged resources (same for every tier); stage once. Resolve the -# MLX release checkout by the catalog revision, not by a historical name. +# Shared packaged resources (same for every tier); stage once. +bash "$ROOT/scripts/stage-codex-runtime.sh" +# Resolve the MLX release checkout by the catalog revision, not a historical name. REPO_SIBLING_ROOT="$(dirname "$ROOT")" if [[ -z "${SYNTH_MLX_RL_PROJECT_ROOT:-}" ]]; then mlx_pin="$(rg -o 'MLX_RUNTIME_SOURCE_REVISION: &str = "([0-9a-f]{40})"' --replace '$1' -m1 \ diff --git a/scripts/install.sh b/scripts/install.sh index 40fd19f1..fb02d4ce 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -7,9 +7,10 @@ MODE="install" usage() { cat <<'EOF' -Usage: ./scripts/install.sh [--check | --dry-run] +Usage: ./scripts/install.sh [--bootstrap | --check | --dry-run] (no option) Check prerequisites, create .env if absent, and run npm ci + --bootstrap Install build tools (Homebrew/Rust), then perform normal installation --check Check prerequisites only; do not change the checkout --dry-run Check prerequisites and print planned changes without making them EOF @@ -45,12 +46,19 @@ require_major() { case "${1:-}" in "") ;; --check) MODE="check" ;; + --bootstrap) MODE="bootstrap" ;; --dry-run) MODE="dry-run" ;; --help|-h) usage; exit 0 ;; *) usage >&2; fail "Unknown option: $1" ;; esac [[ $# -le 1 ]] || { usage >&2; fail "Expected at most one option."; } +if [[ "$MODE" == bootstrap ]]; then + bash "$ROOT/scripts/bootstrap-deps.sh" +fi +source "$ROOT/scripts/local-toolchain-env.sh" +cd "$ROOT" + [[ "$(uname -s)" == "Darwin" ]] || fail "Workshop development currently supports macOS only." [[ "$(uname -m)" == "arm64" ]] || fail "Workshop development currently supports Apple Silicon (arm64) only." @@ -74,6 +82,7 @@ require_command node "Install Node.js 20 or newer." require_command npm "Install npm 10 or newer." require_command rustc "Install Rust from https://rustup.rs/." require_command cargo "Install Rust from https://rustup.rs/." +require_command uv "Run ./scripts/install.sh --bootstrap to install uv and the other build tools." require_command python3 "Install Python 3.11 or newer (required by runtime staging)." require_command jq "Install jq (for example: brew install jq)." require_command rg "Install ripgrep (for example: brew install ripgrep)." @@ -114,4 +123,4 @@ fi note "installing locked npm workspace dependencies" (cd "$ROOT" && npm ci) note "installation complete" -note "configure .env if needed, then launch with: npm run desktop:dev" +note "build and launch with: ./scripts/workshop.sh build-and-run" diff --git a/scripts/local-toolchain-env.sh b/scripts/local-toolchain-env.sh new file mode 100644 index 00000000..584dfc2b --- /dev/null +++ b/scripts/local-toolchain-env.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# Sourced by the local entrypoints; never edits the user's shell profile. +workshop_brew="$(command -v brew || true)" +if [[ -z "$workshop_brew" && -x /opt/homebrew/bin/brew ]]; then + workshop_brew=/opt/homebrew/bin/brew +fi +if [[ -n "$workshop_brew" ]]; then + workshop_prefix="$("$workshop_brew" --prefix)" + export PATH="$workshop_prefix/opt/node@20/bin:$workshop_prefix/opt/python@3.12/libexec/bin:$workshop_prefix/opt/rustup/bin:$workshop_prefix/bin:$PATH" +fi +unset workshop_brew workshop_prefix diff --git a/scripts/stage-codex-runtime.sh b/scripts/stage-codex-runtime.sh new file mode 100644 index 00000000..8bb08da1 --- /dev/null +++ b/scripts/stage-codex-runtime.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# npm ci verifies the lockfile integrity; stage its pinned native runtime. +set -euo pipefail +root="$(cd "$(dirname "$0")/.." && pwd)" +package="$root/node_modules/@openai/codex-darwin-arm64" +expected=0.153.0 +actual="$(node -p 'require(process.argv[1]).version' "$root/node_modules/@openai/codex/package.json")" +[[ "$actual" == "$expected" ]] || { echo "Expected Codex $expected; run npm ci." >&2; exit 1; } +vendor="$package/vendor/aarch64-apple-darwin" +[[ -x "$vendor/bin/codex" && -x "$vendor/codex-path/rg" ]] || { + echo 'Pinned native Codex package is missing. Run npm ci with optional dependencies enabled.' >&2; exit 1; +} +destination="$root/runtime-distributions/codex" +mkdir -p "$destination" +# Preserve the complete upstream layout, including code-mode host and zsh. +/usr/bin/ditto "$vendor" "$destination" +cp "$root/resources/codex/LICENSE" "$destination/LICENSE" +cp "$root/resources/codex/NOTICE" "$destination/NOTICE" +echo "[codex] staged native Codex $expected (no Node required in the installed app)" diff --git a/scripts/workshop.sh b/scripts/workshop.sh index 29859a87..bf0b05a7 100755 --- a/scripts/workshop.sh +++ b/scripts/workshop.sh @@ -6,8 +6,6 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd)" APP="Synth Workshop Local.app" BUNDLE_ROOT="${CARGO_TARGET_DIR:-$ROOT/apps/synth_desktop/src-tauri/target}/release/bundle" APP_PATH="$BUNDLE_ROOT/macos/$APP" -VERSION="$(node -p "require('$ROOT/apps/synth_desktop/package.json').version")" -DMG_PATH="$BUNDLE_ROOT/dmg/Synth Workshop Local_${VERSION}_aarch64.dmg" usage() { cat <<'EOF' @@ -28,7 +26,10 @@ fail() { } build() { + source "$ROOT/scripts/local-toolchain-env.sh" "$ROOT/scripts/install.sh" --check + VERSION="$(node -p 'require(process.argv[1]).version' "$ROOT/apps/synth_desktop/package.json")" + DMG_PATH="$BUNDLE_ROOT/dmg/Synth Workshop Local_${VERSION}_aarch64.dmg" command -v uv >/dev/null || fail "Install uv before building Workshop." [[ -d "$ROOT/node_modules" ]] || fail "Dependencies are not installed. Run: ./scripts/install.sh" printf '[workshop] building local app and DMG (no release credentials)\n'