@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,406 @@
1
+ "use strict";
2
+ /*---------------------------------------------------------------------------------------------
3
+ * Copyright (c) AgentHippo.ai. All rights reserved.
4
+ *
5
+ * The three sandbox properties audited: the socket channel is not raceable by another local
6
+ * account, the tier reported is the tier delivered, and a raw provider key does not cross into a
7
+ * sandbox by accident.
8
+ *--------------------------------------------------------------------------------------------*/
9
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ var desc = Object.getOwnPropertyDescriptor(m, k);
12
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
13
+ desc = { enumerable: true, get: function() { return m[k]; } };
14
+ }
15
+ Object.defineProperty(o, k2, desc);
16
+ }) : (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ o[k2] = m[k];
19
+ }));
20
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
21
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
22
+ }) : function(o, v) {
23
+ o["default"] = v;
24
+ });
25
+ var __importStar = (this && this.__importStar) || (function () {
26
+ var ownKeys = function(o) {
27
+ ownKeys = Object.getOwnPropertyNames || function (o) {
28
+ var ar = [];
29
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
30
+ return ar;
31
+ };
32
+ return ownKeys(o);
33
+ };
34
+ return function (mod) {
35
+ if (mod && mod.__esModule) return mod;
36
+ var result = {};
37
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
38
+ __setModuleDefault(result, mod);
39
+ return result;
40
+ };
41
+ })();
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ require("mocha");
44
+ const assert = __importStar(require("assert"));
45
+ const fs = __importStar(require("node:fs"));
46
+ const net = __importStar(require("node:net"));
47
+ const os = __importStar(require("node:os"));
48
+ const path = __importStar(require("node:path"));
49
+ const containerDriver_1 = require("../sandbox/containerDriver");
50
+ const processDriver_1 = require("../sandbox/processDriver");
51
+ const types_1 = require("../sandbox/types");
52
+ const support_1 = require("../sandbox/support");
53
+ const IMAGE = 'ghcr.io/agenthippoai/pack@sha256:' + 'b'.repeat(64);
54
+ function tmpdir() {
55
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'ah-sandbox-hard-'));
56
+ }
57
+ function spec(over = {}) {
58
+ return {
59
+ sandboxKey: 'sess-1',
60
+ image: IMAGE,
61
+ tier: 2,
62
+ routes: [{ kind: 'inference', target: 'litellm:4000' }],
63
+ fs: { readonly: [], writable: [] },
64
+ limits: { ttlSeconds: 0 },
65
+ ...over,
66
+ };
67
+ }
68
+ /** docker stub: network internal-ness is configurable, everything else succeeds. */
69
+ function execStub(internal, calls = []) {
70
+ return async (args) => {
71
+ calls.push(args);
72
+ if (args[0] === 'network' && args[1] === 'inspect') {
73
+ return { stdout: internal ? 'true\n' : 'false\n', code: 0 };
74
+ }
75
+ if (args[0] === 'run') {
76
+ return { stdout: 'container-id\n', code: 0 };
77
+ }
78
+ return { stdout: '', code: 0 };
79
+ };
80
+ }
81
+ suite('sandbox socket directory permissions', () => {
82
+ test('without a known sandbox uid, the directory is sticky (0o1733) — not the old 0o733', () => {
83
+ if (process.platform === 'win32') {
84
+ return;
85
+ }
86
+ const dir = path.join(tmpdir(), 'sandbox-x');
87
+ try {
88
+ (0, containerDriver_1.prepareSocketDir)(dir);
89
+ const mode = fs.statSync(dir).mode & 0o7777;
90
+ assert.strictEqual(mode, 0o1733, `expected sticky 1733, got ${mode.toString(8)}`);
91
+ // The sticky bit is the whole point: it stops another local account from unlinking or
92
+ // renaming the socket the sandbox bound and substituting its own.
93
+ assert.strictEqual(mode & 0o1000, 0o1000, 'sticky bit is set');
94
+ }
95
+ finally {
96
+ fs.rmSync(path.dirname(dir), { recursive: true, force: true });
97
+ }
98
+ });
99
+ test('when the sandbox uid is our own, the directory is 0o700 — nothing else can even traverse', () => {
100
+ if (process.platform === 'win32' || typeof process.getuid !== 'function') {
101
+ return;
102
+ }
103
+ const dir = path.join(tmpdir(), 'sandbox-y');
104
+ try {
105
+ (0, containerDriver_1.prepareSocketDir)(dir, { uid: process.getuid(), gid: process.getgid() });
106
+ const mode = fs.statSync(dir).mode & 0o7777;
107
+ assert.strictEqual(mode, 0o700, `expected 0700, got ${mode.toString(8)}`);
108
+ }
109
+ finally {
110
+ fs.rmSync(path.dirname(dir), { recursive: true, force: true });
111
+ }
112
+ });
113
+ test('preparing an existing directory still tightens it', () => {
114
+ if (process.platform === 'win32') {
115
+ return;
116
+ }
117
+ const dir = path.join(tmpdir(), 'sandbox-z');
118
+ try {
119
+ fs.mkdirSync(dir, { recursive: true });
120
+ fs.chmodSync(dir, 0o777);
121
+ (0, containerDriver_1.prepareSocketDir)(dir);
122
+ assert.strictEqual(fs.statSync(dir).mode & 0o7777, 0o1733);
123
+ }
124
+ finally {
125
+ fs.rmSync(path.dirname(dir), { recursive: true, force: true });
126
+ }
127
+ });
128
+ });
129
+ suite('sandbox socket identity', () => {
130
+ test('a real socket we own is accepted', async () => {
131
+ if (process.platform === 'win32') {
132
+ return;
133
+ }
134
+ const dir = tmpdir();
135
+ const sockPath = path.join(dir, 'sandbox.sock');
136
+ const server = net.createServer();
137
+ try {
138
+ await new Promise((resolve) => server.listen(sockPath, resolve));
139
+ assert.doesNotThrow(() => (0, containerDriver_1.assertOwnSocket)(sockPath));
140
+ }
141
+ finally {
142
+ await new Promise((resolve) => server.close(() => resolve()));
143
+ fs.rmSync(dir, { recursive: true, force: true });
144
+ }
145
+ });
146
+ test('a regular file at the socket path is refused — existence is not readiness', () => {
147
+ const dir = tmpdir();
148
+ const sockPath = path.join(dir, 'sandbox.sock');
149
+ try {
150
+ fs.writeFileSync(sockPath, 'not a socket');
151
+ assert.throws(() => (0, containerDriver_1.assertOwnSocket)(sockPath), /not a unix socket/);
152
+ }
153
+ finally {
154
+ fs.rmSync(dir, { recursive: true, force: true });
155
+ }
156
+ });
157
+ test('a symlink at the socket path is refused rather than followed', () => {
158
+ if (process.platform === 'win32') {
159
+ return;
160
+ }
161
+ const dir = tmpdir();
162
+ const target = path.join(dir, 'real.sock');
163
+ const sockPath = path.join(dir, 'sandbox.sock');
164
+ try {
165
+ fs.writeFileSync(target, '');
166
+ fs.symlinkSync(target, sockPath);
167
+ // lstat, not stat: a stat-based check would follow this to the attacker's choice.
168
+ assert.throws(() => (0, containerDriver_1.assertOwnSocket)(sockPath), /symlink|not a unix socket/);
169
+ }
170
+ finally {
171
+ fs.rmSync(dir, { recursive: true, force: true });
172
+ }
173
+ });
174
+ test('a socket owned by a third local account is refused', async () => {
175
+ if (process.platform === 'win32' || typeof process.getuid !== 'function') {
176
+ return;
177
+ }
178
+ const dir = tmpdir();
179
+ const sockPath = path.join(dir, 'sandbox.sock');
180
+ const server = net.createServer();
181
+ try {
182
+ await new Promise((resolve) => server.listen(sockPath, resolve));
183
+ // Simulate "another uid won the bind" by telling the check we are someone else and the
184
+ // sandbox is someone else again. Real uid manipulation needs root; the decision logic is
185
+ // what matters and it is exactly this comparison.
186
+ assert.throws(() => (0, containerDriver_1.assertOwnSocket)(sockPath, { uid: 4242, gid: 4242 }, 9999), /owned by uid/);
187
+ }
188
+ finally {
189
+ await new Promise((resolve) => server.close(() => resolve()));
190
+ fs.rmSync(dir, { recursive: true, force: true });
191
+ }
192
+ });
193
+ });
194
+ suite('tier-2 egress containment is reported honestly', () => {
195
+ test('an internal route network yields the requested tier 2', async () => {
196
+ const dir = tmpdir();
197
+ try {
198
+ const driver = new containerDriver_1.ContainerSandboxDriver({
199
+ exec: execStub(true),
200
+ socketDir: dir,
201
+ socketWaitMs: 0,
202
+ connectFn: () => ({ input: null, output: { write: () => true }, closed: Promise.resolve() }),
203
+ strictEgress: false,
204
+ });
205
+ const handle = await driver.create(spec());
206
+ assert.strictEqual(handle.tier, 2);
207
+ }
208
+ finally {
209
+ fs.rmSync(dir, { recursive: true, force: true });
210
+ }
211
+ });
212
+ test('a NAT\'d route network is reported as tier 1, not the requested 2', async () => {
213
+ const dir = tmpdir();
214
+ const warnings = [];
215
+ const calls = [];
216
+ try {
217
+ const driver = new containerDriver_1.ContainerSandboxDriver({
218
+ exec: execStub(false, calls),
219
+ socketDir: dir,
220
+ socketWaitMs: 0,
221
+ connectFn: () => ({ input: null, output: { write: () => true }, closed: Promise.resolve() }),
222
+ logger: { info: () => { }, warn: (m) => warnings.push(m) },
223
+ strictEgress: false,
224
+ });
225
+ const handle = await driver.create(spec());
226
+ // The runtime must never advertise a containment level it is not enforcing.
227
+ assert.strictEqual(handle.tier, 1, 'the reported tier reflects what was actually built');
228
+ assert.ok(warnings.some((w) => w.includes('not --internal')), 'the disclosure survives');
229
+ // And the sandbox itself is told the honest tier, so an engine that stands its own
230
+ // boundary down at tier >= 2 keeps it.
231
+ const runCall = calls.find((c) => c[0] === 'run');
232
+ assert.ok(runCall.includes(`${types_1.SANDBOX_TIER_ENV}=1`), `expected ${types_1.SANDBOX_TIER_ENV}=1 in the run args, got: ${runCall.join(' ')}`);
233
+ }
234
+ finally {
235
+ fs.rmSync(dir, { recursive: true, force: true });
236
+ }
237
+ });
238
+ test('strict mode REFUSES a non-internal route network', async () => {
239
+ const dir = tmpdir();
240
+ try {
241
+ const driver = new containerDriver_1.ContainerSandboxDriver({
242
+ exec: execStub(false),
243
+ socketDir: dir,
244
+ socketWaitMs: 0,
245
+ strictEgress: true,
246
+ });
247
+ await assert.rejects(driver.create(spec()), /strict egress containment is enabled/);
248
+ }
249
+ finally {
250
+ fs.rmSync(dir, { recursive: true, force: true });
251
+ }
252
+ });
253
+ test('strict mode is opt-in via the environment', () => {
254
+ assert.strictEqual((0, containerDriver_1.strictEgressFromEnv)({}), false);
255
+ assert.strictEqual((0, containerDriver_1.strictEgressFromEnv)({ AGENTHIPPO_SANDBOX_STRICT_EGRESS: '1' }), true);
256
+ });
257
+ test('a missing route network is still a hard failure, strict or not', async () => {
258
+ const dir = tmpdir();
259
+ try {
260
+ const driver = new containerDriver_1.ContainerSandboxDriver({
261
+ exec: async (args) => args[0] === 'network' && args[1] === 'inspect'
262
+ ? { stdout: 'no such network', code: 1 }
263
+ : { stdout: '', code: 0 },
264
+ socketDir: dir,
265
+ socketWaitMs: 0,
266
+ });
267
+ await assert.rejects(driver.create(spec()), /does not exist/);
268
+ }
269
+ finally {
270
+ fs.rmSync(dir, { recursive: true, force: true });
271
+ }
272
+ });
273
+ });
274
+ suite('provider key passthrough into a sandbox', () => {
275
+ test('CURSOR_API_KEY does NOT cross by default', () => {
276
+ const warnings = [];
277
+ const env = (0, processDriver_1.baseSandboxEnv)({ logger: { warn: (m) => warnings.push(m) } }, { PATH: '/usr/bin', CURSOR_API_KEY: 'sk-cursor-live' });
278
+ assert.strictEqual(env.CURSOR_API_KEY, undefined, 'the provider key must not cross by default');
279
+ assert.strictEqual(env.PATH, '/usr/bin', 'the non-secret allowlist is unaffected');
280
+ assert.ok(warnings.some((w) => w.includes('AGENTHIPPO_SANDBOX_ALLOW_PROVIDER_KEY')), 'the refusal is actionable, not silent');
281
+ });
282
+ test('the explicit opt-in lets it cross', () => {
283
+ const env = (0, processDriver_1.baseSandboxEnv)({ allowProviderKeyPassthrough: true }, { PATH: '/usr/bin', CURSOR_API_KEY: 'sk-cursor-live' });
284
+ assert.strictEqual(env.CURSOR_API_KEY, 'sk-cursor-live');
285
+ });
286
+ test('the env-var opt-in is read the same way', () => {
287
+ assert.strictEqual((0, processDriver_1.providerKeyPassthroughFromEnv)({}), false);
288
+ assert.strictEqual((0, processDriver_1.providerKeyPassthroughFromEnv)({ AGENTHIPPO_SANDBOX_ALLOW_PROVIDER_KEY: '1' }), true);
289
+ });
290
+ test('no other secret is in the allowlist', () => {
291
+ const env = (0, processDriver_1.baseSandboxEnv)({ allowProviderKeyPassthrough: true }, {
292
+ PATH: '/usr/bin',
293
+ ANTHROPIC_API_KEY: 'sk-ant',
294
+ OPENAI_API_KEY: 'sk-oai',
295
+ AGENTHIPPO_LITELLM_API_KEY: 'ahk',
296
+ AGENTHIPPO_TURN_NONCE: 'nonce',
297
+ });
298
+ assert.deepStrictEqual(Object.keys(env).sort(), ['PATH']);
299
+ });
300
+ test('boot-stable route URLs cross into a tier-0 sandbox (MCP + inference proxy)', () => {
301
+ const env = (0, processDriver_1.baseSandboxEnv)({}, {
302
+ PATH: '/usr/bin',
303
+ AGENTHIPPO_DATA_MCP_URL: 'http://connector:9090/mcp',
304
+ AGENTHIPPO_LITELLM_BASE_URL: 'http://gateway:3456/v1',
305
+ ANTHROPIC_API_KEY: 'sk-ant',
306
+ });
307
+ assert.strictEqual(env.AGENTHIPPO_DATA_MCP_URL, 'http://connector:9090/mcp');
308
+ assert.strictEqual(env.AGENTHIPPO_LITELLM_BASE_URL, 'http://gateway:3456/v1');
309
+ assert.strictEqual(env.ANTHROPIC_API_KEY, undefined);
310
+ });
311
+ test('a sandbox holding a provider key is flagged in its own environment', async () => {
312
+ const spawned = [];
313
+ const driver = new processDriver_1.ProcessSandboxDriver({
314
+ command: 'node',
315
+ allowProviderKeyPassthrough: true,
316
+ env: { CURSOR_API_KEY: 'sk-cursor-live' },
317
+ spawnFn: ((_cmd, _args, opts) => {
318
+ spawned.push(opts);
319
+ return {
320
+ stdout: null, stdin: null, exitCode: null, killed: false,
321
+ once: () => undefined, kill: () => true,
322
+ };
323
+ }),
324
+ });
325
+ await driver.create(spec({ tier: 0, routes: [] }));
326
+ assert.strictEqual(spawned[0].env?.[types_1.SANDBOX_PROVIDER_KEY_ENV], '1');
327
+ assert.strictEqual((0, types_1.sandboxHoldsProviderKey)(spawned[0].env), true);
328
+ });
329
+ test('a sandbox without one carries no such flag', async () => {
330
+ const spawned = [];
331
+ const driver = new processDriver_1.ProcessSandboxDriver({
332
+ command: 'node',
333
+ spawnFn: ((_cmd, _args, opts) => {
334
+ spawned.push(opts);
335
+ return {
336
+ stdout: null, stdin: null, exitCode: null, killed: false,
337
+ once: () => undefined, kill: () => true,
338
+ };
339
+ }),
340
+ });
341
+ await driver.create(spec({ tier: 0, routes: [] }));
342
+ assert.strictEqual(spawned[0].env?.[types_1.SANDBOX_PROVIDER_KEY_ENV], undefined);
343
+ assert.strictEqual((0, types_1.sandboxHoldsProviderKey)(spawned[0].env), false);
344
+ });
345
+ test('the detector looks at the actual child env', () => {
346
+ assert.strictEqual((0, processDriver_1.sandboxEnvCarriesProviderKey)({ PATH: '/usr/bin' }), false);
347
+ assert.strictEqual((0, processDriver_1.sandboxEnvCarriesProviderKey)({ CURSOR_API_KEY: 'x' }), true);
348
+ });
349
+ });
350
+ suite('cell → sandbox environment migration', () => {
351
+ test('a stale AGENTHIPPO_CELL_* variable still works, and says so loudly', () => {
352
+ const env = { AGENTHIPPO_CELL_TIER: '2' };
353
+ const warnings = [];
354
+ const applied = (0, support_1.applyDeprecatedSandboxEnvAliases)(env, (m) => warnings.push(m), new Set());
355
+ assert.deepStrictEqual(applied, ['AGENTHIPPO_SANDBOX_TIER']);
356
+ assert.strictEqual(env.AGENTHIPPO_SANDBOX_TIER, '2', 'the operator gets the tier they asked for');
357
+ assert.strictEqual(warnings.length, 1);
358
+ assert.ok(warnings[0].includes('DEPRECATED'));
359
+ assert.ok(warnings[0].includes('AGENTHIPPO_SANDBOX_TIER'), 'the warning names the new spelling');
360
+ });
361
+ test('the bare AGENTHIPPO_CELL flag is aliased too', () => {
362
+ const env = { AGENTHIPPO_CELL: '1' };
363
+ (0, support_1.applyDeprecatedSandboxEnvAliases)(env, () => { }, new Set());
364
+ assert.strictEqual(env.AGENTHIPPO_SANDBOX, '1');
365
+ });
366
+ test('a migrated operator is never overridden by a leftover old variable', () => {
367
+ const env = {
368
+ AGENTHIPPO_CELL_SOCKET_DIR: '/old/path',
369
+ AGENTHIPPO_SANDBOX_SOCKET_DIR: '/new/path',
370
+ };
371
+ const warnings = [];
372
+ (0, support_1.applyDeprecatedSandboxEnvAliases)(env, (m) => warnings.push(m), new Set());
373
+ assert.strictEqual(env.AGENTHIPPO_SANDBOX_SOCKET_DIR, '/new/path', 'the new name wins');
374
+ assert.ok(warnings[0].includes('IGNORING'), 'and the conflict is still reported');
375
+ });
376
+ test('an unset old variable produces no warning and no alias', () => {
377
+ const env = {};
378
+ const warnings = [];
379
+ assert.deepStrictEqual((0, support_1.applyDeprecatedSandboxEnvAliases)(env, (m) => warnings.push(m), new Set()), []);
380
+ assert.deepStrictEqual(warnings, []);
381
+ });
382
+ test('the warning is emitted once per process, not once per turn', () => {
383
+ const once = new Set();
384
+ const warnings = [];
385
+ for (let i = 0; i < 3; i++) {
386
+ (0, support_1.applyDeprecatedSandboxEnvAliases)({ AGENTHIPPO_CELL_TIER: '2' }, (m) => warnings.push(m), once);
387
+ }
388
+ assert.strictEqual(warnings.length, 1);
389
+ });
390
+ });
391
+ suite('single-file build script detection', () => {
392
+ test('a real script path is forwarded', () => {
393
+ assert.deepStrictEqual((0, support_1.sandboxScriptArg)(['node', '/app/out/cli/index.js'], 'node'), ['/app/out/cli/index.js']);
394
+ });
395
+ test('the bun and pkg virtual roots are not forwarded', () => {
396
+ assert.deepStrictEqual((0, support_1.sandboxScriptArg)(['bin', '/$bunfs/root/index.js'], 'bin'), []);
397
+ assert.deepStrictEqual((0, support_1.sandboxScriptArg)(['bin', '/snapshot/app/index.js'], 'bin'), []);
398
+ });
399
+ test("pkg's Windows form is recognised too", () => {
400
+ // `C:\snapshot\app\index.js` used to slip through the startsWith('/snapshot') check.
401
+ assert.deepStrictEqual((0, support_1.sandboxScriptArg)(['bin.exe', 'C:\\snapshot\\app\\index.js'], 'bin.exe'), []);
402
+ });
403
+ test('argv[1] equal to the exec path (a compiled binary) yields no script', () => {
404
+ assert.deepStrictEqual((0, support_1.sandboxScriptArg)(['/usr/local/bin/agenthippo', '/usr/local/bin/agenthippo'], '/usr/local/bin/agenthippo'), []);
405
+ });
406
+ });