@agenthippo-ai/agenthippo 0.2.3-dataplane → 1.110.11

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.
Files changed (139) hide show
  1. package/out/agentpack/index.js +3 -1
  2. package/out/agentpack/paths.js +25 -2
  3. package/out/analytics/dashboard/localFleetAdapter.js +82 -2
  4. package/out/analytics/dashboard/provider.js +13 -2
  5. package/out/analytics/dashboard/webview/panel.js +3 -2
  6. package/out/analytics/identity.js +0 -0
  7. package/out/analytics/service.js +29 -25
  8. package/out/analytics/sessionEvents.js +100 -24
  9. package/out/auth/authBaseUrl.js +59 -0
  10. package/out/auth/bootstrapApply.js +9 -10
  11. package/out/auth/brokerClient.js +48 -14
  12. package/out/auth/provider.js +8 -21
  13. package/out/auth/sessionStore.js +3 -40
  14. package/out/auth/signInFlow.js +2 -1
  15. package/out/auth-broker/config.js +50 -5
  16. package/out/auth-broker/server.js +126 -26
  17. package/out/auth-broker/store.js +25 -24
  18. package/out/authority/warrant.js +206 -0
  19. package/out/branding.js +27 -1
  20. package/out/cli/commands/conformance.js +249 -0
  21. package/out/cli/commands/data-plane.js +151 -0
  22. package/out/cli/commands/deploy.js +1 -0
  23. package/out/cli/commands/gateway.js +4 -0
  24. package/out/cli/commands/pack.js +182 -3
  25. package/out/cli/commands/sandbox.js +351 -0
  26. package/out/cli/commands/serve.js +1145 -130
  27. package/out/cli/commands/store.js +12 -4
  28. package/out/cli/credential-context.js +17 -0
  29. package/out/cli/data-plane/gateway.js +28 -5
  30. package/out/cli/data-plane/index.js +18 -2
  31. package/out/cli/data-plane/registry.js +9 -0
  32. package/out/cli/data-plane/remote.js +106 -0
  33. package/out/cli/data-plane/sidecar.js +225 -0
  34. package/out/cli/deploy/layers/identity.js +88 -4
  35. package/out/cli/deploy-init.js +89 -24
  36. package/out/cli/deps.js +106 -2
  37. package/out/cli/idp-init.js +35 -0
  38. package/out/cli/index.js +171 -17
  39. package/out/cli/pack-install-ref.js +15 -0
  40. package/out/cli/pack-loader.js +8 -1
  41. package/out/cli/pack-runtime.js +61 -0
  42. package/out/cli/runtime.js +35 -1
  43. package/out/cli/serve-hardening.js +260 -14
  44. package/out/cli/utils.js +12 -1
  45. package/out/engines/base.js +32 -3
  46. package/out/engines/claude.js +33 -15
  47. package/out/engines/codex.js +111 -27
  48. package/out/engines/custom.js +15 -1
  49. package/out/evidence/sessionShip.js +643 -0
  50. package/out/extension.js +11 -2
  51. package/out/gateway/bridge.js +40 -2
  52. package/out/gateway/client.js +34 -1
  53. package/out/gateway/installSource.js +68 -0
  54. package/out/gateway/lifecycle.js +185 -51
  55. package/out/gateway/outboundPaths.js +4 -1
  56. package/out/gateway/secretsPreflight.js +90 -0
  57. package/out/gateway/service.js +7 -2
  58. package/out/headless/index.js +25 -2
  59. package/out/live-e2e/mcp-demo/server.mjs +32 -0
  60. package/out/mcp.js +30 -0
  61. package/out/pack/caps.js +271 -0
  62. package/out/pack/governor.js +326 -0
  63. package/out/pack/lease-schema.js +273 -0
  64. package/out/pack/lease.js +682 -0
  65. package/out/pack/reconcile.js +217 -0
  66. package/out/pack/signature.js +839 -0
  67. package/out/remote/handleRemoteChat.js +44 -3
  68. package/out/remote/remoteTrust.js +277 -0
  69. package/out/remote/serveClient.js +52 -5
  70. package/out/runner/host.js +141 -0
  71. package/out/runner/protocol.js +128 -0
  72. package/out/runner/remoteEngine.js +166 -0
  73. package/out/runtime/interfaces/edit-tracker.js +2 -0
  74. package/out/runtime/vscode/vscode-edit-tracker.js +2 -0
  75. package/out/sandbox/containerDriver.js +572 -0
  76. package/out/sandbox/processDriver.js +318 -0
  77. package/out/sandbox/sandboxRunner.js +303 -0
  78. package/out/sandbox/support.js +301 -0
  79. package/out/sandbox/types.js +55 -0
  80. package/out/scripts/agentide-litellm.sh +67 -5
  81. package/out/scripts/agentide_litellm_hooks_v1.py +266 -21
  82. package/out/scripts/analytics/agentide-litellm-otel.config.yaml +98 -132
  83. package/out/scripts/analytics/setup-analytics.ps1 +45 -14
  84. package/out/scripts/analytics/setup-analytics.sh +106 -15
  85. package/out/scripts/analytics/start-analytics.sh +86 -14
  86. package/out/scripts/gateway/setup-gateway.sh +72 -32
  87. package/out/shared/models.js +11 -110
  88. package/out/spotlight/install.js +7 -1
  89. package/out/store/index.js +41 -10
  90. package/out/sync.js +3 -1
  91. package/out/test/analytics.test.js +162 -23
  92. package/out/test/auth-baseurl.test.js +132 -0
  93. package/out/test/auth-bootstrapApply.test.js +27 -6
  94. package/out/test/auth-broker-hardening.test.js +227 -0
  95. package/out/test/auth-broker.test.js +59 -9
  96. package/out/test/branding.test.js +13 -0
  97. package/out/test/caps-governor.test.js +460 -0
  98. package/out/test/codex-new-file-survival.test.js +119 -0
  99. package/out/test/configuration-scopes.test.js +131 -0
  100. package/out/test/data-plane-engine-wiring.test.js +32 -0
  101. package/out/test/data-plane.test.js +380 -0
  102. package/out/test/deploy-init.test.js +47 -1
  103. package/out/test/gateway-client.test.js +28 -0
  104. package/out/test/gateway-install-source.test.js +111 -0
  105. package/out/test/gateway-lifecycle.test.js +1 -1
  106. package/out/test/gateway-outbound.test.js +7 -0
  107. package/out/test/gateway-secrets-preflight.test.js +127 -0
  108. package/out/test/gateway-session-routing.test.js +18 -0
  109. package/out/test/governor-hardening.test.js +222 -0
  110. package/out/test/headless-sync.test.js +22 -0
  111. package/out/test/lease-signature.test.js +357 -0
  112. package/out/test/lease-trust-root.test.js +273 -0
  113. package/out/test/lease-validation.test.js +224 -0
  114. package/out/test/lease.test.js +114 -0
  115. package/out/test/localFleetAdapter.test.js +91 -0
  116. package/out/test/mcp.test.js +27 -1
  117. package/out/test/pack-admission.test.js +216 -0
  118. package/out/test/pack-digest-parity.test.js +140 -0
  119. package/out/test/pack-reconcile.test.js +183 -0
  120. package/out/test/pack-signature-artifact.test.js +103 -0
  121. package/out/test/pack-signature.test.js +176 -0
  122. package/out/test/pack-trust-root.test.js +202 -0
  123. package/out/test/remote-trust.test.js +242 -0
  124. package/out/test/runner-protocol.test.js +249 -0
  125. package/out/test/sandbox-driver.test.js +553 -0
  126. package/out/test/sandbox-hardening.test.js +406 -0
  127. package/out/test/sandbox-runner.test.js +343 -0
  128. package/out/test/sandbox-scope.test.js +449 -0
  129. package/out/test/serve-cron-acceptance.test.js +370 -0
  130. package/out/test/serve-gateway-turn.test.js +122 -0
  131. package/out/test/serve-hardening.test.js +227 -15
  132. package/out/test/serveClient.test.js +6 -5
  133. package/out/test/sessionShip.test.js +618 -0
  134. package/out/test/spotlightPublish.test.js +28 -0
  135. package/out/test/store-install-safe.test.js +69 -0
  136. package/out/test/tls-bypass.test.js +90 -0
  137. package/out/test/warrant.test.js +138 -0
  138. package/out/utils.js +46 -9
  139. package/package.json +1 -1
@@ -0,0 +1,351 @@
1
+ "use strict";
2
+ /*---------------------------------------------------------------------------------------------
3
+ * Copyright (c) AgentHippo.ai. All rights reserved.
4
+ *--------------------------------------------------------------------------------------------*/
5
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = { enumerable: true, get: function() { return m[k]; } };
10
+ }
11
+ Object.defineProperty(o, k2, desc);
12
+ }) : (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ o[k2] = m[k];
15
+ }));
16
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
18
+ }) : function(o, v) {
19
+ o["default"] = v;
20
+ });
21
+ var __importStar = (this && this.__importStar) || (function () {
22
+ var ownKeys = function(o) {
23
+ ownKeys = Object.getOwnPropertyNames || function (o) {
24
+ var ar = [];
25
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
+ return ar;
27
+ };
28
+ return ownKeys(o);
29
+ };
30
+ return function (mod) {
31
+ if (mod && mod.__esModule) return mod;
32
+ var result = {};
33
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
+ __setModuleDefault(result, mod);
35
+ return result;
36
+ };
37
+ })();
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.resolveSandboxWorkspace = resolveSandboxWorkspace;
40
+ exports.isForeignWorkspace = isForeignWorkspace;
41
+ exports.sandboxCommand = sandboxCommand;
42
+ /**
43
+ * `agenthippo sandbox` — the in-sandbox runner entrypoint (PLAN/serve-v4-clean.md §3–§4).
44
+ *
45
+ * This is the process that runs INSIDE a sandbox. It owns the real engine and speaks the frame
46
+ * protocol out over either a unix socket (the `container` driver) or stdio (the `process`
47
+ * driver). It holds no credential: the model is reached through `baseUrl` + the per-sandbox
48
+ * `modelToken` that arrives on each run frame, and data through the gateway URL with a
49
+ * per-turn nonce. Nothing here can mint authority.
50
+ */
51
+ const fs = __importStar(require("fs"));
52
+ const net = __importStar(require("net"));
53
+ const path = __importStar(require("path"));
54
+ const headless_1 = require("../../headless");
55
+ const utils_1 = require("../utils");
56
+ const pack_loader_1 = require("../pack-loader");
57
+ const pack_runtime_1 = require("../pack-runtime");
58
+ const deps_1 = require("../deps");
59
+ const serve_hardening_1 = require("../serve-hardening");
60
+ const runtime_1 = require("../runtime");
61
+ const host_1 = require("../../runner/host");
62
+ /**
63
+ * The sandbox's OWN workspace is authoritative — never the one on the wire.
64
+ *
65
+ * serve sends its HOST workspace path on every run frame. Across a container boundary that path
66
+ * is meaningless, and honouring it was the bug behind "codex discards the write": the sandbox
67
+ * mkdir'd serve's host path INSIDE the sandbox and ran the engine there, so every file the engine
68
+ * wrote landed in a container-private directory — present via ls/cat inside the sandbox during the
69
+ * turn, absent on the host, gone when the sandbox died. The engine also ran in a directory holding
70
+ * none of the synced pack content. Proven with a probe: a sandbox handed a workspaceRoot that
71
+ * exists on no host created it and worked there.
72
+ *
73
+ * At tier 0 the two are the same path (serve passes `--workspace`), so nothing changes there.
74
+ */
75
+ function resolveSandboxWorkspace(_wireRoot, sandboxWorkspace) {
76
+ return sandboxWorkspace;
77
+ }
78
+ /** True when the wire asked for a workspace this sandbox will not use — worth saying out loud once. */
79
+ function isForeignWorkspace(wireRoot, sandboxWorkspace) {
80
+ if (!wireRoot) {
81
+ return false;
82
+ }
83
+ return path.resolve(wireRoot) !== path.resolve(sandboxWorkspace);
84
+ }
85
+ /**
86
+ * Adapts the headless runner to `AgentEngine` so `startRunnerHost` can drive it. The model
87
+ * credential is *per-run* — it rides each frame rather than the runner — and this process never
88
+ * persists a provider key of its own.
89
+ */
90
+ class SandboxEngine {
91
+ defaults;
92
+ id = 'sandbox';
93
+ displayName = 'Sandbox runner';
94
+ runner;
95
+ builtWith;
96
+ /** Log the wire/sandbox workspace mismatch once, not once per turn. */
97
+ warnedWorkspace = false;
98
+ /** Last native engine session id — rides the terminal frame for resume after sandbox death. */
99
+ nativeSessionId;
100
+ constructor(defaults) {
101
+ this.defaults = defaults;
102
+ }
103
+ ensureRunner(opts) {
104
+ // Keyed on baseUrl ALONE. The warrant is re-minted every turn (runner/remoteEngine.ts), so
105
+ // including it rebuilt — and disposed — the runner on every single turn. Harmless while a
106
+ // sandbox held one session and turns arrived one at a time; with several of a user's
107
+ // sessions here, that dispose lands in the middle of another session's stream. The token
108
+ // now travels per run instead (`modelToken` below).
109
+ const key = opts.baseUrl ?? '';
110
+ if (this.runner && this.builtWith === key) {
111
+ return this.runner;
112
+ }
113
+ this.runner?.dispose();
114
+ // The same factory serve builds its own runner with — engine executables, bundled base
115
+ // prompts and the child env patch all resolve identically on both sides of the boundary.
116
+ this.runner = (0, pack_runtime_1.createPackRunner)({
117
+ storagePath: this.defaults.storagePath,
118
+ analyticsSessionsDir: this.defaults.analyticsSessionsDir,
119
+ packId: this.defaults.packId,
120
+ packVersion: this.defaults.packVersion,
121
+ provenance: () => (0, runtime_1.readProvenanceEnvFromProcess)(),
122
+ workspaceRoot: this.defaults.workspace,
123
+ credentials: {
124
+ baseUrl: opts.baseUrl,
125
+ // The warrant this runner was BUILT with, kept only as the fallback for a frame
126
+ // that carries none; every turn normally supplies its own.
127
+ litellmApiKey: opts.modelToken,
128
+ useLiteLLM: Boolean(opts.baseUrl),
129
+ },
130
+ claudeCheapModel: this.defaults.claudeCheapModel,
131
+ omitGatewayToken: true,
132
+ });
133
+ this.builtWith = key;
134
+ return this.runner;
135
+ }
136
+ async stream(message, options, emitter, _runtime, signal) {
137
+ const wire = options;
138
+ const wireRoot = wire.workspaceRoot;
139
+ const workspaceRoot = resolveSandboxWorkspace(wireRoot, this.defaults.workspace);
140
+ if (isForeignWorkspace(wireRoot, workspaceRoot) && !this.warnedWorkspace) {
141
+ this.warnedWorkspace = true;
142
+ console.warn(`[sandbox] ignoring workspaceRoot ${wireRoot} from the wire — this sandbox works in ${workspaceRoot}`);
143
+ }
144
+ try {
145
+ fs.mkdirSync(workspaceRoot, { recursive: true });
146
+ // Without the chdir the engine inherits the sandbox process's cwd and a tool that writes
147
+ // a RELATIVE path (codex runs `sh -lc 'printf ... > proof.txt'`) lands the file outside
148
+ // the workspace — it exits 0, the agent reports success, work is lost.
149
+ //
150
+ // INVARIANT: workspace scope must never be narrower than sandbox scope. chdir is
151
+ // process-global; concurrent turns in one sandbox are only safe when every session
152
+ // resolves to the same workspaceRoot (serve's sandboxFsFor must key writable mounts at
153
+ // sandbox scope, not session scope). The chdir is then idempotent. Per-session workspaces
154
+ // inside a shared sandbox would race here — stream demuxing would not fix it.
155
+ process.chdir(workspaceRoot);
156
+ }
157
+ catch (err) {
158
+ throw new Error(`sandbox cannot use workspace ${workspaceRoot}: ${err instanceof Error ? err.message : String(err)}`);
159
+ }
160
+ const runner = this.ensureRunner(wire);
161
+ // Permissions MUST cross: they decide the ENGINE's own sandbox mode (codex maps
162
+ // workspace-write → sandboxMode=workspace-write, read-only → read-only). Dropping them
163
+ // silently downgraded every sandbox turn to the default, so codex's shell exited 0 while its
164
+ // sandbox discarded the write — reads and commands worked, files never appeared.
165
+ // Observed live on tier 2. Accept either serve's nested `permissions` or a bare
166
+ // `fileAccess` on the options.
167
+ const loose = wire;
168
+ // Last resort is the PACK's own declared fileAccess, not the engine default: a frame that
169
+ // somehow arrives without permissions must not hand the engine more access than the
170
+ // manifest asks for.
171
+ const perms = loose.permissions
172
+ ?? (loose.fileAccess ? { fileAccess: loose.fileAccess } : { fileAccess: this.defaults.fileAccess });
173
+ const result = await runner.run({
174
+ engineId: wire.engineId ?? this.defaults.engineId,
175
+ modelId: wire.modelId ?? this.defaults.modelId,
176
+ message,
177
+ workspaceRoot,
178
+ additionalDirectories: wire.additionalDirectories,
179
+ sessionId: wire.sessionId,
180
+ // Per-turn warrant, not the one this runner was built with — see ensureRunner.
181
+ modelToken: wire.modelToken,
182
+ permissions: perms,
183
+ // Parity with serve's in-process runner: attribution for evidence, the per-turn
184
+ // data-plane nonce (without which a sandbox cannot reach the data gateway at all), and
185
+ // the bound gateway session.
186
+ analyticsUserId: wire.analyticsUserId,
187
+ // The credential does not cross into this process by design, so the per-user engine
188
+ // home key arrives pre-computed on the frame. Absent ⇒ unscoped, which is correct for
189
+ // a single-tenant run and is what every multi-user sandbox turn used to get.
190
+ engineHomeUserKey: wire.engineHomeUserKey,
191
+ turnEnv: wire.turnEnv,
192
+ gatewaySessionKey: wire.gatewaySessionKey,
193
+ emitter,
194
+ signal,
195
+ });
196
+ // A failed run must reject so the host emits `failed`, not `result` — otherwise serve
197
+ // would record a successful turn for a turn that errored.
198
+ if (!result.success && result.error) {
199
+ throw new Error(result.error);
200
+ }
201
+ }
202
+ onMaintenance() { }
203
+ dispose() { this.runner?.dispose(); }
204
+ }
205
+ /**
206
+ * A sandbox outlives every turn it runs — and under user scope holds several of one person's
207
+ * sessions — so an engine-side async error must not take the process down: that would lose every
208
+ * conversation here, not just the turn. Vendor SDKs reject outside the
209
+ * `engine.stream()` await chain (observed: the Claude SDK's `streamInput` rejecting after its
210
+ * child process dies), which Node treats as fatal by default. Log and stay up; the turn's own
211
+ * catch still emits `failed`, and serve can always destroy the sandbox if it wants it gone.
212
+ *
213
+ * `uncaughtException` is NOT swallowed: after one the process state is unreliable, so exit and
214
+ * let the driver create a fresh sandbox.
215
+ */
216
+ function installSandboxResilience() {
217
+ process.on('unhandledRejection', (reason) => {
218
+ const msg = reason instanceof Error ? reason.message : String(reason);
219
+ console.error(`[sandbox] unhandled rejection (session kept alive): ${msg}`);
220
+ });
221
+ process.on('uncaughtException', (err) => {
222
+ console.error(`[sandbox] uncaught exception — exiting so the driver recreates this sandbox: ${err.message}`);
223
+ process.exit(1);
224
+ });
225
+ }
226
+ async function sandboxCommand(options) {
227
+ // Every other command does this in its action; a sandbox needs it too, or a dev-box sandbox has no
228
+ // engine SDKs at all. In a self-contained image AGENTHIPPO_NO_AUTO_INSTALL=1 makes it a no-op.
229
+ await (0, deps_1.ensureDeps)(false);
230
+ const stateDir = options.stateDir ?? '/state';
231
+ const workspaceForLoad = options.workspace ?? path.join(stateDir, 'workspace');
232
+ fs.mkdirSync(workspaceForLoad, { recursive: true });
233
+ // The real loader, exactly as serve uses it — and `--agent` is a name-OR-path for the same
234
+ // reason it is everywhere else. The previous code resolved it as a path and demanded
235
+ // agent.yaml there, so `serve --agent joker1 --sandbox process` handed the sandbox a bare name
236
+ // it turned into <cwd>/joker1 and every turn died with "No agent.yaml". The hand-rolled regex
237
+ // before that only matched block-style YAML, so a flow-style manifest (`metadata: { name: x }`)
238
+ // silently fell back to 'default' — which is how CODEX_HOME ended up at agents/default instead
239
+ // of agents/<pack>, breaking per-pack engine isolation inside sandboxes.
240
+ const pack = await (0, pack_loader_1.loadAgentPack)(options.agent, workspaceForLoad);
241
+ const manifest = {
242
+ name: pack.manifest.metadata.name,
243
+ version: pack.manifest.metadata.version,
244
+ engine: pack.manifest.spec?.engine ?? 'claude',
245
+ };
246
+ // A sandbox runs on a read-only root by design, so every writable path must live under the one
247
+ // mounted state directory. `--state-dir` is authoritative here and OVERRIDES the image's
248
+ // baked AGENTHIPPO_HOME (the runtime image sets /state, which is read-only unless mounted);
249
+ // otherwise the engine's cache lands on the read-only root and the first turn dies with
250
+ // EROFS — observed live on Linux.
251
+ if (options.stateDir) {
252
+ process.env.AGENTHIPPO_HOME = stateDir;
253
+ }
254
+ else {
255
+ process.env.AGENTHIPPO_HOME = process.env.AGENTHIPPO_HOME ?? stateDir;
256
+ }
257
+ // HOME too, not just AGENTHIPPO_HOME: engine SDKs write to dot-directories under $HOME
258
+ // (the Cursor SDK uses ~/.cursor, codex/claude keep caches there), and the image's default
259
+ // HOME sits on the read-only root — so every such write fails with ENOENT/EROFS inside a
260
+ // sandbox. Observed live on tier 2 with the Cursor SDK.
261
+ // Declare that a sandbox surrounds this process. This says WHERE we are, not how confined we
262
+ // are: whether an engine may stand its own boundary down is answered by AGENTHIPPO_SANDBOX_TIER
263
+ // (see sandbox/types.ts), which only the driver sets and only tier 2 satisfies. Deriving
264
+ // "unconfined is safe" from this flag is what made `--sandbox process` weaker than no sandbox.
265
+ process.env.AGENTHIPPO_SANDBOX = '1';
266
+ const sandboxHome = path.join(stateDir, 'home');
267
+ fs.mkdirSync(sandboxHome, { recursive: true });
268
+ process.env.HOME = sandboxHome;
269
+ // Hydrate the workspace from the mounted pack — the SAME call serve makes at boot. A sandbox
270
+ // holds exactly one user's session, so it should look exactly like the single-user case:
271
+ // pack content (skills, rules, engine config) lives IN the workspace, and every downstream
272
+ // path (`getSkillsSignature(workspaceRoot)`, engine homes) resolves off that one root.
273
+ // Without this the pack stayed a read-only mount the engines never looked at, so no pack
274
+ // skill was ever visible inside a sandbox — observed live on tier 2.
275
+ const workspace = workspaceForLoad;
276
+ await (0, headless_1.syncPackToWorkspace)(workspace, pack.packPath, manifest.name, manifest.engine, console, (0, utils_1.findBundledBasePromptsDir)());
277
+ console.log(`[sandbox] pack ${manifest.name}@${manifest.version ?? '?'} synced into ${workspace} (engine ${manifest.engine})`);
278
+ // Only NOW: everything above is boot, and a boot failure must be fatal so the driver sees a
279
+ // dead sandbox and reports the real error. Swallowing it would leave a sandbox that answers every
280
+ // turn with "channel closed" and no cause.
281
+ installSandboxResilience();
282
+ const rawCheapModel = pack.manifest.spec?.cheapModel?.trim();
283
+ const engine = new SandboxEngine({
284
+ storagePath: stateDir,
285
+ // Land evidence where serve and Spotlight already scan, rather than the headless default.
286
+ analyticsSessionsDir: (0, serve_hardening_1.resolveAnalyticsSessionsDir)(stateDir, false),
287
+ packId: manifest.name,
288
+ packVersion: manifest.version,
289
+ engineId: manifest.engine,
290
+ // serve normally sends modelId on every frame; this is the fallback, and it comes from the
291
+ // pack the same way serve's does instead of a hardcoded alias.
292
+ modelId: (0, headless_1.resolveModelAlias)(pack.manifest.spec?.model?.trim() || 'ah-auto'),
293
+ claudeCheapModel: rawCheapModel ? (0, headless_1.resolveModelAlias)(rawCheapModel) : undefined,
294
+ fileAccess: (0, pack_loader_1.getFileAccess)(pack),
295
+ workspace,
296
+ });
297
+ const runtime = { logger: console };
298
+ if (options.stdio) {
299
+ const host = (0, host_1.startRunnerHost)({
300
+ engine, runtime,
301
+ input: process.stdin,
302
+ output: { write: (c) => process.stdout.write(c) },
303
+ nativeSessionId: () => engine.nativeSessionId,
304
+ });
305
+ await host.done;
306
+ return;
307
+ }
308
+ const socketPath = options.socket;
309
+ if (!socketPath) {
310
+ throw new Error('sandbox requires --socket <path> or --stdio');
311
+ }
312
+ // A stale socket from a previous sandbox would make bind fail; the sandbox owns this path.
313
+ try {
314
+ fs.unlinkSync(socketPath);
315
+ }
316
+ catch { /* not there — fine */ }
317
+ const server = net.createServer((conn) => {
318
+ (0, host_1.startRunnerHost)({
319
+ engine, runtime,
320
+ input: conn,
321
+ output: { write: (c) => conn.write(c) },
322
+ nativeSessionId: () => engine.nativeSessionId,
323
+ });
324
+ });
325
+ // Binding is fatal: a sandbox nobody can reach is useless, and exiting 0 would tell the driver
326
+ // the sandbox is healthy while every turn fails on a dead channel (observed live).
327
+ try {
328
+ await new Promise((resolve, reject) => {
329
+ server.once('error', reject);
330
+ server.listen(socketPath, () => resolve());
331
+ });
332
+ }
333
+ catch (err) {
334
+ console.error(`[sandbox] FATAL: cannot listen at ${socketPath}: ${err instanceof Error ? err.message : String(err)}`);
335
+ process.exit(1);
336
+ }
337
+ // 0o660: the sandbox owns the socket; serve connects from the host. serve manages the container
338
+ // runtime (already root-equivalent), so it can connect; no other local uid can. Only the
339
+ // sandbox's own mount namespace has this path at all — the mode is defence in depth.
340
+ try {
341
+ fs.chmodSync(socketPath, 0o660);
342
+ }
343
+ catch { /* best effort */ }
344
+ console.log(`[sandbox] ${options.sandboxId ?? 'sandbox'} listening on ${socketPath} (pack ${manifest.name}@${manifest.version ?? '?'})`);
345
+ await new Promise((resolve) => {
346
+ const shutdown = () => { server.close(() => resolve()); engine.dispose(); };
347
+ process.once('SIGTERM', shutdown);
348
+ process.once('SIGINT', shutdown);
349
+ server.once('close', () => resolve());
350
+ });
351
+ }