@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,343 @@
1
+ "use strict";
2
+ /*---------------------------------------------------------------------------------------------
3
+ * createSandboxRunner (PLAN/serve-v4-clean.md §3) + per-sandbox model token (blocker C).
4
+ *
5
+ * Behaviors pinned here are the ones a live test caught the hard way:
6
+ * - one sandbox per session, reused across turns;
7
+ * - a dead sandbox is never handed out again, and the next turn recovers transparently;
8
+ * - a retry never duplicates output a streaming client already saw;
9
+ * - admission (lease) is checked BEFORE a sandbox exists — the kill switch;
10
+ * - the model token is sandbox-bound, so lifting it from one sandbox fails in another.
11
+ *--------------------------------------------------------------------------------------------*/
12
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ var desc = Object.getOwnPropertyDescriptor(m, k);
15
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
16
+ desc = { enumerable: true, get: function() { return m[k]; } };
17
+ }
18
+ Object.defineProperty(o, k2, desc);
19
+ }) : (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ o[k2] = m[k];
22
+ }));
23
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
24
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
25
+ }) : function(o, v) {
26
+ o["default"] = v;
27
+ });
28
+ var __importStar = (this && this.__importStar) || (function () {
29
+ var ownKeys = function(o) {
30
+ ownKeys = Object.getOwnPropertyNames || function (o) {
31
+ var ar = [];
32
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
33
+ return ar;
34
+ };
35
+ return ownKeys(o);
36
+ };
37
+ return function (mod) {
38
+ if (mod && mod.__esModule) return mod;
39
+ var result = {};
40
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
41
+ __setModuleDefault(result, mod);
42
+ return result;
43
+ };
44
+ })();
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ require("mocha");
47
+ const assert = __importStar(require("assert"));
48
+ const node_stream_1 = require("node:stream");
49
+ const sandboxRunner_1 = require("../sandbox/sandboxRunner");
50
+ const support_1 = require("../sandbox/support");
51
+ const credential_context_1 = require("../cli/credential-context");
52
+ const warrant_1 = require("../authority/warrant");
53
+ const host_1 = require("../runner/host");
54
+ const sandbox_1 = require("../cli/commands/sandbox");
55
+ const RUNTIME = { logger: { info() { }, warn() { }, error() { }, debug() { } } };
56
+ /** A driver whose sandboxes are real runner hosts over in-memory pipes. */
57
+ function fakeDriver(engineFor) {
58
+ const created = [];
59
+ const specs = [];
60
+ const destroyed = [];
61
+ const revoked = [];
62
+ const closers = new Map();
63
+ let seq = 0;
64
+ const driver = {
65
+ kind: 'process', tier: 0,
66
+ async create(spec) {
67
+ const id = `sandbox-${++seq}`;
68
+ created.push(spec.sandboxKey);
69
+ specs.push(spec);
70
+ const toRunner = new node_stream_1.PassThrough();
71
+ const toServe = new node_stream_1.PassThrough();
72
+ (0, host_1.startRunnerHost)({
73
+ engine: engineFor(), runtime: RUNTIME, input: toRunner,
74
+ output: { write: (c) => toServe.write(c) },
75
+ maintenanceIntervalMs: 60_000,
76
+ });
77
+ let closeChannel = () => undefined;
78
+ const closed = new Promise((res) => { closeChannel = res; });
79
+ closers.set(id, closeChannel);
80
+ return {
81
+ id, tier: 0, endpoint: 'stdio',
82
+ connect: () => ({ input: toServe, output: { write: (c) => toRunner.write(c) }, closed }),
83
+ };
84
+ },
85
+ async revoke(handle, what) { revoked.push({ id: handle.id, what }); },
86
+ async destroy(handle) { destroyed.push(handle.id); closers.get(handle.id)?.(); },
87
+ };
88
+ return { driver, created, specs, destroyed, revoked, kill: (id) => closers.get(id)?.() };
89
+ }
90
+ const echoEngine = () => ({
91
+ id: 'e', displayName: 'E',
92
+ async stream(message, _o, emitter) { await emitter.text(`ECHO:${message}`); },
93
+ onMaintenance() { }, dispose() { },
94
+ });
95
+ function baseOpts(driver) {
96
+ return {
97
+ driver, image: 'img@sha256:' + 'a'.repeat(64), tier: 0,
98
+ routes: [], runtime: RUNTIME,
99
+ };
100
+ }
101
+ suite('sandbox runner — session affinity and recovery', () => {
102
+ test('one sandbox per session, reused across turns', async () => {
103
+ const { driver, created } = fakeDriver(echoEngine);
104
+ const runner = (0, sandboxRunner_1.createSandboxRunner)(baseOpts(driver));
105
+ await runner.run({ engineId: 'e', modelId: 'm', message: 'a', workspaceRoot: '/w', sessionId: 's1' });
106
+ await runner.run({ engineId: 'e', modelId: 'm', message: 'b', workspaceRoot: '/w', sessionId: 's1' });
107
+ assert.deepStrictEqual(created, ['s1'], 'second turn on the same session reuses its sandbox');
108
+ await runner.run({ engineId: 'e', modelId: 'm', message: 'c', workspaceRoot: '/w', sessionId: 's2' });
109
+ assert.deepStrictEqual(created, ['s1', 's2'], 'a different session gets its own sandbox — never cohabitation');
110
+ assert.strictEqual(runner.sandboxes().length, 2);
111
+ });
112
+ test('turn output flows back through the boundary', async () => {
113
+ const { driver } = fakeDriver(echoEngine);
114
+ const runner = (0, sandboxRunner_1.createSandboxRunner)(baseOpts(driver));
115
+ const res = await runner.run({ engineId: 'e', modelId: 'm', message: 'hi', workspaceRoot: '/w', sessionId: 's1' });
116
+ assert.strictEqual(res.success, true);
117
+ assert.strictEqual(res.text, 'ECHO:hi');
118
+ });
119
+ test('a dead sandbox is dropped and the next turn recovers transparently', async () => {
120
+ const { driver, created, kill } = fakeDriver(echoEngine);
121
+ const runner = (0, sandboxRunner_1.createSandboxRunner)(baseOpts(driver));
122
+ await runner.run({ engineId: 'e', modelId: 'm', message: 'a', workspaceRoot: '/w', sessionId: 's1' });
123
+ // TTL reap / OOM / node loss.
124
+ kill(runner.sandboxes()[0].sandboxId);
125
+ await new Promise((r) => setTimeout(r, 10));
126
+ const res = await runner.run({ engineId: 'e', modelId: 'm', message: 'b', workspaceRoot: '/w', sessionId: 's1' });
127
+ assert.strictEqual(res.success, true, 'the turn after a sandbox death must succeed, not fail once first');
128
+ assert.strictEqual(res.text, 'ECHO:b');
129
+ assert.deepStrictEqual(created, ['s1', 's1'], 'a fresh sandbox was created for the same session');
130
+ });
131
+ test('a sandbox whose channel dies is torn down, not left running', async () => {
132
+ // The container survives a dead channel — a refused dial or a dropped socket kills only
133
+ // serve's end. Dropping the map entry without destroying the sandbox leaked one container
134
+ // per failed turn (observed live: four orphans, healthy and unreachable, hours later).
135
+ const { driver, destroyed, kill } = fakeDriver(echoEngine);
136
+ const runner = (0, sandboxRunner_1.createSandboxRunner)(baseOpts(driver));
137
+ await runner.run({ engineId: 'e', modelId: 'm', message: 'a', workspaceRoot: '/w', sessionId: 's1' });
138
+ const sandboxId = runner.sandboxes()[0].sandboxId;
139
+ kill(sandboxId);
140
+ await new Promise((r) => setTimeout(r, 10));
141
+ assert.deepStrictEqual(destroyed, [sandboxId], 'the abandoned sandbox is reaped');
142
+ });
143
+ test('a mid-turn retry never duplicates output a streaming client already saw', async () => {
144
+ // An engine that emits, then hangs — the channel dies after output exists, so retrying
145
+ // would double the client's stream. The runner must surface the failure instead.
146
+ const { driver, kill } = fakeDriver(() => ({
147
+ id: 'e', displayName: 'E',
148
+ async stream(_m, _o, emitter) {
149
+ await emitter.text('partial');
150
+ await new Promise(() => undefined); // never settles
151
+ },
152
+ onMaintenance() { }, dispose() { },
153
+ }));
154
+ const runner = (0, sandboxRunner_1.createSandboxRunner)(baseOpts(driver));
155
+ const seen = [];
156
+ const turn = runner.run({
157
+ engineId: 'e', modelId: 'm', message: 'x', workspaceRoot: '/w', sessionId: 's1',
158
+ emitter: {
159
+ async thinking() { }, async text(d) { seen.push(d); }, async toolStart() { },
160
+ async toolEnd() { }, async progress() { },
161
+ async question() { return { selected: [], freeText: null, declined: true }; },
162
+ async error() { }, async done() { },
163
+ },
164
+ });
165
+ await new Promise((r) => setTimeout(r, 20));
166
+ kill(runner.sandboxes()[0].sandboxId);
167
+ const res = await turn;
168
+ assert.strictEqual(res.success, false, 'output already streamed ⇒ fail rather than silently retry');
169
+ assert.deepStrictEqual(seen, ['partial'], 'the client saw the text exactly once');
170
+ });
171
+ });
172
+ suite('sandbox runner — admission is the kill switch', () => {
173
+ test('a refused lease prevents the sandbox from ever existing', async () => {
174
+ const { driver, created } = fakeDriver(echoEngine);
175
+ const runner = (0, sandboxRunner_1.createSandboxRunner)({
176
+ ...baseOpts(driver),
177
+ admit: () => { throw new Error('lease revoked for pack analyst@1.4.2'); },
178
+ });
179
+ const res = await runner.run({ engineId: 'e', modelId: 'm', message: 'a', workspaceRoot: '/w', sessionId: 's1' });
180
+ assert.strictEqual(res.success, false);
181
+ assert.match(res.error ?? '', /lease revoked/);
182
+ assert.strictEqual(created.length, 0, 'no sandbox is created when admission fails — new work stops immediately');
183
+ });
184
+ test('destroySession and revokeRoute reach the driver', async () => {
185
+ const { driver, destroyed, revoked } = fakeDriver(echoEngine);
186
+ const runner = (0, sandboxRunner_1.createSandboxRunner)(baseOpts(driver));
187
+ await runner.run({ engineId: 'e', modelId: 'm', message: 'a', workspaceRoot: '/w', sessionId: 's1' });
188
+ const sandboxId = runner.sandboxes()[0].sandboxId;
189
+ await runner.revokeRoute('s1', 'inference');
190
+ assert.deepStrictEqual(revoked, [{ id: sandboxId, what: 'inference' }], 'cap breach revokes the inference route');
191
+ assert.strictEqual(runner.sandboxes().length, 1, 'revoking a route does not kill the sandbox');
192
+ await runner.destroySession('s1');
193
+ assert.deepStrictEqual(destroyed, [sandboxId]);
194
+ assert.strictEqual(runner.sandboxes().length, 0);
195
+ });
196
+ });
197
+ suite('sandbox filesystem is per session, not per instance', () => {
198
+ /**
199
+ * The container boundary does not extend to a host directory two containers both mount. With a
200
+ * single static `fs`, every session's sandbox mounted the SAME writable workspace — so one
201
+ * user's file output was the next user's readable input, one turn later.
202
+ */
203
+ test('fsFor gives each session its own writable mount', async () => {
204
+ const { driver, specs } = fakeDriver(echoEngine);
205
+ const runner = (0, sandboxRunner_1.createSandboxRunner)({
206
+ ...baseOpts(driver),
207
+ fs: { readonly: ['/packs/a:/app/pack'], writable: ['/shared:/state/workspace'] },
208
+ fsFor: async (sessionKey) => ({
209
+ readonly: ['/packs/a:/app/pack'],
210
+ writable: [`/state/ws/${sessionKey}:/state/workspace`],
211
+ }),
212
+ });
213
+ await runner.run({ engineId: 'e', modelId: 'm', message: 'a', workspaceRoot: '/w', sessionId: 's1' });
214
+ await runner.run({ engineId: 'e', modelId: 'm', message: 'b', workspaceRoot: '/w', sessionId: 's2' });
215
+ assert.deepStrictEqual(specs[0].fs.writable, ['/state/ws/s1:/state/workspace']);
216
+ assert.deepStrictEqual(specs[1].fs.writable, ['/state/ws/s2:/state/workspace']);
217
+ assert.notStrictEqual(specs[0].fs.writable[0], specs[1].fs.writable[0], 'two sessions never share a writable host directory');
218
+ // The pack is the same for everyone — read-only, and the whole point of pinning it.
219
+ assert.deepStrictEqual(specs[0].fs.readonly, specs[1].fs.readonly);
220
+ });
221
+ test('without fsFor the static fs still applies — --shared-workspace keeps working', async () => {
222
+ const { driver, specs } = fakeDriver(echoEngine);
223
+ const runner = (0, sandboxRunner_1.createSandboxRunner)({
224
+ ...baseOpts(driver),
225
+ fs: { readonly: [], writable: ['/shared:/state/workspace'] },
226
+ });
227
+ await runner.run({ engineId: 'e', modelId: 'm', message: 'a', workspaceRoot: '/w', sessionId: 's1' });
228
+ await runner.run({ engineId: 'e', modelId: 'm', message: 'b', workspaceRoot: '/w', sessionId: 's2' });
229
+ assert.deepStrictEqual(specs[0].fs.writable, specs[1].fs.writable);
230
+ });
231
+ });
232
+ suite('per-sandbox model token (blocker C)', () => {
233
+ /**
234
+ * A warrant is hard-capped at turn length (300s) because nothing revokes one; a sandbox lives
235
+ * for its TTL (30 min by default). Minting once at create handed the sandbox a credential that
236
+ * spent most of its life expired — invisible only because the proxy does not check `exp` yet.
237
+ */
238
+ test('the model warrant is re-minted per turn, not once per sandbox', async () => {
239
+ const { driver } = fakeDriver(echoEngine);
240
+ const minted = [];
241
+ let n = 0;
242
+ const runner = (0, sandboxRunner_1.createSandboxRunner)({
243
+ ...baseOpts(driver),
244
+ mintModelToken: (sandboxId) => {
245
+ const token = `warrant-${sandboxId}-${++n}`;
246
+ minted.push(token);
247
+ return token;
248
+ },
249
+ });
250
+ await runner.run({ engineId: 'e', modelId: 'm', message: 'a', workspaceRoot: '/w', sessionId: 's1' });
251
+ await runner.run({ engineId: 'e', modelId: 'm', message: 'b', workspaceRoot: '/w', sessionId: 's1' });
252
+ // One sandbox, three mints: one at create plus one per turn.
253
+ assert.strictEqual(runner.sandboxes().length, 1, 'the sandbox is reused across turns');
254
+ assert.ok(minted.length >= 3, `expected a mint per turn, got ${minted.length}`);
255
+ assert.strictEqual(new Set(minted).size, minted.length, 'each turn gets its own warrant');
256
+ // Still bound to the one sandbox — a fresh lifetime must not mean a fresh identity.
257
+ assert.ok(minted.every((t) => t.startsWith('warrant-sandbox-1-')), 'every warrant names its sandbox');
258
+ });
259
+ /**
260
+ * The warrant's `user` claim is the ONLY attribution that survives the gateway (it strips
261
+ * caller-supplied user metadata and re-stamps from the verified claim). serve reads the acting
262
+ * user via the ambient credential context at mint time, so what this pins is the plumbing that
263
+ * makes that correct: AsyncLocalStorage must reach mintModelToken through sandbox creation AND
264
+ * through the per-turn re-mint inside RemoteEngine.stream. serve minted `user: packId` for a
265
+ * long time precisely because nothing asserted what the mint callback could see.
266
+ */
267
+ test('mintModelToken sees each turn\'s ambient credential and the session key', async () => {
268
+ const { driver } = fakeDriver(echoEngine);
269
+ const seen = [];
270
+ const runner = (0, sandboxRunner_1.createSandboxRunner)({
271
+ ...baseOpts(driver),
272
+ mintModelToken: (sandboxId, sessionKey) => {
273
+ seen.push({ user: (0, credential_context_1.currentCredential)()?.userId, sessionKey });
274
+ return `warrant-${sandboxId}`;
275
+ },
276
+ });
277
+ await (0, credential_context_1.runWithCredential)({ userId: 'alice@corp.com' }, () => runner.run({ engineId: 'e', modelId: 'm', message: 'a', workspaceRoot: '/w', sessionId: 's1' }));
278
+ await (0, credential_context_1.runWithCredential)({ userId: 'bob@corp.com' }, () => runner.run({ engineId: 'e', modelId: 'm', message: 'b', workspaceRoot: '/w', sessionId: 's1' }));
279
+ await runner.run({ engineId: 'e', modelId: 'm', message: 'c', workspaceRoot: '/w', sessionId: 's1' });
280
+ // Turn 1: create-time mint + per-turn re-mint, both inside alice's credential scope.
281
+ assert.ok(seen.length >= 4, `expected create + per-turn mints, got ${seen.length}`);
282
+ assert.ok(seen.slice(0, 2).every((m) => m.user === 'alice@corp.com'), 'create-time and first-turn mints carry the first turn\'s user');
283
+ // Turn 2 reuses the sandbox but re-mints under bob — attribution is per turn, not per sandbox.
284
+ assert.strictEqual(seen[2].user, 'bob@corp.com', 're-mint carries the current turn\'s user');
285
+ // Outside any credential scope the mint sees no user — the caller's anon fallback, never a
286
+ // stale identity from an earlier turn.
287
+ assert.strictEqual(seen[3].user, undefined, 'no ambient credential leaks across turns');
288
+ assert.ok(seen.every((m) => m.sessionKey === 's1'), 'every mint is handed the session key');
289
+ });
290
+ test('the per-sandbox model credential is a signed warrant, not the provider key', () => {
291
+ // The HMAC-of-the-provider-key token this suite used to cover was removed: nothing shipped
292
+ // used it, and its own docstring conceded the cap it advertised was unenforced. The credential
293
+ // serve actually mints is an ES256 warrant, and these are the properties that matter.
294
+ const provider = 'sk-live-provider-key-must-never-leave';
295
+ const key = (0, warrant_1.generateWarrantKeypair)('test');
296
+ const pubs = { [key.kid]: key.publicKeyPem };
297
+ const token = (0, support_1.mintSandboxInferenceWarrant)(key, {
298
+ user: 'u@example.com', pack: 'p', packVersion: '1.0.0', sandboxId: 'sandbox-1', tier: 2,
299
+ });
300
+ assert.ok(!token.includes(provider), 'the token must not contain the provider key');
301
+ assert.ok((0, warrant_1.verifyWarrant)(token, pubs, { audience: 'inference', channelSandboxId: 'sandbox-1' }).ok, 'verifies for its own sandbox');
302
+ assert.ok(!(0, warrant_1.verifyWarrant)(token, pubs, { audience: 'inference', channelSandboxId: 'sandbox-2' }).ok, 'a token lifted into another sandbox FAILS — this is what makes the cap non-evadable');
303
+ assert.ok(!(0, warrant_1.verifyWarrant)(token, pubs, { audience: 'data', channelSandboxId: 'sandbox-1' }).ok, 'an inference warrant is not a data-plane warrant');
304
+ });
305
+ test('a tampered warrant is refused', () => {
306
+ const key = (0, warrant_1.generateWarrantKeypair)('test');
307
+ const pubs = { [key.kid]: key.publicKeyPem };
308
+ const token = (0, support_1.mintSandboxInferenceWarrant)(key, {
309
+ user: 'u@example.com', pack: 'p', packVersion: '1.0.0', sandboxId: 'sandbox-1', tier: 2,
310
+ });
311
+ // Flip a character in the MIDDLE of the signature segment. The final base64url
312
+ // character encodes only padding bits, so flipping it decodes to the same bytes
313
+ // roughly a quarter of the time and the token still verifies.
314
+ const dot = token.lastIndexOf('.');
315
+ const sigMid = dot + 1 + Math.floor((token.length - dot - 1) / 2);
316
+ const tampered = token.slice(0, sigMid) + (token[sigMid] === 'A' ? 'B' : 'A') + token.slice(sigMid + 1);
317
+ assert.ok(!(0, warrant_1.verifyWarrant)(tampered, pubs, { audience: 'inference', channelSandboxId: 'sandbox-1' }).ok, 'a bad signature is refused');
318
+ assert.ok(!(0, warrant_1.verifyWarrant)('sk-provider', pubs, { audience: 'inference', channelSandboxId: 'sandbox-1' }).ok, 'a raw provider key is not a valid sandbox credential');
319
+ });
320
+ test('two sandboxes get different warrants from the same key', () => {
321
+ const key = (0, warrant_1.generateWarrantKeypair)('test');
322
+ const base = { user: 'u@example.com', pack: 'p', packVersion: '1.0.0', tier: 2 };
323
+ const a = (0, support_1.mintSandboxInferenceWarrant)(key, { ...base, sandboxId: 'sandbox-1' });
324
+ const b = (0, support_1.mintSandboxInferenceWarrant)(key, { ...base, sandboxId: 'sandbox-2' });
325
+ assert.notStrictEqual(a, b);
326
+ });
327
+ });
328
+ suite('sandbox workspace authority', () => {
329
+ test('the wire never redirects a sandbox out of its own workspace', () => {
330
+ // The "codex discards the write" bug: serve sends its HOST workspace path on every run
331
+ // frame, and the sandbox used to mkdir + chdir into it. Inside a container that path is
332
+ // container-private, so the engine's files were visible via ls/cat in the sandbox and absent
333
+ // on the host — and the engine ran with none of the synced pack content around it.
334
+ assert.strictEqual((0, sandbox_1.resolveSandboxWorkspace)('/Users/someone/host/ws', '/state/workspace'), '/state/workspace', 'a host path from the wire must never win');
335
+ assert.strictEqual((0, sandbox_1.resolveSandboxWorkspace)(undefined, '/state/workspace'), '/state/workspace', 'no wire path — the sandbox workspace still governs');
336
+ });
337
+ test('tier 0 sees no mismatch, because serve passes the same path', () => {
338
+ assert.ok(!(0, sandbox_1.isForeignWorkspace)('/tmp/ws', '/tmp/ws'));
339
+ assert.ok(!(0, sandbox_1.isForeignWorkspace)('/tmp/ws/', '/tmp/ws'), 'trailing slash is the same directory');
340
+ assert.ok(!(0, sandbox_1.isForeignWorkspace)(undefined, '/tmp/ws'), 'nothing asked for — nothing to warn about');
341
+ assert.ok((0, sandbox_1.isForeignWorkspace)('/tmp/other', '/tmp/ws'), 'a genuinely different root is worth a warning');
342
+ });
343
+ });