@agenthippo-ai/agenthippo 0.2.3-dataplane → 1.110.9

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 (130) hide show
  1. package/out/analytics/dashboard/provider.js +13 -2
  2. package/out/analytics/dashboard/webview/panel.js +3 -2
  3. package/out/analytics/identity.js +0 -0
  4. package/out/analytics/service.js +23 -25
  5. package/out/analytics/sessionEvents.js +98 -23
  6. package/out/auth/authBaseUrl.js +59 -0
  7. package/out/auth/bootstrapApply.js +9 -10
  8. package/out/auth/brokerClient.js +48 -14
  9. package/out/auth/provider.js +8 -21
  10. package/out/auth/sessionStore.js +3 -40
  11. package/out/auth/signInFlow.js +2 -1
  12. package/out/auth-broker/config.js +50 -5
  13. package/out/auth-broker/server.js +122 -26
  14. package/out/auth-broker/store.js +25 -24
  15. package/out/authority/warrant.js +206 -0
  16. package/out/branding.js +27 -1
  17. package/out/cli/commands/conformance.js +249 -0
  18. package/out/cli/commands/data-plane.js +67 -0
  19. package/out/cli/commands/deploy.js +1 -0
  20. package/out/cli/commands/gateway.js +4 -0
  21. package/out/cli/commands/pack.js +182 -3
  22. package/out/cli/commands/sandbox.js +332 -0
  23. package/out/cli/commands/serve.js +875 -52
  24. package/out/cli/commands/store.js +12 -4
  25. package/out/cli/data-plane/gateway.js +28 -5
  26. package/out/cli/data-plane/index.js +17 -2
  27. package/out/cli/data-plane/registry.js +9 -0
  28. package/out/cli/data-plane/remote.js +106 -0
  29. package/out/cli/data-plane/sidecar.js +161 -0
  30. package/out/cli/deploy/layers/identity.js +85 -4
  31. package/out/cli/deploy-init.js +89 -24
  32. package/out/cli/deps.js +106 -2
  33. package/out/cli/idp-init.js +35 -0
  34. package/out/cli/index.js +155 -16
  35. package/out/cli/pack-install-ref.js +15 -0
  36. package/out/cli/pack-loader.js +8 -1
  37. package/out/cli/pack-runtime.js +61 -0
  38. package/out/cli/runtime.js +35 -1
  39. package/out/cli/serve-hardening.js +80 -3
  40. package/out/cli/utils.js +12 -1
  41. package/out/engines/base.js +32 -3
  42. package/out/engines/claude.js +33 -15
  43. package/out/engines/codex.js +110 -26
  44. package/out/engines/custom.js +8 -0
  45. package/out/evidence/sessionShip.js +610 -0
  46. package/out/extension.js +11 -2
  47. package/out/gateway/bridge.js +40 -2
  48. package/out/gateway/client.js +34 -1
  49. package/out/gateway/installSource.js +68 -0
  50. package/out/gateway/lifecycle.js +185 -51
  51. package/out/gateway/outboundPaths.js +4 -1
  52. package/out/gateway/secretsPreflight.js +90 -0
  53. package/out/gateway/service.js +7 -2
  54. package/out/headless/index.js +17 -1
  55. package/out/live-e2e/mcp-demo/server.mjs +32 -0
  56. package/out/mcp.js +30 -0
  57. package/out/pack/caps.js +271 -0
  58. package/out/pack/governor.js +309 -0
  59. package/out/pack/lease-schema.js +270 -0
  60. package/out/pack/lease.js +640 -0
  61. package/out/pack/reconcile.js +217 -0
  62. package/out/pack/signature.js +839 -0
  63. package/out/remote/handleRemoteChat.js +44 -3
  64. package/out/remote/remoteTrust.js +277 -0
  65. package/out/remote/serveClient.js +52 -5
  66. package/out/runner/host.js +131 -0
  67. package/out/runner/protocol.js +108 -0
  68. package/out/runner/remoteEngine.js +130 -0
  69. package/out/runtime/interfaces/edit-tracker.js +2 -0
  70. package/out/runtime/vscode/vscode-edit-tracker.js +2 -0
  71. package/out/sandbox/containerDriver.js +570 -0
  72. package/out/sandbox/processDriver.js +315 -0
  73. package/out/sandbox/sandboxRunner.js +191 -0
  74. package/out/sandbox/support.js +301 -0
  75. package/out/sandbox/types.js +55 -0
  76. package/out/scripts/agentide-litellm.sh +67 -5
  77. package/out/scripts/agentide_litellm_hooks_v1.py +182 -21
  78. package/out/scripts/analytics/agentide-litellm-otel.config.yaml +94 -132
  79. package/out/scripts/analytics/setup-analytics.ps1 +37 -13
  80. package/out/scripts/analytics/setup-analytics.sh +93 -13
  81. package/out/scripts/analytics/start-analytics.sh +86 -14
  82. package/out/scripts/gateway/setup-gateway.sh +72 -32
  83. package/out/shared/models.js +11 -110
  84. package/out/spotlight/install.js +7 -1
  85. package/out/store/index.js +41 -10
  86. package/out/sync.js +3 -1
  87. package/out/test/analytics.test.js +162 -23
  88. package/out/test/auth-baseurl.test.js +132 -0
  89. package/out/test/auth-bootstrapApply.test.js +27 -6
  90. package/out/test/auth-broker-hardening.test.js +227 -0
  91. package/out/test/auth-broker.test.js +59 -9
  92. package/out/test/branding.test.js +13 -0
  93. package/out/test/caps-governor.test.js +455 -0
  94. package/out/test/codex-new-file-survival.test.js +119 -0
  95. package/out/test/configuration-scopes.test.js +131 -0
  96. package/out/test/data-plane.test.js +67 -0
  97. package/out/test/deploy-init.test.js +40 -0
  98. package/out/test/gateway-client.test.js +28 -0
  99. package/out/test/gateway-install-source.test.js +111 -0
  100. package/out/test/gateway-lifecycle.test.js +1 -1
  101. package/out/test/gateway-outbound.test.js +7 -0
  102. package/out/test/gateway-secrets-preflight.test.js +127 -0
  103. package/out/test/gateway-session-routing.test.js +18 -0
  104. package/out/test/governor-hardening.test.js +217 -0
  105. package/out/test/headless-sync.test.js +22 -0
  106. package/out/test/lease-signature.test.js +357 -0
  107. package/out/test/lease-trust-root.test.js +273 -0
  108. package/out/test/lease-validation.test.js +169 -0
  109. package/out/test/lease.test.js +114 -0
  110. package/out/test/mcp.test.js +27 -1
  111. package/out/test/pack-admission.test.js +216 -0
  112. package/out/test/pack-digest-parity.test.js +140 -0
  113. package/out/test/pack-reconcile.test.js +183 -0
  114. package/out/test/pack-signature-artifact.test.js +103 -0
  115. package/out/test/pack-signature.test.js +176 -0
  116. package/out/test/pack-trust-root.test.js +202 -0
  117. package/out/test/remote-trust.test.js +242 -0
  118. package/out/test/runner-protocol.test.js +249 -0
  119. package/out/test/sandbox-driver.test.js +553 -0
  120. package/out/test/sandbox-hardening.test.js +395 -0
  121. package/out/test/sandbox-runner.test.js +343 -0
  122. package/out/test/serve-hardening.test.js +45 -6
  123. package/out/test/serveClient.test.js +6 -5
  124. package/out/test/sessionShip.test.js +560 -0
  125. package/out/test/spotlightPublish.test.js +28 -0
  126. package/out/test/store-install-safe.test.js +69 -0
  127. package/out/test/tls-bypass.test.js +90 -0
  128. package/out/test/warrant.test.js +138 -0
  129. package/out/utils.js +46 -9
  130. package/package.json +1 -1
@@ -44,6 +44,10 @@ const fs = __importStar(require("fs"));
44
44
  const path = __importStar(require("path"));
45
45
  const pack_loader_1 = require("../pack-loader");
46
46
  const branding_1 = require("../../branding");
47
+ const warrant_1 = require("../../authority/warrant");
48
+ const signature_1 = require("../../pack/signature");
49
+ const store_1 = require("./store");
50
+ const pack_install_ref_1 = require("../pack-install-ref");
47
51
  const BANNER_LINE = '-'.repeat(62);
48
52
  async function packCommand(options) {
49
53
  const workspace = options.workspace;
@@ -65,9 +69,20 @@ async function packCommand(options) {
65
69
  }
66
70
  await showPackInfo(options.name, workspace);
67
71
  break;
72
+ case 'sign':
73
+ await signPackCommand(options);
74
+ break;
75
+ case 'verify':
76
+ await verifyPackCommand(options);
77
+ break;
78
+ case 'digest':
79
+ await digestCommand(options);
80
+ break;
81
+ case 'keygen':
82
+ keygenCommand(options);
83
+ break;
68
84
  case 'install':
69
- console.log('Pack installation is not yet implemented.');
70
- console.log(` For now, manually copy your pack to ~/${branding_1.AGENTIDE_DIR_NAME}/agents/<name>/`);
85
+ await installPackCommand(options);
71
86
  break;
72
87
  case 'export':
73
88
  console.log('Pack export is not yet implemented.');
@@ -79,11 +94,44 @@ async function packCommand(options) {
79
94
  console.log(' list List installed packs');
80
95
  console.log(' validate Validate a pack');
81
96
  console.log(' info Show pack details');
82
- console.log(' install Install a pack (coming soon)');
97
+ console.log(' sign Sign a pack so a fleet can verify it before running');
98
+ console.log(' verify Verify a pack against trusted public keys');
99
+ console.log(' digest Print the pack content digest (what a signature covers)');
100
+ console.log(' keygen Generate an ES256 signing keypair');
101
+ console.log(' install Install a pack from the store to a folder');
83
102
  console.log(' export Export a pack (coming soon)');
84
103
  process.exit(1);
85
104
  }
86
105
  }
106
+ async function installPackCommand(options) {
107
+ if (!options.name?.trim()) {
108
+ console.error('Error: pack slug required.');
109
+ console.log('Usage: agenthippo pack install <slug>[@version] --to <dir>');
110
+ console.log('');
111
+ console.log('Examples:');
112
+ console.log(' agenthippo pack install joker1 --to ./packs/joker1');
113
+ console.log(' agenthippo pack install code-reviewer@1.0.0 --to ./packs/code-reviewer');
114
+ process.exit(1);
115
+ }
116
+ const dest = options.to?.trim();
117
+ if (!dest) {
118
+ console.error('Error: --to <dir> is required.');
119
+ console.log('Usage: agenthippo pack install <slug>[@version] --to <dir>');
120
+ console.log('');
121
+ console.log('Same download as `agenthippo store install pack/<slug>`, extracted flat for serve/deploy.');
122
+ process.exit(1);
123
+ }
124
+ const { slug, version } = (0, pack_install_ref_1.parsePackInstallRef)(options.name);
125
+ await (0, store_1.installFromStore)({
126
+ action: 'install',
127
+ query: `pack/${slug}${version ? `@${version}` : ''}`,
128
+ workspace: options.workspace,
129
+ version,
130
+ force: options.force,
131
+ installDir: path.resolve(dest),
132
+ verbose: options.verbose,
133
+ });
134
+ }
87
135
  async function listPacks(workspace) {
88
136
  const packs = (0, pack_loader_1.listAvailablePacks)(workspace);
89
137
  if (packs.length === 0) {
@@ -218,3 +266,134 @@ async function showPackInfo(nameOrPath, workspace) {
218
266
  process.exit(1);
219
267
  }
220
268
  }
269
+ // -----------------------------------------------------------------------------
270
+ // Signing
271
+ //
272
+ // Publishing has to be a one-liner before `--require-signed-pack` can be turned
273
+ // on in anger: a requirement nobody can satisfy just gets switched off again.
274
+ // -----------------------------------------------------------------------------
275
+ /** Resolve a pack path and its declared identity — a signature binds both, not just bytes. */
276
+ async function loadForSigning(nameOrPath, workspace) {
277
+ if (!nameOrPath) {
278
+ console.error('Error: pack name or path required');
279
+ process.exit(1);
280
+ }
281
+ const packPath = (0, pack_loader_1.resolvePackPath)(nameOrPath, workspace);
282
+ if (!packPath) {
283
+ console.error(`Error: pack not found: ${nameOrPath}`);
284
+ process.exit(1);
285
+ }
286
+ const loaded = await (0, pack_loader_1.loadAgentPack)(packPath, workspace);
287
+ return {
288
+ packPath: loaded.packPath,
289
+ name: loaded.manifest.metadata.name,
290
+ version: loaded.manifest.metadata.version ?? '0.0.0',
291
+ };
292
+ }
293
+ function keygenCommand(options) {
294
+ const kid = options.kid || `sign-${new Date().toISOString().slice(0, 10)}`;
295
+ const key = (0, warrant_1.generateWarrantKeypair)(kid);
296
+ const out = options.output;
297
+ if (out) {
298
+ fs.writeFileSync(out, key.privateKeyPem, { mode: 0o600 });
299
+ console.log(`Private key written to ${out} (mode 0600) — keep it off shared disks.`);
300
+ }
301
+ else {
302
+ console.log(key.privateKeyPem);
303
+ }
304
+ console.log('');
305
+ console.log(`kid: ${kid}`);
306
+ console.log('Public half — publish this to the control plane so instances can verify:');
307
+ console.log(key.publicKeyPem.trim());
308
+ console.log('');
309
+ console.log(` curl -X POST "$CONTROL/api/keys" -H 'content-type: application/json' \\`);
310
+ console.log(` -d '{"kid":"${kid}","publicPem":"…"}'`);
311
+ }
312
+ async function signPackCommand(options) {
313
+ const { packPath, name, version } = await loadForSigning(options.name, options.workspace);
314
+ if (!options.key) {
315
+ console.error('Error: --key <private-key.pem> required. Generate one with: agenthippo pack keygen -o key.pem');
316
+ process.exit(1);
317
+ }
318
+ let privateKeyPem;
319
+ try {
320
+ privateKeyPem = fs.readFileSync(options.key, 'utf8');
321
+ }
322
+ catch (err) {
323
+ console.error(`Error: cannot read key ${options.key}: ${err instanceof Error ? err.message : err}`);
324
+ process.exit(1);
325
+ return;
326
+ }
327
+ const kid = options.kid;
328
+ if (!kid) {
329
+ console.error('Error: --kid <id> required — it is how a verifier finds the public half in JWKS.');
330
+ process.exit(1);
331
+ }
332
+ const { digest } = (0, signature_1.signPack)({ packDir: packPath, pack: name, packVersion: version, privateKeyPem, kid: kid });
333
+ console.log(`Signed ${name}@${version}`);
334
+ console.log(` digest ${digest}`);
335
+ console.log(` key ${kid}`);
336
+ console.log(` written ${path.join(packPath, signature_1.PACK_SIGNATURE_FILE)}`);
337
+ console.log('');
338
+ console.log('Commit pack.sig with the pack: it travels WITH the artifact, and a fleet that');
339
+ console.log('requires signatures refuses anything whose contents no longer match it.');
340
+ }
341
+ async function verifyPackCommand(options) {
342
+ const { packPath, name, version } = await loadForSigning(options.name, options.workspace);
343
+ const token = (0, signature_1.readPackSignature)(packPath);
344
+ if (!token) {
345
+ console.error(`UNSIGNED: no ${signature_1.PACK_SIGNATURE_FILE} at ${packPath}`);
346
+ console.error((0, signature_1.describePackRefusal)({ kind: 'unsigned', detail: `no ${signature_1.PACK_SIGNATURE_FILE}`, pack: name, packVersion: version, packDir: packPath }));
347
+ process.exit(1);
348
+ }
349
+ let keys = {};
350
+ if (options.keys) {
351
+ try {
352
+ keys = JSON.parse(fs.readFileSync(options.keys, 'utf8'));
353
+ }
354
+ catch (err) {
355
+ console.error(`Error: cannot read keys ${options.keys}: ${err instanceof Error ? err.message : err}`);
356
+ process.exit(1);
357
+ }
358
+ }
359
+ else {
360
+ const trust = await (0, signature_1.loadPackTrustRoot)({
361
+ env: options.jwks ? { ...process.env, [signature_1.PACK_JWKS_URL_ENV]: options.jwks } : process.env,
362
+ leaseUrl: process.env.AGENTHIPPO_LEASE_URL,
363
+ });
364
+ for (const w of trust.warnings) {
365
+ console.error(`Warning: ${w}`);
366
+ }
367
+ if (trust.status === 'unavailable') {
368
+ // Distinct from "no keys configured", and with a different fix. Collapsing the two into
369
+ // an empty keyring is finding A4 — a 503 read exactly like an unmanaged install.
370
+ console.error(`REFUSED: a trust root is configured but could not be loaded — ${trust.reason}`);
371
+ console.error((0, signature_1.describePackRefusal)({
372
+ kind: 'trust-root-unavailable', detail: trust.reason ?? 'unavailable', pack: name, packVersion: version,
373
+ packDir: packPath, trustRootHint: trust.url ? `Trust root: ${trust.url}` : undefined,
374
+ }));
375
+ process.exit(1);
376
+ }
377
+ keys = trust.keys;
378
+ }
379
+ if (Object.keys(keys).length === 0) {
380
+ // "No keys" is a refusal, never a pass: verifying against an empty keyring would report
381
+ // success for any signature at all.
382
+ console.error('REFUSED: no trusted public keys. Pass --keys <file>, --jwks <url>, or set AGENTHIPPO_PACK_SIGNING_KEYS.');
383
+ process.exit(1);
384
+ }
385
+ const res = (0, signature_1.verifyPack)(token, keys, { packDir: packPath, pack: name, packVersion: version });
386
+ if (!res.ok) {
387
+ console.error(`INVALID: ${res.reason}`);
388
+ console.error((0, signature_1.describePackRefusal)({ kind: res.kind, detail: res.reason, pack: name, packVersion: version, packDir: packPath }));
389
+ process.exit(1);
390
+ }
391
+ console.log(`OK: ${name}@${version} verified`);
392
+ console.log(` digest ${res.claims.digest}`);
393
+ console.log(` NOTE: .claude/, .codex/ and .agent-hippo/ ARE covered by this digest — they carry hooks`);
394
+ console.log(' and other executable content, and were previously outside the signature.');
395
+ }
396
+ async function digestCommand(options) {
397
+ const { packPath, name, version } = await loadForSigning(options.name, options.workspace);
398
+ console.log(`${(0, signature_1.computePackDigest)(packPath)} ${name}@${version}`);
399
+ }
@@ -0,0 +1,332 @@
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*: the sandbox rebuilds its runner if the token changes (rotation), and
88
+ * never 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
+ const key = `${opts.baseUrl ?? ''}|${opts.modelToken ?? ''}`;
105
+ if (this.runner && this.builtWith === key) {
106
+ return this.runner;
107
+ }
108
+ this.runner?.dispose();
109
+ // The same factory serve builds its own runner with — engine executables, bundled base
110
+ // prompts and the child env patch all resolve identically on both sides of the boundary.
111
+ this.runner = (0, pack_runtime_1.createPackRunner)({
112
+ storagePath: this.defaults.storagePath,
113
+ analyticsSessionsDir: this.defaults.analyticsSessionsDir,
114
+ packId: this.defaults.packId,
115
+ packVersion: this.defaults.packVersion,
116
+ provenance: () => (0, runtime_1.readProvenanceEnvFromProcess)(),
117
+ workspaceRoot: this.defaults.workspace,
118
+ credentials: {
119
+ baseUrl: opts.baseUrl,
120
+ litellmApiKey: opts.modelToken,
121
+ useLiteLLM: Boolean(opts.baseUrl),
122
+ },
123
+ claudeCheapModel: this.defaults.claudeCheapModel,
124
+ omitGatewayToken: true,
125
+ });
126
+ this.builtWith = key;
127
+ return this.runner;
128
+ }
129
+ async stream(message, options, emitter, _runtime, signal) {
130
+ const wire = options;
131
+ const wireRoot = wire.workspaceRoot;
132
+ const workspaceRoot = resolveSandboxWorkspace(wireRoot, this.defaults.workspace);
133
+ if (isForeignWorkspace(wireRoot, workspaceRoot) && !this.warnedWorkspace) {
134
+ this.warnedWorkspace = true;
135
+ console.warn(`[sandbox] ignoring workspaceRoot ${wireRoot} from the wire — this sandbox works in ${workspaceRoot}`);
136
+ }
137
+ try {
138
+ fs.mkdirSync(workspaceRoot, { recursive: true });
139
+ // A sandbox hosts exactly one session, so its working directory IS that session's
140
+ // workspace. Without the chdir the engine inherits the sandbox process's cwd and a tool
141
+ // that writes a RELATIVE path (codex runs `sh -lc 'printf ... > proof.txt'`) lands the
142
+ // file outside the workspace — it exits 0, the agent reports success, work is lost.
143
+ process.chdir(workspaceRoot);
144
+ }
145
+ catch (err) {
146
+ throw new Error(`sandbox cannot use workspace ${workspaceRoot}: ${err instanceof Error ? err.message : String(err)}`);
147
+ }
148
+ const runner = this.ensureRunner(wire);
149
+ // Permissions MUST cross: they decide the ENGINE's own sandbox mode (codex maps
150
+ // workspace-write → sandboxMode=workspace-write, read-only → read-only). Dropping them
151
+ // silently downgraded every sandbox turn to the default, so codex's shell exited 0 while its
152
+ // sandbox discarded the write — reads and commands worked, files never appeared.
153
+ // Observed live on tier 2. Accept either serve's nested `permissions` or a bare
154
+ // `fileAccess` on the options.
155
+ const loose = wire;
156
+ // Last resort is the PACK's own declared fileAccess, not the engine default: a frame that
157
+ // somehow arrives without permissions must not hand the engine more access than the
158
+ // manifest asks for.
159
+ const perms = loose.permissions
160
+ ?? (loose.fileAccess ? { fileAccess: loose.fileAccess } : { fileAccess: this.defaults.fileAccess });
161
+ const result = await runner.run({
162
+ engineId: wire.engineId ?? this.defaults.engineId,
163
+ modelId: wire.modelId ?? this.defaults.modelId,
164
+ message,
165
+ workspaceRoot,
166
+ additionalDirectories: wire.additionalDirectories,
167
+ sessionId: wire.sessionId,
168
+ permissions: perms,
169
+ // Parity with serve's in-process runner: attribution for evidence, the per-turn
170
+ // data-plane nonce (without which a sandbox cannot reach the data gateway at all), and
171
+ // the bound gateway session.
172
+ analyticsUserId: wire.analyticsUserId,
173
+ turnEnv: wire.turnEnv,
174
+ gatewaySessionKey: wire.gatewaySessionKey,
175
+ emitter,
176
+ signal,
177
+ });
178
+ // A failed run must reject so the host emits `failed`, not `result` — otherwise serve
179
+ // would record a successful turn for a turn that errored.
180
+ if (!result.success && result.error) {
181
+ throw new Error(result.error);
182
+ }
183
+ }
184
+ onMaintenance() { }
185
+ dispose() { this.runner?.dispose(); }
186
+ }
187
+ /**
188
+ * A sandbox hosts ONE session for its whole life, so an engine-side async error must not take the
189
+ * process down — that would lose the session, not just the turn. Vendor SDKs reject outside the
190
+ * `engine.stream()` await chain (observed: the Claude SDK's `streamInput` rejecting after its
191
+ * child process dies), which Node treats as fatal by default. Log and stay up; the turn's own
192
+ * catch still emits `failed`, and serve can always destroy the sandbox if it wants it gone.
193
+ *
194
+ * `uncaughtException` is NOT swallowed: after one the process state is unreliable, so exit and
195
+ * let the driver create a fresh sandbox.
196
+ */
197
+ function installSandboxResilience() {
198
+ process.on('unhandledRejection', (reason) => {
199
+ const msg = reason instanceof Error ? reason.message : String(reason);
200
+ console.error(`[sandbox] unhandled rejection (session kept alive): ${msg}`);
201
+ });
202
+ process.on('uncaughtException', (err) => {
203
+ console.error(`[sandbox] uncaught exception — exiting so the driver recreates this sandbox: ${err.message}`);
204
+ process.exit(1);
205
+ });
206
+ }
207
+ async function sandboxCommand(options) {
208
+ // Every other command does this in its action; a sandbox needs it too, or a dev-box sandbox has no
209
+ // engine SDKs at all. In a self-contained image AGENTHIPPO_NO_AUTO_INSTALL=1 makes it a no-op.
210
+ await (0, deps_1.ensureDeps)(false);
211
+ const stateDir = options.stateDir ?? '/state';
212
+ const workspaceForLoad = options.workspace ?? path.join(stateDir, 'workspace');
213
+ fs.mkdirSync(workspaceForLoad, { recursive: true });
214
+ // The real loader, exactly as serve uses it — and `--agent` is a name-OR-path for the same
215
+ // reason it is everywhere else. The previous code resolved it as a path and demanded
216
+ // agent.yaml there, so `serve --agent joker1 --sandbox process` handed the sandbox a bare name
217
+ // it turned into <cwd>/joker1 and every turn died with "No agent.yaml". The hand-rolled regex
218
+ // before that only matched block-style YAML, so a flow-style manifest (`metadata: { name: x }`)
219
+ // silently fell back to 'default' — which is how CODEX_HOME ended up at agents/default instead
220
+ // of agents/<pack>, breaking per-pack engine isolation inside sandboxes.
221
+ const pack = await (0, pack_loader_1.loadAgentPack)(options.agent, workspaceForLoad);
222
+ const manifest = {
223
+ name: pack.manifest.metadata.name,
224
+ version: pack.manifest.metadata.version,
225
+ engine: pack.manifest.spec?.engine ?? 'claude',
226
+ };
227
+ // A sandbox runs on a read-only root by design, so every writable path must live under the one
228
+ // mounted state directory. `--state-dir` is authoritative here and OVERRIDES the image's
229
+ // baked AGENTHIPPO_HOME (the runtime image sets /state, which is read-only unless mounted);
230
+ // otherwise the engine's cache lands on the read-only root and the first turn dies with
231
+ // EROFS — observed live on Linux.
232
+ if (options.stateDir) {
233
+ process.env.AGENTHIPPO_HOME = stateDir;
234
+ }
235
+ else {
236
+ process.env.AGENTHIPPO_HOME = process.env.AGENTHIPPO_HOME ?? stateDir;
237
+ }
238
+ // HOME too, not just AGENTHIPPO_HOME: engine SDKs write to dot-directories under $HOME
239
+ // (the Cursor SDK uses ~/.cursor, codex/claude keep caches there), and the image's default
240
+ // HOME sits on the read-only root — so every such write fails with ENOENT/EROFS inside a
241
+ // sandbox. Observed live on tier 2 with the Cursor SDK.
242
+ // Declare that a sandbox surrounds this process. This says WHERE we are, not how confined we
243
+ // are: whether an engine may stand its own boundary down is answered by AGENTHIPPO_SANDBOX_TIER
244
+ // (see sandbox/types.ts), which only the driver sets and only tier 2 satisfies. Deriving
245
+ // "unconfined is safe" from this flag is what made `--sandbox process` weaker than no sandbox.
246
+ process.env.AGENTHIPPO_SANDBOX = '1';
247
+ const sandboxHome = path.join(stateDir, 'home');
248
+ fs.mkdirSync(sandboxHome, { recursive: true });
249
+ process.env.HOME = sandboxHome;
250
+ // Hydrate the workspace from the mounted pack — the SAME call serve makes at boot. A sandbox
251
+ // holds exactly one user's session, so it should look exactly like the single-user case:
252
+ // pack content (skills, rules, engine config) lives IN the workspace, and every downstream
253
+ // path (`getSkillsSignature(workspaceRoot)`, engine homes) resolves off that one root.
254
+ // Without this the pack stayed a read-only mount the engines never looked at, so no pack
255
+ // skill was ever visible inside a sandbox — observed live on tier 2.
256
+ const workspace = workspaceForLoad;
257
+ await (0, headless_1.syncPackToWorkspace)(workspace, pack.packPath, manifest.name, manifest.engine, console, (0, utils_1.findBundledBasePromptsDir)());
258
+ console.log(`[sandbox] pack ${manifest.name}@${manifest.version ?? '?'} synced into ${workspace} (engine ${manifest.engine})`);
259
+ // Only NOW: everything above is boot, and a boot failure must be fatal so the driver sees a
260
+ // dead sandbox and reports the real error. Swallowing it would leave a sandbox that answers every
261
+ // turn with "channel closed" and no cause.
262
+ installSandboxResilience();
263
+ const rawCheapModel = pack.manifest.spec?.cheapModel?.trim();
264
+ const engine = new SandboxEngine({
265
+ storagePath: stateDir,
266
+ // Land evidence where serve and Spotlight already scan, rather than the headless default.
267
+ analyticsSessionsDir: (0, serve_hardening_1.resolveAnalyticsSessionsDir)(stateDir, false),
268
+ packId: manifest.name,
269
+ packVersion: manifest.version,
270
+ engineId: manifest.engine,
271
+ // serve normally sends modelId on every frame; this is the fallback, and it comes from the
272
+ // pack the same way serve's does instead of a hardcoded alias.
273
+ modelId: (0, headless_1.resolveModelAlias)(pack.manifest.spec?.model?.trim() || 'ah-auto'),
274
+ claudeCheapModel: rawCheapModel ? (0, headless_1.resolveModelAlias)(rawCheapModel) : undefined,
275
+ fileAccess: (0, pack_loader_1.getFileAccess)(pack),
276
+ workspace,
277
+ });
278
+ const runtime = { logger: console };
279
+ if (options.stdio) {
280
+ const host = (0, host_1.startRunnerHost)({
281
+ engine, runtime,
282
+ input: process.stdin,
283
+ output: { write: (c) => process.stdout.write(c) },
284
+ nativeSessionId: () => engine.nativeSessionId,
285
+ });
286
+ await host.done;
287
+ return;
288
+ }
289
+ const socketPath = options.socket;
290
+ if (!socketPath) {
291
+ throw new Error('sandbox requires --socket <path> or --stdio');
292
+ }
293
+ // A stale socket from a previous sandbox would make bind fail; the sandbox owns this path.
294
+ try {
295
+ fs.unlinkSync(socketPath);
296
+ }
297
+ catch { /* not there — fine */ }
298
+ const server = net.createServer((conn) => {
299
+ (0, host_1.startRunnerHost)({
300
+ engine, runtime,
301
+ input: conn,
302
+ output: { write: (c) => conn.write(c) },
303
+ nativeSessionId: () => engine.nativeSessionId,
304
+ });
305
+ });
306
+ // Binding is fatal: a sandbox nobody can reach is useless, and exiting 0 would tell the driver
307
+ // the sandbox is healthy while every turn fails on a dead channel (observed live).
308
+ try {
309
+ await new Promise((resolve, reject) => {
310
+ server.once('error', reject);
311
+ server.listen(socketPath, () => resolve());
312
+ });
313
+ }
314
+ catch (err) {
315
+ console.error(`[sandbox] FATAL: cannot listen at ${socketPath}: ${err instanceof Error ? err.message : String(err)}`);
316
+ process.exit(1);
317
+ }
318
+ // 0o660: the sandbox owns the socket; serve connects from the host. serve manages the container
319
+ // runtime (already root-equivalent), so it can connect; no other local uid can. Only the
320
+ // sandbox's own mount namespace has this path at all — the mode is defence in depth.
321
+ try {
322
+ fs.chmodSync(socketPath, 0o660);
323
+ }
324
+ catch { /* best effort */ }
325
+ console.log(`[sandbox] ${options.sandboxId ?? 'sandbox'} listening on ${socketPath} (pack ${manifest.name}@${manifest.version ?? '?'})`);
326
+ await new Promise((resolve) => {
327
+ const shutdown = () => { server.close(() => resolve()); engine.dispose(); };
328
+ process.once('SIGTERM', shutdown);
329
+ process.once('SIGINT', shutdown);
330
+ server.once('close', () => resolve());
331
+ });
332
+ }