rovecode 0.4.0-beta.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -69
- package/THIRD_PARTY_NOTICES.md +0 -44
- package/bin/rovecode.ts +21 -0
- package/package.json +16 -37
- package/src/account/keys.ts +97 -0
- package/src/account/login.ts +158 -0
- package/src/account/provision.ts +47 -0
- package/src/account/store.ts +63 -0
- package/src/acp/server.ts +373 -0
- package/src/cli/account-cmd.ts +116 -0
- package/src/cli/connect.ts +244 -0
- package/src/cli/context-cmd.ts +199 -0
- package/src/cli/dispatch.ts +109 -0
- package/src/cli/doctor.ts +324 -0
- package/src/cli/export.ts +278 -0
- package/src/cli/help.ts +240 -0
- package/src/cli/is-tui-invocation.ts +8 -0
- package/src/cli/main.ts +599 -0
- package/src/cli/market-cmd.ts +658 -0
- package/src/cli/mcp-market-cmd.ts +299 -0
- package/src/cli/output.ts +382 -0
- package/src/cli/repl.ts +172 -0
- package/src/cli/resume.ts +32 -0
- package/src/cli/run-limits.ts +78 -0
- package/src/cli/runtime.ts +792 -0
- package/src/cli/setup.ts +187 -0
- package/src/cli/update-cmd.ts +78 -0
- package/src/cli/workflow-cmd.ts +100 -0
- package/src/coding/checkpoints.ts +270 -0
- package/src/coding/diff.ts +136 -0
- package/src/coding/files.ts +339 -0
- package/src/coding/hashline.ts +319 -0
- package/src/coding/lsp.ts +406 -0
- package/src/coding/repomap-cache.ts +99 -0
- package/src/coding/repomap-files.ts +110 -0
- package/src/coding/repomap.ts +392 -0
- package/src/core/compaction.ts +399 -0
- package/src/core/config.ts +289 -0
- package/src/core/context-report.ts +228 -0
- package/src/core/context.ts +60 -0
- package/src/core/count-remote.ts +107 -0
- package/src/core/execpolicy-rules.ts +196 -0
- package/src/core/execpolicy.ts +385 -0
- package/src/core/executor.ts +397 -0
- package/src/core/guardrails.ts +400 -0
- package/src/core/hooks.ts +398 -0
- package/src/core/images.ts +230 -0
- package/src/core/intro.ts +236 -0
- package/src/core/loop.ts +621 -0
- package/src/core/modes.ts +372 -0
- package/src/core/orchestrator.ts +207 -0
- package/src/core/reflection.ts +165 -0
- package/src/core/sandbox-config.ts +167 -0
- package/src/core/session-images.ts +73 -0
- package/src/core/session.ts +398 -0
- package/src/core/settings.ts +98 -0
- package/src/core/stuck-detector.ts +273 -0
- package/src/core/tasks.ts +374 -0
- package/src/core/token-scale.ts +108 -0
- package/src/core/tool-output-budget.ts +166 -0
- package/src/core/tools.ts +288 -0
- package/src/core/types.ts +330 -0
- package/src/core/update-check.ts +171 -0
- package/src/core/update.ts +158 -0
- package/src/core/usage.ts +204 -0
- package/src/core/validate.ts +121 -0
- package/src/core/verify-gate.ts +159 -0
- package/src/core/verify.ts +237 -0
- package/src/core/voice.ts +158 -0
- package/src/core/win-job.ts +183 -0
- package/src/design/audit.ts +797 -0
- package/src/design/direction.ts +190 -0
- package/src/design/rules.ts +157 -0
- package/src/eval/bench.ts +150 -0
- package/src/eval/gauntlet-runner.ts +218 -0
- package/src/eval/gauntlet.ts +226 -0
- package/src/eval/grader.ts +186 -0
- package/src/eval/record.ts +202 -0
- package/src/eval/redact.ts +141 -0
- package/src/eval/replay.ts +147 -0
- package/src/eval/trajectory.ts +373 -0
- package/src/index.ts +17 -0
- package/src/market/catalogs/mcp-docs.json +111 -0
- package/src/market/catalogs/plugins.json +111 -0
- package/src/market/catalogs/skills.json +478 -0
- package/src/market/clone.ts +72 -0
- package/src/market/context-cost.ts +121 -0
- package/src/market/digest.ts +106 -0
- package/src/market/index.ts +22 -0
- package/src/market/install.ts +578 -0
- package/src/market/manifest.ts +187 -0
- package/src/market/prereq.ts +145 -0
- package/src/market/registry.ts +363 -0
- package/src/market/resolve.ts +111 -0
- package/src/market/types.ts +236 -0
- package/src/market/validate.ts +227 -0
- package/src/mcp/client.ts +431 -0
- package/src/mcp/config.ts +239 -0
- package/src/mcp/local-package.ts +211 -0
- package/src/mcp/market-catalog.ts +84 -0
- package/src/mcp/market-install.ts +289 -0
- package/src/mcp/market.ts +0 -0
- package/src/mcp/tools.ts +131 -0
- package/src/mcp/trust.ts +49 -0
- package/src/memory/blocks.ts +175 -0
- package/src/memory/recall.ts +355 -0
- package/src/memory/store.ts +105 -0
- package/src/memory/tools.ts +99 -0
- package/src/plugins/cli.ts +123 -0
- package/src/plugins/discover.ts +108 -0
- package/src/plugins/index.ts +50 -0
- package/src/plugins/init.ts +140 -0
- package/src/plugins/install.ts +184 -0
- package/src/plugins/load.ts +149 -0
- package/src/plugins/manifest.ts +106 -0
- package/src/plugins/state.ts +83 -0
- package/src/providers/auth.ts +293 -0
- package/src/providers/cache.ts +223 -0
- package/src/providers/catalog-local.ts +160 -0
- package/src/providers/catalog.ts +408 -0
- package/src/providers/middleware-context.ts +86 -0
- package/src/providers/middleware.ts +373 -0
- package/src/providers/profile-glm53.ts +111 -0
- package/src/providers/profile-sonnet5-persona.ts +65 -0
- package/src/providers/profile-sonnet5-voice.ts +23 -0
- package/src/providers/profiles.ts +156 -0
- package/src/providers/provider-config.ts +311 -0
- package/src/providers/registry.ts +302 -0
- package/src/providers/response-validation.ts +80 -0
- package/src/providers/retry.ts +234 -0
- package/src/providers/router.ts +294 -0
- package/src/providers/sse.ts +26 -0
- package/src/providers/stream-errors.ts +117 -0
- package/src/providers/stream.ts +569 -0
- package/src/providers/thinking.ts +189 -0
- package/src/providers/wire-messages.ts +129 -0
- package/src/sdk/client.ts +225 -0
- package/src/sdk/index.ts +3 -0
- package/src/server/dashboard.ts +144 -0
- package/src/server/http.ts +343 -0
- package/src/server/openapi.ts +246 -0
- package/src/sextant/card-hits.ts +102 -0
- package/src/sextant/card-keys.ts +55 -0
- package/src/sextant/context-source.ts +157 -0
- package/src/sextant/draw-agents.ts +273 -0
- package/src/sextant/draw-code.ts +388 -0
- package/src/sextant/draw-context.ts +222 -0
- package/src/sextant/draw-frame.ts +164 -0
- package/src/sextant/draw-market.ts +573 -0
- package/src/sextant/draw-messages.ts +386 -0
- package/src/sextant/draw-pet.ts +230 -0
- package/src/sextant/draw-plan.ts +159 -0
- package/src/sextant/draw-tabs.ts +85 -0
- package/src/sextant/draw-util.ts +65 -0
- package/src/sextant/engine.ts +230 -0
- package/src/sextant/frame-hits.ts +25 -0
- package/src/sextant/frame.ts +101 -0
- package/src/sextant/git-status.ts +197 -0
- package/src/sextant/grid.ts +59 -0
- package/src/sextant/input.ts +119 -0
- package/src/sextant/keys.ts +488 -0
- package/src/sextant/layout.ts +86 -0
- package/src/sextant/local-commands.ts +156 -0
- package/src/sextant/market-source.ts +287 -0
- package/src/sextant/mentions.ts +141 -0
- package/src/sextant/message-hits.ts +26 -0
- package/src/sextant/model.ts +387 -0
- package/src/sextant/overlays.ts +451 -0
- package/src/sextant/panel-hits.ts +38 -0
- package/src/sextant/pet.ts +399 -0
- package/src/sextant/screen.ts +324 -0
- package/src/sextant/scroll-hits.ts +66 -0
- package/src/sextant/scrollbar.ts +82 -0
- package/src/sextant/selection.ts +123 -0
- package/src/sextant/sextant-bridge.ts +174 -0
- package/src/sextant/sextant-cards.ts +142 -0
- package/src/sextant/sextant-diff-base.ts +63 -0
- package/src/sextant/sextant-files.ts +154 -0
- package/src/sextant/sextant-frame-loop.ts +314 -0
- package/src/sextant/sextant-renderer.ts +478 -0
- package/src/sextant/sextant-repo.ts +131 -0
- package/src/sextant/theme.ts +66 -0
- package/src/sextant/tool-rows.ts +189 -0
- package/src/sextant/types.ts +473 -0
- package/src/skills/index.ts +306 -0
- package/src/skills/tools.ts +69 -0
- package/src/skills/versioned.ts +227 -0
- package/src/telemetry/otel.ts +353 -0
- package/src/telemetry/otlp.ts +68 -0
- package/src/tools/ask-user.ts +156 -0
- package/src/tools/design.ts +151 -0
- package/src/tools/evalcell.ts +338 -0
- package/src/tools/html-text.ts +139 -0
- package/src/tools/provider.ts +149 -0
- package/src/tools/task.ts +216 -0
- package/src/tools/todo.ts +320 -0
- package/src/tools/webfetch.ts +331 -0
- package/src/tui/app.ts +608 -0
- package/src/tui/attach.ts +127 -0
- package/src/tui/checkpoints-cmd.ts +70 -0
- package/src/tui/clipboard-image.ts +81 -0
- package/src/tui/commands.ts +277 -0
- package/src/tui/cost.ts +108 -0
- package/src/tui/info-cmd.ts +144 -0
- package/src/tui/mcp-cmd.ts +128 -0
- package/src/tui/modes-cmd.ts +45 -0
- package/src/tui/overlays.ts +97 -0
- package/src/tui/pi-renderer.ts +424 -0
- package/src/tui/providers-cmd.ts +366 -0
- package/src/tui/renderer.ts +101 -0
- package/src/tui/replay-marker.ts +29 -0
- package/src/tui/session-cmd.ts +146 -0
- package/src/tui/sextant-attach.ts +68 -0
- package/src/tui/sextant-io.ts +184 -0
- package/src/tui/sextant-smoke.ts +110 -0
- package/src/tui/smoke.ts +72 -0
- package/src/tui/theme.ts +59 -0
- package/src/tui/todo-label.ts +7 -0
- package/src/workflow/engine.ts +266 -0
- package/tsconfig.json +30 -0
- package/vendor/pi-tui/LICENSE +21 -0
- package/vendor/pi-tui/PATCHES.md +12 -0
- package/vendor/pi-tui/PROVENANCE.md +12 -0
- package/vendor/pi-tui/README.upstream.md +854 -0
- package/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
- package/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
- package/vendor/pi-tui/src/alt-screen-search.ts +158 -0
- package/vendor/pi-tui/src/autocomplete.ts +827 -0
- package/vendor/pi-tui/src/components/alt-screen-flash.ts +52 -0
- package/vendor/pi-tui/src/components/box.ts +138 -0
- package/vendor/pi-tui/src/components/cancellable-loader.ts +41 -0
- package/vendor/pi-tui/src/components/editor.ts +2364 -0
- package/vendor/pi-tui/src/components/h-stack.ts +45 -0
- package/vendor/pi-tui/src/components/image.ts +128 -0
- package/vendor/pi-tui/src/components/input.ts +448 -0
- package/vendor/pi-tui/src/components/loader.ts +93 -0
- package/vendor/pi-tui/src/components/markdown.ts +1016 -0
- package/vendor/pi-tui/src/components/scroll-view.ts +217 -0
- package/vendor/pi-tui/src/components/select-list.ts +230 -0
- package/vendor/pi-tui/src/components/settings-list.ts +277 -0
- package/vendor/pi-tui/src/components/spacer.ts +29 -0
- package/vendor/pi-tui/src/components/stack.ts +155 -0
- package/vendor/pi-tui/src/components/text.ts +108 -0
- package/vendor/pi-tui/src/components/truncated-text.ts +66 -0
- package/vendor/pi-tui/src/components/v-stack.ts +34 -0
- package/vendor/pi-tui/src/editor-component.ts +75 -0
- package/vendor/pi-tui/src/fuzzy.ts +138 -0
- package/vendor/pi-tui/src/index.ts +149 -0
- package/vendor/pi-tui/src/keybindings.ts +321 -0
- package/vendor/pi-tui/src/keys.ts +1402 -0
- package/vendor/pi-tui/src/kill-ring.ts +47 -0
- package/vendor/pi-tui/src/latex.ts +1381 -0
- package/vendor/pi-tui/src/layout-node.ts +52 -0
- package/vendor/pi-tui/src/layout.ts +411 -0
- package/vendor/pi-tui/src/native-modifiers.ts +60 -0
- package/vendor/pi-tui/src/native-module-path.ts +32 -0
- package/vendor/pi-tui/src/stdin-buffer.ts +445 -0
- package/vendor/pi-tui/src/terminal-colors.ts +74 -0
- package/vendor/pi-tui/src/terminal-image.ts +701 -0
- package/vendor/pi-tui/src/terminal.ts +554 -0
- package/vendor/pi-tui/src/tui-alt-screen.ts +1379 -0
- package/vendor/pi-tui/src/tui-main-screen.ts +655 -0
- package/vendor/pi-tui/src/tui.ts +1264 -0
- package/vendor/pi-tui/src/undo-stack.ts +29 -0
- package/vendor/pi-tui/src/utils.ts +1327 -0
- package/vendor/pi-tui/src/word-navigation.ts +118 -0
- package/vendor/pi-tui/test/test-themes.ts +39 -0
- package/vendor/pi-tui/test/virtual-terminal.ts +219 -0
- package/CHANGELOG.md +0 -512
- package/bin/rovecode.js +0 -24
- package/dist/cli/app-dybnr56b.js +0 -2
- package/dist/cli/ask-user-p8hq4xgj.js +0 -2
- package/dist/cli/auth-login-ewpgw5sm.js +0 -2
- package/dist/cli/auth-m8p9grty.js +0 -2
- package/dist/cli/bench-xv3ypwev.js +0 -9
- package/dist/cli/catalog-737wb2s0.js +0 -2
- package/dist/cli/cli-arhg40m0.js +0 -2
- package/dist/cli/client-cf2pxx8q.js +0 -2
- package/dist/cli/commands-3p7e4xxs.js +0 -2
- package/dist/cli/connect-3q93d7cb.js +0 -2
- package/dist/cli/context-cmd-eqxmxhzq.js +0 -2
- package/dist/cli/context-report-hbw9zfes.js +0 -2
- package/dist/cli/count-remote-mby98cd0.js +0 -2
- package/dist/cli/design-122y0axd.js +0 -2
- package/dist/cli/dispatch-b4egzvvh.js +0 -2
- package/dist/cli/doctor-x4jkv72e.js +0 -3
- package/dist/cli/executor-ftvg6tsy.js +0 -2
- package/dist/cli/export-pdgdhkch.js +0 -2
- package/dist/cli/files-cez9a96p.js +0 -2
- package/dist/cli/gauntlet-r3xxaszc.js +0 -2
- package/dist/cli/gauntlet-runner-r515m7kk.js +0 -10
- package/dist/cli/gauntlet-wave3-bnkjk2v2.js +0 -5
- package/dist/cli/gauntlet-wave4-acs9s60q.js +0 -14
- package/dist/cli/hashline-ewg5hbe3.js +0 -2
- package/dist/cli/http-n0kehsk8.js +0 -5
- package/dist/cli/index-z5qt1s76.js +0 -2
- package/dist/cli/install-80mp63kx.js +0 -2
- package/dist/cli/loop-12twjcat.js +0 -2
- package/dist/cli/main-01pv9206.js +0 -4
- package/dist/cli/main-0jys2ccn.js +0 -3
- package/dist/cli/main-1ztz6fkj.js +0 -10
- package/dist/cli/main-23q7cmww.js +0 -9
- package/dist/cli/main-2rzbexn2.js +0 -3
- package/dist/cli/main-2wyax8k9.js +0 -9
- package/dist/cli/main-2yeveeve.js +0 -6
- package/dist/cli/main-2z3dek0b.js +0 -3
- package/dist/cli/main-2zgsknth.js +0 -3
- package/dist/cli/main-45ejth3a.js +0 -4
- package/dist/cli/main-45rn3trk.js +0 -22
- package/dist/cli/main-4p4e2w7x.js +0 -4
- package/dist/cli/main-4y0tnfpa.js +0 -16
- package/dist/cli/main-5py0rkmc.js +0 -4
- package/dist/cli/main-6dtqmbt6.js +0 -7
- package/dist/cli/main-6h9x282m.js +0 -4
- package/dist/cli/main-6vjeds42.js +0 -3
- package/dist/cli/main-78gq4bt9.js +0 -6
- package/dist/cli/main-7jd5vh3x.js +0 -4
- package/dist/cli/main-7kt6r53y.js +0 -4
- package/dist/cli/main-8c1tbazx.js +0 -58
- package/dist/cli/main-9a9rnh47.js +0 -19
- package/dist/cli/main-9ht36z12.js +0 -3
- package/dist/cli/main-a2yfvcy9.js +0 -7
- package/dist/cli/main-a3f51n0x.js +0 -5
- package/dist/cli/main-b8zq261k.js +0 -3
- package/dist/cli/main-bxtvnf6d.js +0 -13
- package/dist/cli/main-edxc3yzt.js +0 -4
- package/dist/cli/main-evgz4mp5.js +0 -21
- package/dist/cli/main-f33fc5je.js +0 -9
- package/dist/cli/main-fvnpq46y.js +0 -12
- package/dist/cli/main-gbbty4d4.js +0 -3
- package/dist/cli/main-gth53dnt.js +0 -25
- package/dist/cli/main-hqbz10aw.js +0 -9
- package/dist/cli/main-hrrvcfan.js +0 -38
- package/dist/cli/main-hzwtsb2m.js +0 -5
- package/dist/cli/main-j7ttv0sd.js +0 -34
- package/dist/cli/main-jak598k9.js +0 -5
- package/dist/cli/main-kba6zeyd.js +0 -6
- package/dist/cli/main-kwwsz6rq.js +0 -3
- package/dist/cli/main-m8vm17zq.js +0 -3
- package/dist/cli/main-mg4f96e1.js +0 -3
- package/dist/cli/main-mg9b20ac.js +0 -18
- package/dist/cli/main-mgb9ccnx.js +0 -3
- package/dist/cli/main-mjt2p7aj.js +0 -3
- package/dist/cli/main-n6qrdbmy.js +0 -3
- package/dist/cli/main-na7wse0x.js +0 -5
- package/dist/cli/main-nqveez48.js +0 -4
- package/dist/cli/main-ntqef02r.js +0 -10
- package/dist/cli/main-nvc3yjay.js +0 -136
- package/dist/cli/main-p0cfn6nr.js +0 -16
- package/dist/cli/main-qj2djy17.js +0 -19
- package/dist/cli/main-qsevpgsv.js +0 -3
- package/dist/cli/main-qvarybsp.js +0 -3
- package/dist/cli/main-rebtt91r.js +0 -5
- package/dist/cli/main-rpg7h8mb.js +0 -3
- package/dist/cli/main-rsy72qmw.js +0 -15
- package/dist/cli/main-rvetps99.js +0 -18
- package/dist/cli/main-s4bb0jav.js +0 -3
- package/dist/cli/main-s9v8k74e.js +0 -3
- package/dist/cli/main-tjvwmscs.js +0 -3
- package/dist/cli/main-tkgarpjj.js +0 -4
- package/dist/cli/main-v8y60bb2.js +0 -3
- package/dist/cli/main-vhrrq337.js +0 -3
- package/dist/cli/main-vp2dfb7s.js +0 -4
- package/dist/cli/main-vqbr22sz.js +0 -8
- package/dist/cli/main-vxnwe5xx.js +0 -18
- package/dist/cli/main-wgph00xf.js +0 -5
- package/dist/cli/main-wk2csfnj.js +0 -5
- package/dist/cli/main-wm997zjx.js +0 -3
- package/dist/cli/main-wpkyraxh.js +0 -3
- package/dist/cli/main-wqt32p5x.js +0 -4
- package/dist/cli/main-x9ct6y1a.js +0 -3
- package/dist/cli/main-xfekqh9m.js +0 -7
- package/dist/cli/main-xt9zc3n6.js +0 -7
- package/dist/cli/main-xx2z3zh5.js +0 -4
- package/dist/cli/main-y5c82rxr.js +0 -3
- package/dist/cli/main-yrjt2sqt.js +0 -14
- package/dist/cli/main-ys6zj3yr.js +0 -3
- package/dist/cli/main-ywbxshqc.js +0 -8
- package/dist/cli/main-z13755t8.js +0 -25
- package/dist/cli/main-zc7pyrbj.js +0 -4
- package/dist/cli/main.js +0 -279
- package/dist/cli/market-cmd-bm5xvn9f.js +0 -5
- package/dist/cli/mcp-login-bthtfpt7.js +0 -2
- package/dist/cli/mcp-market-cmd-mbeshfyd.js +0 -2
- package/dist/cli/notify-54v5z9dz.js +0 -2
- package/dist/cli/oauth-g5gme95c.js +0 -2
- package/dist/cli/output-satndjap.js +0 -16
- package/dist/cli/profiles-sfhpbq3m.js +0 -2
- package/dist/cli/provider-config-hv3xtdt4.js +0 -2
- package/dist/cli/provider-kwzq6g84.js +0 -2
- package/dist/cli/registry-fh0hdnyn.js +0 -2
- package/dist/cli/registry-y1y8e94r.js +0 -2
- package/dist/cli/repl-t4z03mqq.js +0 -11
- package/dist/cli/resume-fqt4chg8.js +0 -2
- package/dist/cli/run-flags-rysbag9t.js +0 -2
- package/dist/cli/runtime-j19fjbsa.js +0 -2
- package/dist/cli/sandbox-config-g4qxd7y5.js +0 -2
- package/dist/cli/server-r0b6bksk.js +0 -5
- package/dist/cli/session-arg-txmn5g4x.js +0 -2
- package/dist/cli/session-ed250d9j.js +0 -2
- package/dist/cli/sessions-cmd-adw7svfn.js +0 -7
- package/dist/cli/settings-y9rzcqx8.js +0 -2
- package/dist/cli/setup-jmbr11j0.js +0 -2
- package/dist/cli/sextant-smoke-tcth0vea.js +0 -5
- package/dist/cli/skills-cmd-zbdy99v6.js +0 -2
- package/dist/cli/smoke-1bg937kx.js +0 -8
- package/dist/cli/start-chat-p01cdks3.js +0 -12
- package/dist/cli/stream-4wmyaypz.js +0 -2
- package/dist/cli/task-eg4s093s.js +0 -2
- package/dist/cli/tasks-12v9rr9k.js +0 -2
- package/dist/cli/thinking-a5ngvqyh.js +0 -2
- package/dist/cli/todo-1wxpcecx.js +0 -2
- package/dist/cli/tools-2ftsya7w.js +0 -2
- package/dist/cli/tools-x1tj4fxm.js +0 -2
- package/dist/cli/trust-cmd-hccxehzb.js +0 -2
- package/dist/cli/update-check-ygt3vd7m.js +0 -2
- package/dist/cli/update-cmd-v23qhr8c.js +0 -2
- package/dist/cli/voice-g1gtck92.js +0 -2
- package/dist/cli/webfetch-0nnrjgb5.js +0 -2
- package/dist/cli/websearch-f0vr2p7d.js +0 -2
- package/dist/cli/workspace-9rq1w4ta.js +0 -2
- package/dist/lib/index.js +0 -62
- package/dist/lib/models-index.json +0 -1
- package/dist/lib/plugins.js +0 -6
- package/dist/lib/providers.js +0 -17
- package/dist/lib/public-api.js +0 -20
- package/dist/rovecode.exe +0 -4
- /package/{dist/cli → src/providers}/models-index.json +0 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
/** Executor sandbox ladder (PORT #10): ONE seam for shell execution with
|
|
2
|
+
* selectable rungs — `direct` | `wsl` | `docker`.
|
|
3
|
+
*
|
|
4
|
+
* Pattern source: openai/codex, Apache-2.0, Copyright 2025 OpenAI (snapshot
|
|
5
|
+
* research/source_snapshots/openai-codex @ 379d50be). Ported at pattern
|
|
6
|
+
* level, no code copied:
|
|
7
|
+
* - tiered execution selected per platform: SandboxType +
|
|
8
|
+
* get_platform_sandbox(), codex-rs/sandboxing/src/manager.rs:37,62;
|
|
9
|
+
* - availability PROBED at runtime by a trial spawn THROUGH the wrapper
|
|
10
|
+
* itself — the probe argv is the rung's own run shape with `true` as the
|
|
11
|
+
* command, exactly as codex probes bubblewrap by running `bwrap … /bin/true`
|
|
12
|
+
* (codex-rs/sandboxing/src/bwrap.rs:74). Probing anything weaker lies:
|
|
13
|
+
* `wsl.exe --status` exits 0 on a machine whose default distro has no bash
|
|
14
|
+
* (e.g. docker-desktop), and `docker version` proves a daemon but not that
|
|
15
|
+
* the image exists or contains bash;
|
|
16
|
+
* - probes are TIME-BOUNDED at codex's own 500ms cap (bwrap.rs:36,67);
|
|
17
|
+
* a probe that cannot answer in time IS unavailable right now;
|
|
18
|
+
* - an explicitly requested but unprovidable tier is a HARD ERROR
|
|
19
|
+
* (SandboxTransformError::{SeatbeltUnavailable,…}, manager.rs:203-222,410).
|
|
20
|
+
* Deliberately NOT ported: codex's silent `unwrap_or(SandboxType::None)`
|
|
21
|
+
* degrade (manager.rs:305) — codex recovers via its approval layer
|
|
22
|
+
* (core/src/exec_policy.rs:753-771); rovecode has no compensator at this seam,
|
|
23
|
+
* so per the bar an unavailable rung is a loud RungUnavailableError and
|
|
24
|
+
* NEVER a silent fallback DOWN the ladder. The docker rung follows the
|
|
25
|
+
* OpenHands runtime-boundary shape (execute inside a container with the
|
|
26
|
+
* workspace mounted) — OpenHands is not snapshotted; provenance gap.
|
|
27
|
+
*
|
|
28
|
+
* Honesty note (matches README): every rung is DELEGATION, not a native
|
|
29
|
+
* sandbox. `direct` is today's blocklist-only behavior; `wsl`/`docker`
|
|
30
|
+
* isolate only as well as the wrapped runtime does. */
|
|
31
|
+
|
|
32
|
+
import { existsSync } from "node:fs";
|
|
33
|
+
import { createWinJob, type WinJob } from "./win-job.ts";
|
|
34
|
+
|
|
35
|
+
// ---------- Ladder ----------
|
|
36
|
+
|
|
37
|
+
export type Rung = "direct" | "wsl" | "docker";
|
|
38
|
+
|
|
39
|
+
/** Ladder order, weakest isolation first. Order is informational only —
|
|
40
|
+
* selection is always explicit, never walked automatically. */
|
|
41
|
+
export const RUNGS: readonly Rung[] = ["direct", "wsl", "docker"];
|
|
42
|
+
|
|
43
|
+
export interface ExecResult { code: number; text: string }
|
|
44
|
+
|
|
45
|
+
export interface Executor {
|
|
46
|
+
readonly rung: Rung;
|
|
47
|
+
/** Run `cmd` through bash in `cwd`. Result matches today's bashTool
|
|
48
|
+
* internals: exit code + stdout (+ stderr section), sliced to 10k chars. */
|
|
49
|
+
run(cmd: string, cwd: string, signal?: AbortSignal): Promise<ExecResult>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------- Raw process running (injectable so tests cover probe paths) ----------
|
|
53
|
+
|
|
54
|
+
export interface RawResult {
|
|
55
|
+
code: number; stdout: string; stderr: string;
|
|
56
|
+
/** Windows runs with a signal: "job" (Job Object tree kill + taskkill sweep)
|
|
57
|
+
* or the fail-safe "taskkill-only" (no bun:ffi/kernel32, or not assignable) */
|
|
58
|
+
treeKill?: "job" | "taskkill-only";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type SpawnRunner = (
|
|
62
|
+
argv: readonly string[],
|
|
63
|
+
opts: { cwd?: string; signal?: AbortSignal },
|
|
64
|
+
) => Promise<RawResult>;
|
|
65
|
+
|
|
66
|
+
/** Post-abort grace (port #21 HIGH-1b): the runner waits this long for pipes +
|
|
67
|
+
* exit, then settles with the bytes so far + ABORT_TRUNCATED_MARKER, code 143.
|
|
68
|
+
* Only a process outside the job can hold a pipe end past the kill (the
|
|
69
|
+
* taskkill-only fallback measured an msys `sleep` holding stdout — and the
|
|
70
|
+
* runner promise — for its whole duration). REF'D timer, deliberately: Bun's
|
|
71
|
+
* AbortSignal.timeout timers are not, and an idle loop never fires them. */
|
|
72
|
+
export const ABORT_GRACE_MS = 500;
|
|
73
|
+
export const ABORT_TRUNCATED_MARKER = "[output truncated: process tree terminated on abort]";
|
|
74
|
+
|
|
75
|
+
/** Incremental pipe reader: bytes so far are readable at any moment, and the
|
|
76
|
+
* read is cancellable (drops our end) when an orphan holds the other.
|
|
77
|
+
* Decodes like Response.text(): UTF-8, leading BOM stripped. */
|
|
78
|
+
function collect(stream: ReadableStream<Uint8Array>): { done: Promise<void>; text(): string; cancel(): void } {
|
|
79
|
+
const chunks: Uint8Array[] = [];
|
|
80
|
+
const reader = stream.getReader();
|
|
81
|
+
const done = (async () => {
|
|
82
|
+
for (let c = await reader.read(); !c.done; c = await reader.read()) if (c.value) chunks.push(c.value);
|
|
83
|
+
})().catch(() => {});
|
|
84
|
+
return { done, text: () => new TextDecoder().decode(Buffer.concat(chunks)), cancel: () => { reader.cancel().catch(() => {}); } };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Default runner: Bun.spawn, stdout/stderr piped. A spawn failure (missing
|
|
88
|
+
* binary) is returned as code -1 with the message in stderr, so probes can
|
|
89
|
+
* report "not installed" instead of crashing.
|
|
90
|
+
* Windows abort = TREE kill (port #21). Bun's own signal handling
|
|
91
|
+
* TerminateProcess-es only the DIRECT child, and `taskkill /T` walks live
|
|
92
|
+
* parent links that msys2's exec breaks (win-job.ts) — measured with taskkill
|
|
93
|
+
* alone, the `sleep` inside `a; sleep N; b`, `bash -c 'sleep N'` and
|
|
94
|
+
* `sleep N & sleep N & wait` survived 3/3 and held stdout so the runner hung
|
|
95
|
+
* 3/3. So on Windows the signal is NOT handed to Bun.spawn: the launcher goes
|
|
96
|
+
* into a Job Object right after spawn; abort fires TerminateJobObject (every
|
|
97
|
+
* descendant, connected or not) plus `taskkill /T /F` as a sweep; a box
|
|
98
|
+
* without job objects keeps taskkill alone and says so in `treeKill`.
|
|
99
|
+
* POSIX keeps Bun's signal passthrough: SIGTERM reaches the shell, which never
|
|
100
|
+
* runs its next statement — but NOT a forked grandchild (`sleep`/`npm` inside
|
|
101
|
+
* a compound command), which is orphaned and finishes on its own; only a bare
|
|
102
|
+
* command, exec'd without a fork, dies with the shell. The process-group kill
|
|
103
|
+
* that would close this (detached spawn + kill(-pgid)) is not implemented: it
|
|
104
|
+
* cannot be exercised on the Windows-only reference box. */
|
|
105
|
+
export const bunRunner: SpawnRunner = async (argv, opts) => {
|
|
106
|
+
const signal = opts.signal;
|
|
107
|
+
const treeKill = process.platform === "win32" && signal !== undefined;
|
|
108
|
+
// pre-aborted (G4 pin): never spawn at all — a kill would race a fast
|
|
109
|
+
// command and lose; 143 matches the killed-at-spawn shape the pin measured
|
|
110
|
+
if (treeKill && signal?.aborted) return { code: 143, stdout: "", stderr: "aborted before spawn" };
|
|
111
|
+
let onAbort: (() => void) | undefined;
|
|
112
|
+
let job: WinJob | null = null;
|
|
113
|
+
let killed = false;
|
|
114
|
+
let graceTimer: ReturnType<typeof setTimeout> | undefined;
|
|
115
|
+
try {
|
|
116
|
+
const proc = Bun.spawn([...argv], { cwd: opts.cwd, signal: treeKill ? undefined : signal, stdout: "pipe", stderr: "pipe" });
|
|
117
|
+
const out = collect(proc.stdout), err = collect(proc.stderr);
|
|
118
|
+
const finished = Promise.all([out.done, err.done, proc.exited]).then(() => "done" as const);
|
|
119
|
+
let graceUp!: () => void;
|
|
120
|
+
const grace = new Promise<"grace">((r) => { graceUp = () => r("grace"); });
|
|
121
|
+
if (signal) {
|
|
122
|
+
if (treeKill) { job = createWinJob(); if (job && !job.assign(proc.pid)) job = null; } // null → taskkill-only
|
|
123
|
+
onAbort = () => {
|
|
124
|
+
// a dead launcher may have left a child holding a pipe end (`sleep N & echo x`): the job
|
|
125
|
+
// is still armed (the RUNNER has not settled) — terminate regardless; taskkill only a LIVE pid (reuse)
|
|
126
|
+
const launcherAlive = proc.exitCode === null;
|
|
127
|
+
killed = treeKill;
|
|
128
|
+
if (treeKill) {
|
|
129
|
+
job?.terminate();
|
|
130
|
+
if (launcherAlive) { try { Bun.spawn(["taskkill", "/T", "/F", "/PID", String(proc.pid)], { stdout: "ignore", stderr: "ignore" }); } catch { /* best-effort */ } }
|
|
131
|
+
}
|
|
132
|
+
graceTimer = setTimeout(graceUp, ABORT_GRACE_MS);
|
|
133
|
+
};
|
|
134
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
135
|
+
void finished.then(() => { if (onAbort) signal.removeEventListener("abort", onAbort); }, () => {});
|
|
136
|
+
}
|
|
137
|
+
const won = await Promise.race([finished, grace]);
|
|
138
|
+
if (won === "grace") { out.cancel(); err.cancel(); }
|
|
139
|
+
const code = (killed || won === "grace") ? 143 : await proc.exited;
|
|
140
|
+
let stderr = err.text();
|
|
141
|
+
if (won === "grace") stderr += (stderr ? "\n" : "") + ABORT_TRUNCATED_MARKER;
|
|
142
|
+
const r: RawResult = { code, stdout: out.text(), stderr };
|
|
143
|
+
if (treeKill) r.treeKill = job ? "job" : "taskkill-only";
|
|
144
|
+
return r;
|
|
145
|
+
} catch (e) {
|
|
146
|
+
return { code: -1, stdout: "", stderr: `spawn failed: ${e instanceof Error ? e.message : String(e)}` };
|
|
147
|
+
} finally {
|
|
148
|
+
clearTimeout(graceTimer);
|
|
149
|
+
if (killed) job?.terminate(); else job?.release(); // both no-ops once the abort path closed the job
|
|
150
|
+
// the signal is the RUN's (long-lived): drop this spawn's listener or a
|
|
151
|
+
// multi-bash run accumulates one dead closure per command
|
|
152
|
+
if (onAbort) signal?.removeEventListener("abort", onAbort);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/** Byte-compatible with hashline runOnce text assembly: stdout, then an
|
|
157
|
+
* optional `\nstderr:\n…` section, sliced to 10k chars. */
|
|
158
|
+
function assemble(stdout: string, stderr: string): string {
|
|
159
|
+
return (stdout + (stderr ? `\nstderr:\n${stderr}` : "")).slice(0, 10_000);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Byte-compatible with hashline bashBin(): on Windows prefer Git bash —
|
|
163
|
+
* System32 bash.exe is the WSL relay and breaks on non-WSL machines. */
|
|
164
|
+
let bashCache: string | null = null;
|
|
165
|
+
function bashBin(): string {
|
|
166
|
+
if (bashCache !== null) return bashCache;
|
|
167
|
+
if (process.platform !== "win32") return (bashCache = "bash");
|
|
168
|
+
const git = "C:/Program Files/Git/bin/bash.exe";
|
|
169
|
+
bashCache = existsSync(git) ? git : "bash";
|
|
170
|
+
return bashCache;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ---------- Probes (availability is checked, never assumed) ----------
|
|
174
|
+
|
|
175
|
+
export interface RungProbe { rung: Rung; available: boolean; detail: string }
|
|
176
|
+
|
|
177
|
+
/** Probe deadline: 500ms, upstream's own bwrap cap (bwrap.rs:36,67). Known
|
|
178
|
+
* tradeoff, accepted deliberately: a COLD `wsl.exe --exec bash -c true`
|
|
179
|
+
* (utility-VM boot) measured 2842ms on the reference machine vs 197ms warm —
|
|
180
|
+
* a cold probe times out and the rung reports unavailable with a detail that
|
|
181
|
+
* says to warm it and retry. That is the contract: timeout ⇒ unavailable NOW,
|
|
182
|
+
* never a probe that hangs the session. */
|
|
183
|
+
export const PROBE_TIMEOUT_MS = 500;
|
|
184
|
+
|
|
185
|
+
export interface ProbeOptions {
|
|
186
|
+
/** docker rung: image the trial (and later every command) runs in */
|
|
187
|
+
dockerImage?: string;
|
|
188
|
+
/** probe deadline override (tests); defaults to PROBE_TIMEOUT_MS */
|
|
189
|
+
timeoutMs?: number;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** wsl.exe emits UTF-16LE; drop NULs before quoting output in a detail. */
|
|
193
|
+
function probeText(s: string): string {
|
|
194
|
+
return s.replace(/\u0000/g, "").trim().slice(0, 200);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** One bounded trial spawn. The AbortSignal kills the trial process at the
|
|
198
|
+
* deadline; a separate REF'D setTimeout resolves the race. Two reasons the
|
|
199
|
+
* race must NOT wait on the signal's own 'abort' event: (a) the killed
|
|
200
|
+
* wrapper's children can keep the stdout pipe open past the kill — measured
|
|
201
|
+
* on Windows: a 300ms abort delivered exit 143 but the runner promise only
|
|
202
|
+
* settled ~5s later when the orphaned grandchild released the pipe; (b) Bun's
|
|
203
|
+
* AbortSignal.timeout timer is UNREF'D — on an otherwise idle event loop it
|
|
204
|
+
* never fires at all (this hung the whole test run before it was caught). */
|
|
205
|
+
async function trialSpawn(
|
|
206
|
+
runner: SpawnRunner,
|
|
207
|
+
argv: string[],
|
|
208
|
+
timeoutMs: number,
|
|
209
|
+
): Promise<{ r: RawResult; timedOut: boolean }> {
|
|
210
|
+
const deadline: RawResult = { code: -1, stdout: "", stderr: `probe timed out after ${timeoutMs}ms` };
|
|
211
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
212
|
+
const onDeadline = new Promise<RawResult>((resolve) => { timer = setTimeout(() => resolve(deadline), timeoutMs); });
|
|
213
|
+
try {
|
|
214
|
+
const r = await Promise.race([runner(argv, { signal: AbortSignal.timeout(timeoutMs) }), onDeadline]);
|
|
215
|
+
return { r, timedOut: r === deadline };
|
|
216
|
+
} finally {
|
|
217
|
+
clearTimeout(timer);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Probe verdicts come from spawning `bash -c true` THROUGH the rung's own
|
|
222
|
+
* wrapper (bwrap.rs:74 shape) — the exact failure a real command would hit
|
|
223
|
+
* (missing wsl.exe, bash-less default distro, dead daemon, unpulled or
|
|
224
|
+
* bash-less image) is the failure the probe reports. */
|
|
225
|
+
export async function probeRung(
|
|
226
|
+
rung: Rung,
|
|
227
|
+
runner: SpawnRunner = bunRunner,
|
|
228
|
+
platform: NodeJS.Platform = process.platform,
|
|
229
|
+
opts: ProbeOptions = {},
|
|
230
|
+
): Promise<RungProbe> {
|
|
231
|
+
const timeoutMs = opts.timeoutMs ?? PROBE_TIMEOUT_MS;
|
|
232
|
+
switch (rung) {
|
|
233
|
+
case "direct":
|
|
234
|
+
return { rung, available: true, detail: "always available — today's in-process bash (blocklist only, NOT a sandbox)" };
|
|
235
|
+
case "wsl": {
|
|
236
|
+
if (platform !== "win32") {
|
|
237
|
+
return { rung, available: false, detail: `wsl rung requires Windows wsl.exe (platform is ${platform})` };
|
|
238
|
+
}
|
|
239
|
+
// Trial spawn through the wrapper, NOT `wsl.exe --status`: --status exits
|
|
240
|
+
// 0 whenever the subsystem is installed, even when the default distro has
|
|
241
|
+
// no bash (docker-desktop) and every real command would exit 1.
|
|
242
|
+
const shape = "wsl.exe --exec bash -c true";
|
|
243
|
+
const { r, timedOut } = await trialSpawn(runner, ["wsl.exe", "--exec", "bash", "-c", "true"], timeoutMs);
|
|
244
|
+
if (timedOut) {
|
|
245
|
+
return { rung, available: false, detail: `wsl trial (${shape}) timed out after ${timeoutMs}ms — wrapper did not answer (a cold WSL utility-VM boot exceeds this cap); warm it with the same command and reconfigure` };
|
|
246
|
+
}
|
|
247
|
+
return r.code === 0
|
|
248
|
+
? { rung, available: true, detail: `wsl trial (${shape}) ok` }
|
|
249
|
+
: { rung, available: false, detail: `wsl trial (${shape}) exited ${r.code}: ${probeText(r.stderr || r.stdout) || "no output"}` };
|
|
250
|
+
}
|
|
251
|
+
case "docker": {
|
|
252
|
+
// Trial container run, NOT `docker version`: the version handshake proves
|
|
253
|
+
// a daemon but not that the image is present or contains bash.
|
|
254
|
+
const image = opts.dockerImage ?? DEFAULT_DOCKER_IMAGE;
|
|
255
|
+
const shape = `docker run --rm ${image} bash -c true`;
|
|
256
|
+
const { r, timedOut } = await trialSpawn(runner, ["docker", "run", "--rm", image, "bash", "-c", "true"], timeoutMs);
|
|
257
|
+
if (timedOut) {
|
|
258
|
+
return { rung, available: false, detail: `docker trial (${shape}) timed out after ${timeoutMs}ms — daemon wedged or pulling the image; pre-pull it and retry` };
|
|
259
|
+
}
|
|
260
|
+
return r.code === 0
|
|
261
|
+
? { rung, available: true, detail: `docker trial (${shape}) ok` }
|
|
262
|
+
: { rung, available: false, detail: `docker trial (${shape}) exited ${r.code}: ${probeText(r.stderr || r.stdout) || "no output"}` };
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Probe every rung (status surfaces; lets a caller CHOOSE, not fall). */
|
|
268
|
+
export function probeLadder(
|
|
269
|
+
runner: SpawnRunner = bunRunner,
|
|
270
|
+
platform: NodeJS.Platform = process.platform,
|
|
271
|
+
opts: ProbeOptions = {},
|
|
272
|
+
): Promise<RungProbe[]> {
|
|
273
|
+
return Promise.all(RUNGS.map((r) => probeRung(r, runner, platform, opts)));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ---------- Unavailable rung = loud error (never fall DOWN the ladder) ----------
|
|
277
|
+
|
|
278
|
+
export class RungUnavailableError extends Error {
|
|
279
|
+
readonly rung: Rung;
|
|
280
|
+
readonly detail: string;
|
|
281
|
+
constructor(rung: Rung, detail: string) {
|
|
282
|
+
super(
|
|
283
|
+
`executor rung '${rung}' is unavailable: ${detail}. ` +
|
|
284
|
+
`Refusing to fall back down the ladder — pick an available rung explicitly ` +
|
|
285
|
+
`(probeLadder() reports availability) or make '${rung}' usable and retry.`,
|
|
286
|
+
);
|
|
287
|
+
this.name = "RungUnavailableError";
|
|
288
|
+
this.rung = rung;
|
|
289
|
+
this.detail = detail;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ---------- Rung constructors ----------
|
|
294
|
+
|
|
295
|
+
/** Must contain bash; override per-project via ExecutorOptions.dockerImage. */
|
|
296
|
+
export const DEFAULT_DOCKER_IMAGE = "debian:stable-slim";
|
|
297
|
+
|
|
298
|
+
export interface ExecutorOptions {
|
|
299
|
+
runner?: SpawnRunner;
|
|
300
|
+
/** docker rung: image the command runs in (must provide bash) */
|
|
301
|
+
dockerImage?: string;
|
|
302
|
+
/** test seam: platform used by probes (defaults to process.platform) */
|
|
303
|
+
platform?: NodeJS.Platform;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function directExecutor(runner: SpawnRunner): Executor {
|
|
307
|
+
return {
|
|
308
|
+
rung: "direct",
|
|
309
|
+
async run(cmd, cwd, signal) {
|
|
310
|
+
// Byte-compatible with hashline runOnce: same argv, cwd, signal, assembly.
|
|
311
|
+
const r = await runner([bashBin(), "-c", cmd], { cwd, signal });
|
|
312
|
+
return { code: r.code, text: assemble(r.stdout, r.stderr) };
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function wslExecutor(runner: SpawnRunner): Executor {
|
|
318
|
+
return {
|
|
319
|
+
rung: "wsl",
|
|
320
|
+
async run(cmd, cwd, signal) {
|
|
321
|
+
// --cd translates the Windows cwd into the distro mount; --exec runs
|
|
322
|
+
// bash directly with argv boundaries intact (no double-shell quoting).
|
|
323
|
+
const r = await runner(["wsl.exe", "--cd", cwd, "--exec", "bash", "-c", cmd], { cwd, signal });
|
|
324
|
+
return { code: r.code, text: assemble(r.stdout, r.stderr) };
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function dockerExecutor(runner: SpawnRunner, image: string): Executor {
|
|
330
|
+
return {
|
|
331
|
+
rung: "docker",
|
|
332
|
+
async run(cmd, cwd, signal) {
|
|
333
|
+
// Workspace mounted read-write at /workspace; container removed after
|
|
334
|
+
// the run. The container is the boundary (OpenHands runtime shape).
|
|
335
|
+
const argv = ["docker", "run", "--rm", "-v", `${cwd}:/workspace`, "-w", "/workspace", image, "bash", "-c", cmd];
|
|
336
|
+
const r = await runner(argv, { cwd, signal });
|
|
337
|
+
return { code: r.code, text: assemble(r.stdout, r.stderr) };
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Probe `rung`, then construct its executor. Unavailable → throws
|
|
343
|
+
* RungUnavailableError. The requested rung is always the returned rung —
|
|
344
|
+
* no substitution, in either direction. */
|
|
345
|
+
export async function createExecutor(rung: Rung, opts: ExecutorOptions = {}): Promise<Executor> {
|
|
346
|
+
const runner = opts.runner ?? bunRunner;
|
|
347
|
+
const probe = await probeRung(rung, runner, opts.platform ?? process.platform, { dockerImage: opts.dockerImage });
|
|
348
|
+
if (!probe.available) throw new RungUnavailableError(rung, probe.detail);
|
|
349
|
+
switch (rung) {
|
|
350
|
+
case "direct": return directExecutor(runner);
|
|
351
|
+
case "wsl": return wslExecutor(runner);
|
|
352
|
+
case "docker": return dockerExecutor(runner, opts.dockerImage ?? DEFAULT_DOCKER_IMAGE);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ---------- Session seam (what bashTool calls through) ----------
|
|
357
|
+
|
|
358
|
+
/** The seam tracks the DESIRED rung, not just the installed executor: a
|
|
359
|
+
* failed configure must never silently degrade to whatever was installed
|
|
360
|
+
* before (or to lazy `direct`). Until the desire is met, getExecutor()
|
|
361
|
+
* throws — the same loud RungUnavailableError contract as createExecutor. */
|
|
362
|
+
let current: Executor | null = null;
|
|
363
|
+
let desired: Rung = "direct";
|
|
364
|
+
let lastConfigureFailure: string | null = null;
|
|
365
|
+
|
|
366
|
+
/** Probe + install the session executor. The requested rung becomes the
|
|
367
|
+
* seam's DESIRED rung before probing: if the probe fails, this throws AND
|
|
368
|
+
* every later getExecutor() throws too, until a configure succeeds or
|
|
369
|
+
* resetExecutor() restores the direct default. The seam never hands out a
|
|
370
|
+
* rung other than the one last asked for. */
|
|
371
|
+
export async function configureExecutor(rung: Rung, opts?: ExecutorOptions): Promise<Executor> {
|
|
372
|
+
desired = rung;
|
|
373
|
+
try {
|
|
374
|
+
current = await createExecutor(rung, opts);
|
|
375
|
+
lastConfigureFailure = null;
|
|
376
|
+
return current;
|
|
377
|
+
} catch (e) {
|
|
378
|
+
lastConfigureFailure = e instanceof RungUnavailableError ? e.detail
|
|
379
|
+
: e instanceof Error ? e.message : String(e);
|
|
380
|
+
throw e;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** Current executor. Until configured, the `direct` rung (today's behavior,
|
|
385
|
+
* the one rung that needs no probe). After a FAILED configure this throws
|
|
386
|
+
* RungUnavailableError for the desired rung — no silent fallback. */
|
|
387
|
+
export function getExecutor(): Executor {
|
|
388
|
+
if (current?.rung === desired) return current;
|
|
389
|
+
if (desired === "direct") return (current = directExecutor(bunRunner));
|
|
390
|
+
throw new RungUnavailableError(
|
|
391
|
+
desired,
|
|
392
|
+
lastConfigureFailure ?? `configureExecutor('${desired}') has not succeeded; the seam refuses to substitute another rung`,
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** Test seam: forget the configured executor and the desired rung. */
|
|
397
|
+
export function resetExecutor(): void { current = null; desired = "direct"; lastConfigureFailure = null; }
|