@mcpjam/inspector 3.12.3 → 3.12.5

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 (37) hide show
  1. package/README.md +29 -21
  2. package/bin/access-link.mjs +53 -0
  3. package/bin/start.js +47 -14
  4. package/dist/client/assets/OAuthDesktopReturnNotice-B_x9xTE-.css +2 -0
  5. package/dist/client/assets/OAuthDesktopReturnNotice-LlC451lX.js +11 -0
  6. package/dist/client/assets/{app-bootstrap-C1Jn01kP.js → app-bootstrap-2jatN7gw.js} +376 -374
  7. package/dist/client/assets/{button-RD2Dghjp.js → button-B2OH2ei2.js} +2 -2
  8. package/dist/client/assets/{chunk-YOKDWASO-DTTU1wSE.js → chunk-YOKDWASO-uJ3KOQV7.js} +5 -5
  9. package/dist/client/assets/{dead-clicks-autocapture-CDkbT21a.js → dead-clicks-autocapture-D055GjVp.js} +2 -2
  10. package/dist/client/assets/{error-reporting-Dg5b1JwS.js → error-reporting-DGP36KLx.js} +57 -57
  11. package/dist/client/assets/{error-reporting-_SEhX6IL.js → error-reporting-FWZKbZxG.js} +1 -1
  12. package/dist/client/assets/{exception-autocapture-CtxHwPtn.js → exception-autocapture-103gdJjL.js} +2 -2
  13. package/dist/client/assets/{failed-request-tracker-kUaJ-sVM.js → failed-request-tracker-Cp9_VFMm.js} +2 -2
  14. package/dist/client/assets/{highlighted-body-KPVGNVTW-CBC2hHHb.js → highlighted-body-KPVGNVTW-Dx70h1nm.js} +2 -2
  15. package/dist/client/assets/index-rskHnM3O.js +4 -0
  16. package/dist/client/assets/{jszip.min-B3Qii_az.js → jszip.min-CwJyI5tb.js} +2 -2
  17. package/dist/client/assets/{mermaid-HWGCJPDP-BzT3-9sO.js → mermaid-HWGCJPDP-Ck3prJKl.js} +1 -1
  18. package/dist/client/assets/{posthog-recorder-B06ULcLl.js → posthog-recorder-tKuMFijV.js} +2 -2
  19. package/dist/client/assets/{preload-helper-DvWwGI6x.js → preload-helper-DrcoTQ49.js} +2 -2
  20. package/dist/client/assets/{rolldown-runtime-DVMSi0Yr.js → rolldown-runtime-7FnpGOz6.js} +1 -1
  21. package/dist/client/assets/{surveys-D941hgn3.js → surveys-BsZYxM6N.js} +2 -2
  22. package/dist/client/assets/{trace-timeline-Bk3hAXNq.js → trace-timeline-BOZvHcOG.js} +1 -1
  23. package/dist/client/assets/{trace-timeline-DVZqlQSW.js → trace-timeline-Bf9Oai70.js} +8 -8
  24. package/dist/client/assets/{web-vitals-DZUBt4dL.js → web-vitals-DdzaVN3H.js} +2 -2
  25. package/dist/client/index.html +7 -7
  26. package/dist/server/chunk-3EDQFXTF.js +4801 -0
  27. package/dist/server/{chunk-5RE7KJ2M.js → chunk-4PYR7GMQ.js} +16 -197
  28. package/dist/server/{chunk-WGJHSGJP.js → chunk-QA6VJT6A.js} +961 -963
  29. package/dist/server/chunk-SH24KJBI.js +194 -0
  30. package/dist/server/{consent-lifetime-PTGAZQSU.js → consent-lifetime-M7PS6RUU.js} +3 -2
  31. package/dist/server/harness-install-cli.js +1 -1
  32. package/dist/server/index.js +15110 -16012
  33. package/dist/server/local-turn-XXOSZHMT.js +12 -0
  34. package/package.json +14 -11
  35. package/dist/client/assets/OAuthDesktopReturnNotice-C5D90OSm.js +0 -11
  36. package/dist/client/assets/OAuthDesktopReturnNotice-C95FI7VH.css +0 -2
  37. package/dist/client/assets/index-yOjxp36p.js +0 -4
@@ -0,0 +1,4801 @@
1
+ import {
2
+ LOCAL_HARNESS_MANIFEST,
3
+ LOCAL_HARNESS_POLICY_VERSION,
4
+ LOCAL_ISOLATION_POLICY_VERSION,
5
+ createLocalStateMutationLock,
6
+ currentLocalPlatform,
7
+ listGroupMembers,
8
+ localPackTarget,
9
+ logger,
10
+ manifestWithExpectedBundleDigest,
11
+ probeProcess,
12
+ probeProcessGroup,
13
+ readProcessBirthIdentity,
14
+ readProcessGroupId,
15
+ readRuntimeInstallStatus,
16
+ reserveRuntimeUse,
17
+ resolveLocalCompatibility,
18
+ resolveManagedBundle,
19
+ resolveSystemInstall,
20
+ revalidateRuntime,
21
+ runtimeInstallRoot,
22
+ supportsOwnershipProof,
23
+ terminateOwnedProcess,
24
+ terminateOwnedProcessGroup
25
+ } from "./chunk-QA6VJT6A.js";
26
+ import {
27
+ HOSTED_MODE,
28
+ LOCAL_HARNESS_ENABLED,
29
+ capabilityFingerprint,
30
+ capabilityMatches,
31
+ mintCapabilityToken,
32
+ persistCapabilityState
33
+ } from "./chunk-SH24KJBI.js";
34
+
35
+ // shared/harness-model-support-evidence.json
36
+ var harness_model_support_evidence_default = {
37
+ $comment: `Harness x model compatibility evidence. Rows are evaluated in order; the first row whose harness matches, whose versionRange admits the runtime version and whose familyPattern matches the canonical model id wins. versionRange is "*" (any version, including an unknown one), "<=A.B.x" or ">A.B.x". When the runtime version is unknown and a version-specific row's pattern matches, the verdict is unknown. Mirrored byte-for-byte between mcpjam-inspector/shared/harness-model-support-evidence.json (source) and mcpjam-backend convex/lib/harnessModelSupportEvidence.json.`,
38
+ rows: [
39
+ {
40
+ harness: "claude-code",
41
+ versionRange: "*",
42
+ familyPattern: "^anthropic/claude-(haiku|sonnet|opus)-\\d+(\\.\\d+)?$",
43
+ status: "supported",
44
+ evidence: "Claude Code accepts the haiku/sonnet/opus family ids natively (toClaudeCodeModel maps them to CLI model names).",
45
+ observedAt: "2026-09-25"
46
+ },
47
+ {
48
+ harness: "claude-code",
49
+ versionRange: "*",
50
+ familyPattern: "^anthropic/",
51
+ status: "unknown",
52
+ evidence: "Anthropic model outside the haiku/sonnet/opus families: no native Claude Code model id has been verified.",
53
+ observedAt: "2026-09-25"
54
+ },
55
+ {
56
+ harness: "claude-code",
57
+ versionRange: "*",
58
+ familyPattern: ".*",
59
+ status: "unsupported",
60
+ evidence: "Claude Code only runs Anthropic models.",
61
+ observedAt: "2026-09-25"
62
+ },
63
+ {
64
+ harness: "codex",
65
+ versionRange: "<=0.149.x",
66
+ familyPattern: "^openai/gpt-5\\.6($|-)",
67
+ status: "unsupported",
68
+ evidence: "Loopback capture 2026-09-01: the pinned Codex CLI sends tools: [] for the gpt-5.6 line, so the model cannot call MCP tools.",
69
+ observedAt: "2026-09-01"
70
+ },
71
+ {
72
+ harness: "codex",
73
+ versionRange: ">0.149.x",
74
+ familyPattern: "^openai/gpt-5\\.6($|-)",
75
+ status: "unknown",
76
+ evidence: "The gpt-5.6 tool-less behavior was measured only on Codex <=0.149.x; newer CLI versions have not been captured.",
77
+ observedAt: "2026-09-01"
78
+ },
79
+ {
80
+ harness: "codex",
81
+ versionRange: "*",
82
+ familyPattern: "^openai/gpt-5",
83
+ status: "supported",
84
+ evidence: "Codex runs the OpenAI gpt-5 family natively (toCodexModel).",
85
+ observedAt: "2026-09-25"
86
+ },
87
+ {
88
+ harness: "codex",
89
+ versionRange: "*",
90
+ familyPattern: ".*",
91
+ status: "unsupported",
92
+ evidence: "Codex only runs OpenAI gpt-5 family models.",
93
+ observedAt: "2026-09-25"
94
+ },
95
+ {
96
+ harness: "cursor",
97
+ versionRange: "*",
98
+ familyPattern: "^cursor/auto$",
99
+ status: "supported",
100
+ evidence: "Cursor's agent CLI selects its own model; cursor/auto is the only selectable id.",
101
+ observedAt: "2026-09-25"
102
+ },
103
+ {
104
+ harness: "cursor",
105
+ versionRange: "*",
106
+ familyPattern: ".*",
107
+ status: "unsupported",
108
+ evidence: "Cursor's agent CLI does not accept an explicit model id.",
109
+ observedAt: "2026-09-25"
110
+ }
111
+ ]
112
+ };
113
+
114
+ // shared/harness-model-support.ts
115
+ var HARNESS_MODEL_SUPPORT_ROWS = harness_model_support_evidence_default.rows;
116
+ var HARNESS_PINNED_VERSIONS = {
117
+ "claude-code": "2.1.245",
118
+ codex: "0.149.1",
119
+ cursor: null
120
+ };
121
+ function harnessPinnedVersion(harnessId) {
122
+ return Object.prototype.hasOwnProperty.call(
123
+ HARNESS_PINNED_VERSIONS,
124
+ harnessId
125
+ ) ? HARNESS_PINNED_VERSIONS[harnessId] ?? void 0 : void 0;
126
+ }
127
+ var HARNESS_DISPLAY_NAMES = {
128
+ "claude-code": "Claude Code",
129
+ codex: "Codex",
130
+ cursor: "Cursor CLI"
131
+ };
132
+ function harnessDisplayName(harnessId) {
133
+ return HARNESS_DISPLAY_NAMES[harnessId] ?? harnessId;
134
+ }
135
+ function parseHarnessVersion(version) {
136
+ if (typeof version !== "string") return void 0;
137
+ const match = version.trim().match(/^v?(\d+)\.(\d+)(?:\.(\d+))?(?:[-+].*)?$/);
138
+ if (!match) return void 0;
139
+ return [Number(match[1]), Number(match[2]), Number(match[3] ?? 0)];
140
+ }
141
+ function compareMajorMinor(a, b) {
142
+ if (a[0] !== b[0]) return a[0] - b[0];
143
+ return a[1] - b[1];
144
+ }
145
+ function parseVersionRange(range) {
146
+ if (range === "*") return { kind: "any" };
147
+ const match = range.match(/^(<=|>)(\d+)\.(\d+)\.x$/);
148
+ if (!match) return void 0;
149
+ return {
150
+ kind: match[1] === "<=" ? "lte" : "gt",
151
+ bound: [Number(match[2]), Number(match[3])]
152
+ };
153
+ }
154
+ function harnessVersionInRange(range, version) {
155
+ const parsedRange = parseVersionRange(range);
156
+ if (!parsedRange) return false;
157
+ if (parsedRange.kind === "any") return true;
158
+ const parsed = parseHarnessVersion(version);
159
+ if (!parsed) return false;
160
+ const cmp = compareMajorMinor(parsed, parsedRange.bound);
161
+ return parsedRange.kind === "lte" ? cmp <= 0 : cmp > 0;
162
+ }
163
+ var CLAUDE_FAMILY_ID = /^claude-(haiku|sonnet|opus)-(\d+)(?:-\d{8}|[.-](\d+)(?:-\d{8})?)?$/;
164
+ function normalizeHarnessModelId(modelId) {
165
+ let id = modelId.trim().toLowerCase();
166
+ if (!id.includes("/")) {
167
+ if (id.startsWith("claude-")) id = `anthropic/${id}`;
168
+ else if (id.startsWith("gpt-")) id = `openai/${id}`;
169
+ }
170
+ if (id.startsWith("anthropic/")) {
171
+ const slug = id.slice("anthropic/".length);
172
+ const match = slug.match(CLAUDE_FAMILY_ID);
173
+ if (match) {
174
+ const [, family, major, minor] = match;
175
+ id = `anthropic/claude-${family}-${major}${minor ? `.${minor}` : ""}`;
176
+ }
177
+ }
178
+ return id;
179
+ }
180
+ function notVerifiedReason(harnessId, runtimeVersion) {
181
+ const version = typeof runtimeVersion === "string" && runtimeVersion.trim() ? runtimeVersion.trim() : "(unknown version)";
182
+ return `not verified for ${harnessId} ${version}`;
183
+ }
184
+ function harnessModelUnsupportedReason(harnessId) {
185
+ const name = harnessDisplayName(harnessId);
186
+ return `the ${name} harness can't run this host's model \u2014 pick a ${name}-compatible model to run the real runtime`;
187
+ }
188
+ function harnessModelSupport(args) {
189
+ const rows = args.rows ?? HARNESS_MODEL_SUPPORT_ROWS;
190
+ const modelId = normalizeHarnessModelId(args.modelId);
191
+ const versionKnown = parseHarnessVersion(args.runtimeVersion) !== void 0;
192
+ const unknown = (evidence) => ({
193
+ status: "unknown",
194
+ reason: notVerifiedReason(args.harnessId, args.runtimeVersion),
195
+ ...evidence ? { evidence } : {}
196
+ });
197
+ for (const row of rows) {
198
+ if (row.harness !== args.harnessId) continue;
199
+ let pattern;
200
+ try {
201
+ pattern = new RegExp(row.familyPattern);
202
+ } catch {
203
+ continue;
204
+ }
205
+ if (!pattern.test(modelId)) continue;
206
+ if (row.versionRange !== "*") {
207
+ if (!versionKnown) return unknown(row);
208
+ if (!harnessVersionInRange(row.versionRange, args.runtimeVersion)) {
209
+ continue;
210
+ }
211
+ }
212
+ switch (row.status) {
213
+ case "supported":
214
+ return { status: "supported", reason: row.evidence, evidence: row };
215
+ case "unsupported":
216
+ return {
217
+ status: "unsupported",
218
+ reason: harnessModelUnsupportedReason(args.harnessId),
219
+ evidence: row
220
+ };
221
+ default:
222
+ return unknown(row);
223
+ }
224
+ }
225
+ return unknown();
226
+ }
227
+
228
+ // server/utils/harness/harness-model-broker.ts
229
+ function harnessRuntimeVersionField(harnessId) {
230
+ const version = harnessPinnedVersion(harnessId);
231
+ return version ? { harnessRuntimeVersion: version } : {};
232
+ }
233
+ function getConvexHttpUrl() {
234
+ const convexHttpUrl = process.env.CONVEX_HTTP_URL;
235
+ if (!convexHttpUrl) {
236
+ throw new Error("CONVEX_HTTP_URL is required for harness model broker");
237
+ }
238
+ return convexHttpUrl;
239
+ }
240
+ function bearerHeader(bearer) {
241
+ const trimmed = bearer.trim();
242
+ return /^Bearer\s/i.test(trimmed) ? trimmed : `Bearer ${trimmed}`;
243
+ }
244
+ function boxRequestFields(box) {
245
+ return box.kind === "computer" ? {
246
+ projectId: box.projectId,
247
+ computerId: box.computerId,
248
+ ...box.executionScope ? { executionScope: box.executionScope } : {}
249
+ } : { sandboxRowId: box.sandboxRowId };
250
+ }
251
+ async function startHarnessModelBroker(args) {
252
+ let url;
253
+ try {
254
+ url = new URL(
255
+ "/web/harness/model-broker/start",
256
+ getConvexHttpUrl()
257
+ ).toString();
258
+ } catch (err) {
259
+ logger.error("[harness-model-broker] missing endpoint config", err);
260
+ return {
261
+ ok: false,
262
+ status: 500,
263
+ error: "Harness model-broker endpoint is not configured"
264
+ };
265
+ }
266
+ let response;
267
+ try {
268
+ response = await fetch(url, {
269
+ method: "POST",
270
+ headers: {
271
+ "content-type": "application/json",
272
+ authorization: bearerHeader(args.bearer)
273
+ },
274
+ body: JSON.stringify({
275
+ ...boxRequestFields(args.box),
276
+ harnessId: args.harnessId,
277
+ ...harnessRuntimeVersionField(args.harnessId),
278
+ modelId: args.modelId,
279
+ ...args.runId ? { runId: args.runId } : {},
280
+ ...args.maxOutputTokens !== void 0 ? { maxOutputTokens: args.maxOutputTokens } : {}
281
+ }),
282
+ signal: args.signal
283
+ });
284
+ } catch (err) {
285
+ logger.error("[harness-model-broker] network error", err);
286
+ return {
287
+ ok: false,
288
+ status: 502,
289
+ error: "Failed to reach harness model-broker endpoint"
290
+ };
291
+ }
292
+ let payload = null;
293
+ try {
294
+ payload = await response.json();
295
+ } catch {
296
+ return {
297
+ ok: false,
298
+ status: response.ok ? 502 : response.status,
299
+ error: `Harness model-broker returned ${response.status} with non-JSON body`
300
+ };
301
+ }
302
+ const validShape = response.ok && payload?.ok === true && typeof payload?.runId === "string" && payload.runId.length > 0 && typeof payload?.proxyBaseUrl === "string" && payload.proxyBaseUrl.length > 0 && typeof payload?.expiresAt === "number" && Number.isFinite(payload.expiresAt) && (payload?.protocol === "anthropic" || payload?.protocol === "openai") && payload?.delivery === "e2b-network-transform";
303
+ if (!validShape) {
304
+ return {
305
+ ok: false,
306
+ status: response.ok ? 502 : response.status,
307
+ error: typeof payload?.error === "string" ? payload.error : `Harness model-broker failed (${response.status})`
308
+ };
309
+ }
310
+ return {
311
+ ok: true,
312
+ runId: payload.runId,
313
+ expiresAt: payload.expiresAt,
314
+ protocol: payload.protocol,
315
+ proxyBaseUrl: payload.proxyBaseUrl,
316
+ delivery: "e2b-network-transform"
317
+ };
318
+ }
319
+ async function registerLocalInstance(args) {
320
+ let url;
321
+ try {
322
+ url = new URL(
323
+ "/web/harness/local-instance/register",
324
+ getConvexHttpUrl()
325
+ ).toString();
326
+ } catch (err) {
327
+ logger.error("[harness-model-broker] missing register endpoint config", err);
328
+ return {
329
+ ok: false,
330
+ status: 500,
331
+ error: "Harness local-instance endpoint is not configured"
332
+ };
333
+ }
334
+ let response;
335
+ try {
336
+ response = await fetch(url, {
337
+ method: "POST",
338
+ headers: {
339
+ "content-type": "application/json",
340
+ authorization: bearerHeader(args.bearer)
341
+ },
342
+ body: JSON.stringify({
343
+ machineId: args.machineId,
344
+ publicKey: args.publicKey
345
+ }),
346
+ ...args.signal ? { signal: args.signal } : {}
347
+ });
348
+ } catch (err) {
349
+ logger.error("[harness-model-broker] register network error", err);
350
+ return {
351
+ ok: false,
352
+ status: 502,
353
+ error: "Failed to reach the harness local-instance endpoint"
354
+ };
355
+ }
356
+ const payload = await response.json().catch(() => null);
357
+ if (!response.ok || payload?.ok !== true || typeof payload?.keyId !== "string") {
358
+ return {
359
+ ok: false,
360
+ status: response.ok ? 502 : response.status,
361
+ error: typeof payload?.error === "string" ? payload.error : `Local instance registration failed (${response.status})`
362
+ };
363
+ }
364
+ return { ok: true, keyId: payload.keyId };
365
+ }
366
+ async function startLoopbackModelBroker(args) {
367
+ let url;
368
+ try {
369
+ url = new URL(
370
+ "/web/harness/model-broker/start",
371
+ getConvexHttpUrl()
372
+ ).toString();
373
+ } catch (err) {
374
+ logger.error("[harness-model-broker] missing endpoint config", err);
375
+ return {
376
+ ok: false,
377
+ status: 500,
378
+ error: "Harness model-broker endpoint is not configured"
379
+ };
380
+ }
381
+ let response;
382
+ try {
383
+ response = await fetch(url, {
384
+ method: "POST",
385
+ headers: {
386
+ "content-type": "application/json",
387
+ authorization: bearerHeader(args.bearer)
388
+ },
389
+ body: JSON.stringify({
390
+ // Declared, not inferred: the backend forks on this, and a caller that
391
+ // means "local" says so rather than being detected by an absent
392
+ // computerId. No box fields travel — the backend refuses a local
393
+ // request that carries one.
394
+ delivery: "inspector-loopback-gateway",
395
+ projectId: args.projectId,
396
+ harnessId: args.harnessId,
397
+ ...harnessRuntimeVersionField(args.harnessId),
398
+ modelId: args.modelId,
399
+ ...args.evalIterationId ? { evalIterationId: args.evalIterationId } : {},
400
+ ...args.journeyRunId ? { journeyRunId: args.journeyRunId, hostId: args.hostId, targetId: args.targetId, sessionIdx: args.sessionIdx } : {},
401
+ machineId: args.machineId,
402
+ keyId: args.keyId,
403
+ ...args.runId ? { runId: args.runId } : {},
404
+ ...args.maxOutputTokens !== void 0 ? { maxOutputTokens: args.maxOutputTokens } : {}
405
+ }),
406
+ ...args.signal ? { signal: args.signal } : {}
407
+ });
408
+ } catch (err) {
409
+ logger.error("[harness-model-broker] network error", err);
410
+ return {
411
+ ok: false,
412
+ status: 502,
413
+ error: "Failed to reach harness model-broker endpoint"
414
+ };
415
+ }
416
+ let payload = null;
417
+ try {
418
+ payload = await response.json();
419
+ } catch {
420
+ return {
421
+ ok: false,
422
+ status: response.ok ? 502 : response.status,
423
+ error: `Harness model-broker returned ${response.status} with non-JSON body`
424
+ };
425
+ }
426
+ const validShape = response.ok && payload?.ok === true && typeof payload?.runId === "string" && payload.runId.length > 0 && typeof payload?.proxyBaseUrl === "string" && payload.proxyBaseUrl.length > 0 && typeof payload?.expiresAt === "number" && Number.isFinite(payload.expiresAt) && (payload?.protocol === "anthropic" || payload?.protocol === "openai") && payload?.delivery === "inspector-loopback-gateway" && // The lease is the point of this delivery. A response without one is not a
427
+ // usable local start, whatever else it says.
428
+ typeof payload?.lease === "string" && payload.lease.length > 0;
429
+ if (!validShape) {
430
+ return {
431
+ ok: false,
432
+ status: response.ok ? 502 : response.status,
433
+ error: typeof payload?.error === "string" ? payload.error : `Harness model-broker failed (${response.status})`
434
+ };
435
+ }
436
+ return {
437
+ ok: true,
438
+ runId: payload.runId,
439
+ expiresAt: payload.expiresAt,
440
+ protocol: payload.protocol,
441
+ proxyBaseUrl: payload.proxyBaseUrl,
442
+ delivery: "inspector-loopback-gateway",
443
+ lease: payload.lease
444
+ };
445
+ }
446
+ async function revokeHarnessModelBroker(args) {
447
+ let url;
448
+ try {
449
+ url = new URL(
450
+ "/web/harness/model-broker/revoke",
451
+ getConvexHttpUrl()
452
+ ).toString();
453
+ } catch (err) {
454
+ logger.error("[harness-model-broker] missing revoke endpoint config", err);
455
+ return { ok: false };
456
+ }
457
+ try {
458
+ const response = await fetch(url, {
459
+ method: "POST",
460
+ headers: {
461
+ "content-type": "application/json",
462
+ authorization: bearerHeader(args.bearer)
463
+ },
464
+ body: JSON.stringify({
465
+ ...args.projectId ? { projectId: args.projectId } : {},
466
+ ...args.computerId ? { computerId: args.computerId } : {},
467
+ runId: args.runId
468
+ }),
469
+ signal: args.signal ? AbortSignal.any([args.signal, AbortSignal.timeout(1e4)]) : AbortSignal.timeout(1e4)
470
+ });
471
+ const payload = await response.json().catch(() => null);
472
+ if (!response.ok || payload?.ok !== true) {
473
+ logger.warn(`[harness-model-broker] revoke returned ${response.status}`);
474
+ return { ok: false };
475
+ }
476
+ return {
477
+ ok: true,
478
+ revoked: payload.revoked,
479
+ networkCleared: payload.networkCleared
480
+ };
481
+ } catch (err) {
482
+ logger.warn("[harness-model-broker] revoke network error", {
483
+ error: err instanceof Error ? err.message : String(err)
484
+ });
485
+ return { ok: false };
486
+ }
487
+ }
488
+ async function reserveHarnessBox(args) {
489
+ let url;
490
+ try {
491
+ url = new URL(
492
+ "/web/harness/model-broker/reserve",
493
+ getConvexHttpUrl()
494
+ ).toString();
495
+ } catch (err) {
496
+ logger.error("[harness-model-broker] missing reserve endpoint config", err);
497
+ return {
498
+ ok: false,
499
+ status: 500,
500
+ error: "Harness model-broker endpoint is not configured"
501
+ };
502
+ }
503
+ let response;
504
+ try {
505
+ response = await fetch(url, {
506
+ method: "POST",
507
+ headers: {
508
+ "content-type": "application/json",
509
+ authorization: bearerHeader(args.bearer)
510
+ },
511
+ body: JSON.stringify({
512
+ ...boxRequestFields(args.box),
513
+ // The same harness + model the lease will name: the backend authorizes a
514
+ // reservation exactly as it authorizes a lease, which for an ephemeral
515
+ // box means checking both against what the run actually pinned.
516
+ harnessId: args.harnessId,
517
+ ...harnessRuntimeVersionField(args.harnessId),
518
+ modelId: args.modelId,
519
+ runId: args.runId
520
+ }),
521
+ signal: args.signal
522
+ });
523
+ } catch (err) {
524
+ logger.error("[harness-model-broker] reserve network error", err);
525
+ return {
526
+ ok: false,
527
+ status: 502,
528
+ error: "Failed to reach harness model-broker endpoint"
529
+ };
530
+ }
531
+ const payload = await response.json().catch(() => null);
532
+ if (!response.ok || payload?.ok !== true) {
533
+ return {
534
+ ok: false,
535
+ status: response.status,
536
+ error: typeof payload?.error === "string" ? payload.error : `Couldn't reserve the computer (${response.status})`
537
+ };
538
+ }
539
+ return {
540
+ ok: true,
541
+ ...typeof payload.expiresAt === "number" ? { expiresAt: payload.expiresAt } : {}
542
+ };
543
+ }
544
+ async function renewHarnessBoxReservation(args) {
545
+ let url;
546
+ try {
547
+ url = new URL(
548
+ "/web/harness/model-broker/reserve/renew",
549
+ getConvexHttpUrl()
550
+ ).toString();
551
+ } catch {
552
+ return { ok: false };
553
+ }
554
+ try {
555
+ const response = await fetch(url, {
556
+ method: "POST",
557
+ headers: {
558
+ "content-type": "application/json",
559
+ authorization: bearerHeader(args.bearer)
560
+ },
561
+ body: JSON.stringify({
562
+ ...boxRequestFields(args.box),
563
+ harnessId: args.harnessId,
564
+ modelId: args.modelId,
565
+ runId: args.runId
566
+ }),
567
+ signal: args.signal
568
+ });
569
+ const payload = await response.json().catch(() => null);
570
+ if (!response.ok || payload?.ok !== true) {
571
+ logger.error(
572
+ `[harness-model-broker] reservation renewal returned ${response.status}`
573
+ );
574
+ return { ok: false };
575
+ }
576
+ return {
577
+ ok: true,
578
+ ...typeof payload.expiresAt === "number" ? { expiresAt: payload.expiresAt } : {}
579
+ };
580
+ } catch (err) {
581
+ logger.error(
582
+ "[harness-model-broker] reservation renewal network error",
583
+ err
584
+ );
585
+ return { ok: false };
586
+ }
587
+ }
588
+ async function releaseHarnessBoxReservation(args) {
589
+ let url;
590
+ try {
591
+ url = new URL(
592
+ "/web/harness/model-broker/reserve/release",
593
+ getConvexHttpUrl()
594
+ ).toString();
595
+ } catch {
596
+ return { ok: false };
597
+ }
598
+ try {
599
+ const response = await fetch(url, {
600
+ method: "POST",
601
+ headers: {
602
+ "content-type": "application/json",
603
+ authorization: bearerHeader(args.bearer)
604
+ },
605
+ body: JSON.stringify({
606
+ ...boxRequestFields(args.box),
607
+ harnessId: args.harnessId,
608
+ modelId: args.modelId,
609
+ runId: args.runId
610
+ }),
611
+ signal: args.signal
612
+ });
613
+ if (!response.ok) {
614
+ logger.warn(
615
+ `[harness-model-broker] reservation release returned ${response.status}`
616
+ );
617
+ return { ok: false };
618
+ }
619
+ return { ok: true };
620
+ } catch (err) {
621
+ logger.error("[harness-model-broker] reservation release network error", err);
622
+ return { ok: false };
623
+ }
624
+ }
625
+
626
+ // server/utils/harness/local/grants.ts
627
+ import { randomUUID } from "crypto";
628
+ import { mkdir, readFile, rename, writeFile, chmod } from "fs/promises";
629
+ import { homedir } from "os";
630
+ import { join, sep } from "path";
631
+ import { realpath, stat } from "fs/promises";
632
+ var LOCAL_HARNESS_GRANT_HEADER = "x-mcpjam-local-harness-grant";
633
+ function localHarnessStateRoot() {
634
+ return join(homedir(), ".mcpjam", "harness-local");
635
+ }
636
+ function grantsFilePath() {
637
+ return join(localHarnessStateRoot(), "grants.json");
638
+ }
639
+ function machineFilePath() {
640
+ return join(localHarnessStateRoot(), "machine.json");
641
+ }
642
+ function emptyState() {
643
+ return { version: 1, workspaces: [], harnessGrants: [] };
644
+ }
645
+ var GRANT_TTL_MS = 12 * 60 * 60 * 1e3;
646
+ var withGrantLock = createLocalStateMutationLock({
647
+ rootDir: localHarnessStateRoot,
648
+ lockFileName: "grants.lock",
649
+ resourceLabel: "grant store"
650
+ });
651
+ function hashGrantBinding(binding) {
652
+ return capabilityFingerprint(
653
+ JSON.stringify([
654
+ binding.scope ?? "attended",
655
+ binding.userId,
656
+ binding.machineId,
657
+ binding.projectId,
658
+ binding.workspaceGrantId,
659
+ binding.harnessId,
660
+ binding.targetKind,
661
+ binding.backend ?? null,
662
+ binding.runtimeId,
663
+ binding.permissionProfile,
664
+ binding.policyVersion,
665
+ // Only isolated bindings carry one, and only they hash it. A native
666
+ // binding has no isolation policy to be bound to, so folding a constant
667
+ // `null` into its hash would change every native hash for a value that
668
+ // means nothing there — invalidating consents that the local policy
669
+ // never touched. A native binding that carries one anyway is refused
670
+ // outright by `verifyLocalHarnessGrant`, not admitted by the hash.
671
+ ...binding.targetKind === "local-isolated" ? [binding.isolationPolicyVersion ?? null] : []
672
+ ])
673
+ );
674
+ }
675
+ var GrantStateUnreadableError = class extends Error {
676
+ };
677
+ function isValidDate(value) {
678
+ return typeof value === "string" && Number.isFinite(Date.parse(value));
679
+ }
680
+ function isWorkspaceGrant(value) {
681
+ if (!value || typeof value !== "object") return false;
682
+ const workspace = value;
683
+ return typeof workspace.workspaceGrantId === "string" && workspace.workspaceGrantId.length > 0 && typeof workspace.canonicalPath === "string" && workspace.canonicalPath.length > 0 && isValidDate(workspace.createdAt);
684
+ }
685
+ function isHarnessGrantBinding(value) {
686
+ if (!value || typeof value !== "object") return false;
687
+ const binding = value;
688
+ const isolatedBackend = binding.backend === "linux-bwrap" || binding.backend === "darwin-seatbelt" || binding.backend === "oci-v1" || binding.backend === "vm-v1";
689
+ return typeof binding.userId === "string" && binding.userId.length > 0 && typeof binding.machineId === "string" && binding.machineId.length > 0 && (binding.scope === void 0 || binding.scope === "attended" || binding.scope === "unattended") && typeof binding.projectId === "string" && binding.projectId.length > 0 && typeof binding.workspaceGrantId === "string" && binding.workspaceGrantId.length > 0 && (binding.harnessId === "claude-code" || binding.harnessId === "codex") && (binding.targetKind === "local-native" || binding.targetKind === "local-isolated") && typeof binding.runtimeId === "string" && binding.runtimeId.length > 0 && (binding.permissionProfile === "read-only" || binding.permissionProfile === "workspace-edits" || binding.permissionProfile === "unrestricted") && typeof binding.policyVersion === "string" && binding.policyVersion.length > 0 && (binding.targetKind === "local-native" ? binding.backend === void 0 && binding.isolationPolicyVersion === void 0 : isolatedBackend && typeof binding.isolationPolicyVersion === "string" && binding.isolationPolicyVersion.length > 0);
690
+ }
691
+ function isPersistedHarnessGrant(value) {
692
+ if (!value || typeof value !== "object") return false;
693
+ const grant = value;
694
+ return typeof grant.grantId === "string" && grant.grantId.length > 0 && typeof grant.tokenHash === "string" && /^[0-9a-f]{64}$/.test(grant.tokenHash) && typeof grant.bindingHash === "string" && /^[0-9a-f]{64}$/.test(grant.bindingHash) && isHarnessGrantBinding(grant.binding) && isValidDate(grant.grantedAt) && isValidDate(grant.expiresAt);
695
+ }
696
+ async function readState() {
697
+ let raw;
698
+ try {
699
+ raw = await readFile(grantsFilePath(), "utf8");
700
+ } catch (error) {
701
+ if (error.code === "ENOENT") return emptyState();
702
+ throw new GrantStateUnreadableError(
703
+ `the local harness grant store could not be read: ${error instanceof Error ? error.message : String(error)}`
704
+ );
705
+ }
706
+ let parsed;
707
+ try {
708
+ parsed = JSON.parse(raw);
709
+ } catch {
710
+ throw new GrantStateUnreadableError(
711
+ "the local harness grant store is corrupt; refusing to treat it as empty or overwrite it"
712
+ );
713
+ }
714
+ if (!parsed || typeof parsed !== "object") {
715
+ throw new GrantStateUnreadableError(
716
+ "the local harness grant store is not an object"
717
+ );
718
+ }
719
+ const record = parsed;
720
+ if (record.version !== 1 || !Array.isArray(record.workspaces) || !Array.isArray(record.harnessGrants) || !record.workspaces.every(isWorkspaceGrant) || !record.harnessGrants.every(isPersistedHarnessGrant)) {
721
+ throw new GrantStateUnreadableError(
722
+ "the local harness grant store has an unrecognized or malformed shape"
723
+ );
724
+ }
725
+ return {
726
+ version: 1,
727
+ workspaces: [...record.workspaces],
728
+ harnessGrants: [...record.harnessGrants]
729
+ };
730
+ }
731
+ async function writeState(state) {
732
+ const dir = localHarnessStateRoot();
733
+ await mkdir(dir, { recursive: true, mode: 448 });
734
+ await chmod(dir, 448).catch(() => {
735
+ });
736
+ await persistCapabilityState(grantsFilePath(), state);
737
+ }
738
+ async function getLocalMachineId() {
739
+ const readExisting = async () => {
740
+ let raw;
741
+ try {
742
+ raw = await readFile(machineFilePath(), "utf8");
743
+ } catch (error) {
744
+ if (error.code === "ENOENT") return null;
745
+ throw new GrantStateUnreadableError(
746
+ `the local harness machine identity could not be read: ${error instanceof Error ? error.message : String(error)}`
747
+ );
748
+ }
749
+ let parsed;
750
+ try {
751
+ parsed = JSON.parse(raw);
752
+ } catch {
753
+ throw new GrantStateUnreadableError(
754
+ "the local harness machine identity is corrupt; refusing to replace it"
755
+ );
756
+ }
757
+ if (!parsed || typeof parsed !== "object") {
758
+ throw new GrantStateUnreadableError(
759
+ "the local harness machine identity has an unrecognized shape"
760
+ );
761
+ }
762
+ const machineId = parsed.machineId;
763
+ if (typeof machineId !== "string" || machineId.length < 8) {
764
+ throw new GrantStateUnreadableError(
765
+ "the local harness machine identity has an invalid id"
766
+ );
767
+ }
768
+ return machineId;
769
+ };
770
+ const existing = await readExisting();
771
+ if (existing !== null) return existing;
772
+ return withGrantLock(async () => {
773
+ const raced = await readExisting();
774
+ if (raced !== null) return raced;
775
+ const machineId = `mach_${randomUUID()}`;
776
+ const dir = localHarnessStateRoot();
777
+ await mkdir(dir, { recursive: true, mode: 448 });
778
+ const file = machineFilePath();
779
+ const tmp = `${file}.tmp`;
780
+ await writeFile(tmp, JSON.stringify({ machineId }), { mode: 384 });
781
+ await rename(tmp, file);
782
+ return machineId;
783
+ });
784
+ }
785
+ function isFilesystemRoot(canonicalPath) {
786
+ return canonicalPath === sep || canonicalPath === "/" || // A drive root: `C:`, `C:\`, `C:/`.
787
+ /^[A-Za-z]:[\\/]?$/.test(canonicalPath) || // A UNC share root: `\\server\share` and its trailing-separator form, and
788
+ // a bare `\\server`. Scoping a session to a whole network share is the
789
+ // same mistake as scoping it to a whole volume, and the earlier rule
790
+ // covered only the volume. Anything BELOW the share
791
+ // (`\\server\share\project`) stays acceptable.
792
+ //
793
+ // Matched on the BACKSLASH form only. Accepting `//` here as well read a
794
+ // POSIX path with a doubled leading slash — `//tmp/project`, which POSIX
795
+ // expressly permits an implementation to keep — as a share root, and
796
+ // refused an ordinary directory. Windows has no such ambiguity to trade
797
+ // against: `realpath` and `path.resolve` both answer `\\server\share`
798
+ // there, so the canonical path this predicate is given is always the
799
+ // backslash form.
800
+ /^\\{2}[^\\/]+([\\/][^\\/]+)?[\\/]?$/.test(canonicalPath);
801
+ }
802
+ async function validateWorkspaceCandidate(rawPath) {
803
+ let canonicalPath;
804
+ try {
805
+ canonicalPath = await realpath(rawPath);
806
+ const info = await stat(canonicalPath);
807
+ if (!info.isDirectory()) {
808
+ return { ok: false, message: "the selection is not a directory" };
809
+ }
810
+ } catch (error) {
811
+ return {
812
+ ok: false,
813
+ message: `the selected workspace could not be resolved: ${error instanceof Error ? error.message : String(error)}`
814
+ };
815
+ }
816
+ const home = await realpath(homedir()).catch(() => homedir());
817
+ const appRoot = join(home, ".mcpjam");
818
+ const workspaces = join(appRoot, "harness-workspaces");
819
+ if (canonicalPath === home || isFilesystemRoot(canonicalPath) || (canonicalPath === appRoot || canonicalPath.startsWith(appRoot + sep)) && !canonicalPath.startsWith(workspaces + sep)) {
820
+ return {
821
+ ok: false,
822
+ message: "pick a project directory rather than your home directory or the filesystem root \u2014 the workspace is what the session is scoped to."
823
+ };
824
+ }
825
+ return { ok: true, canonicalPath };
826
+ }
827
+ function registerWorkspaceGrant(rawPath) {
828
+ return withGrantLock(async () => {
829
+ const candidate = await validateWorkspaceCandidate(rawPath);
830
+ if (!candidate.ok) {
831
+ return { ok: false, message: candidate.message };
832
+ }
833
+ const canonicalPath = candidate.canonicalPath;
834
+ const state = await readState();
835
+ const existing = state.workspaces.find(
836
+ (w) => w.canonicalPath === canonicalPath
837
+ );
838
+ if (existing) return { ok: true, grant: existing };
839
+ const grant = {
840
+ workspaceGrantId: `ws_${randomUUID()}`,
841
+ canonicalPath,
842
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
843
+ };
844
+ state.workspaces.push(grant);
845
+ await writeState(state);
846
+ logger.info("[local-harness] workspace grant registered");
847
+ return { ok: true, grant };
848
+ });
849
+ }
850
+ async function resolveWorkspaceGrant(workspaceGrantId) {
851
+ let state;
852
+ try {
853
+ state = await readState();
854
+ } catch (error) {
855
+ return {
856
+ ok: false,
857
+ message: error instanceof Error ? error.message : "the local harness grant store could not be read"
858
+ };
859
+ }
860
+ const grant = state.workspaces.find(
861
+ (w) => w.workspaceGrantId === workspaceGrantId
862
+ );
863
+ if (!grant) {
864
+ return { ok: false, message: "unknown workspace grant" };
865
+ }
866
+ try {
867
+ const canonical = await realpath(grant.canonicalPath);
868
+ if (canonical !== grant.canonicalPath) {
869
+ return {
870
+ ok: false,
871
+ message: "the granted workspace now resolves somewhere else; re-select it before running locally"
872
+ };
873
+ }
874
+ const info = await stat(canonical);
875
+ if (!info.isDirectory()) {
876
+ return {
877
+ ok: false,
878
+ message: "the granted workspace is no longer a directory"
879
+ };
880
+ }
881
+ const candidate = await validateWorkspaceCandidate(canonical);
882
+ if (!candidate.ok) return candidate;
883
+ return { ok: true, canonicalPath: canonical };
884
+ } catch {
885
+ return {
886
+ ok: false,
887
+ message: "the granted workspace is no longer reachable"
888
+ };
889
+ }
890
+ }
891
+ function grantLocalHarnessConsent(binding, opts) {
892
+ return withGrantLock(async () => {
893
+ const now = opts?.now ?? Date.now();
894
+ const ttlMs = Math.min(opts?.ttlMs ?? GRANT_TTL_MS, GRANT_TTL_MS);
895
+ const token = mintCapabilityToken();
896
+ const grant = {
897
+ grantId: `grant_${randomUUID()}`,
898
+ tokenHash: capabilityFingerprint(token),
899
+ bindingHash: hashGrantBinding(binding),
900
+ binding,
901
+ grantedAt: new Date(now).toISOString(),
902
+ expiresAt: new Date(now + ttlMs).toISOString()
903
+ };
904
+ const state = await readState();
905
+ state.harnessGrants = state.harnessGrants.filter(
906
+ (g) => Date.parse(g.expiresAt) > now
907
+ );
908
+ state.harnessGrants.push(grant);
909
+ await writeState(state);
910
+ logger.info("[local-harness] consent granted", {
911
+ harnessId: binding.harnessId,
912
+ targetKind: binding.targetKind,
913
+ permissionProfile: binding.permissionProfile,
914
+ policyVersion: binding.policyVersion
915
+ });
916
+ return { grantId: grant.grantId, token, expiresAt: grant.expiresAt };
917
+ });
918
+ }
919
+ async function verifyLocalHarnessGrant(token, binding, opts) {
920
+ if (!token || token.length < 16 || token.length > 256) {
921
+ return {
922
+ ok: false,
923
+ reason: "invalid",
924
+ message: "no local harness grant was presented"
925
+ };
926
+ }
927
+ if (binding.policyVersion !== LOCAL_HARNESS_POLICY_VERSION) {
928
+ return {
929
+ ok: false,
930
+ reason: "binding-mismatch",
931
+ message: `this grant was minted under policy ${binding.policyVersion}; the current policy is ${LOCAL_HARNESS_POLICY_VERSION}. The terms changed, so consent is asked again.`
932
+ };
933
+ }
934
+ if (binding.targetKind === "local-isolated") {
935
+ if (binding.isolationPolicyVersion !== LOCAL_ISOLATION_POLICY_VERSION) {
936
+ return {
937
+ ok: false,
938
+ reason: "binding-mismatch",
939
+ message: `this grant was minted under isolation policy ${binding.isolationPolicyVersion ?? "(none)"}; the current isolation policy is ${LOCAL_ISOLATION_POLICY_VERSION}.`
940
+ };
941
+ }
942
+ } else if (binding.isolationPolicyVersion !== void 0) {
943
+ return {
944
+ ok: false,
945
+ reason: "binding-mismatch",
946
+ message: "a native grant must not carry an isolation policy version"
947
+ };
948
+ }
949
+ const bindingHash = hashGrantBinding(binding);
950
+ let state;
951
+ try {
952
+ state = await readState();
953
+ } catch (error) {
954
+ return {
955
+ ok: false,
956
+ reason: "invalid",
957
+ message: error instanceof Error ? error.message : "the local harness grant store could not be read"
958
+ };
959
+ }
960
+ const now = opts?.now ?? Date.now();
961
+ for (const grant of state.harnessGrants) {
962
+ if (!capabilityMatches(token, grant.tokenHash)) continue;
963
+ if (grant.bindingHash !== bindingHash) {
964
+ return {
965
+ ok: false,
966
+ reason: "binding-mismatch",
967
+ message: "the presented grant was issued for different terms (workspace, harness, runtime, target mode, permission profile, or policy version). Local execution never runs under terms the user did not see."
968
+ };
969
+ }
970
+ if (Date.parse(grant.expiresAt) <= now) {
971
+ return {
972
+ ok: false,
973
+ reason: "expired",
974
+ message: "the local harness grant expired; consent is attended and short-lived"
975
+ };
976
+ }
977
+ return { ok: true, grantId: grant.grantId };
978
+ }
979
+ return {
980
+ ok: false,
981
+ reason: "absent",
982
+ message: "no local harness grant matches the presented capability"
983
+ };
984
+ }
985
+ function revokeLocalHarnessGrants(selector) {
986
+ return withGrantLock(async () => {
987
+ const state = await readState();
988
+ const before = state.harnessGrants.length;
989
+ if (!selector) {
990
+ state.harnessGrants = [];
991
+ } else if (selector.userId) {
992
+ state.harnessGrants = state.harnessGrants.filter((g) => g.binding.userId !== selector.userId || selector.projectId !== void 0 && g.binding.projectId !== selector.projectId);
993
+ } else if (selector.grantId) {
994
+ state.harnessGrants = state.harnessGrants.filter(
995
+ (g) => g.grantId !== selector.grantId
996
+ );
997
+ } else if (selector.binding) {
998
+ const hash = hashGrantBinding(selector.binding);
999
+ state.harnessGrants = state.harnessGrants.filter(
1000
+ (g) => g.bindingHash !== hash
1001
+ );
1002
+ }
1003
+ const removed = before - state.harnessGrants.length;
1004
+ if (removed > 0) {
1005
+ await writeState(state);
1006
+ logger.info("[local-harness] consent revoked", { removed });
1007
+ }
1008
+ return removed;
1009
+ });
1010
+ }
1011
+ function pruneExpiredHarnessGrants(now = Date.now()) {
1012
+ return withGrantLock(async () => {
1013
+ const state = await readState();
1014
+ const before = state.harnessGrants.length;
1015
+ state.harnessGrants = state.harnessGrants.filter(
1016
+ (g) => Date.parse(g.expiresAt) > now
1017
+ );
1018
+ const removed = before - state.harnessGrants.length;
1019
+ if (removed > 0) await writeState(state);
1020
+ return removed;
1021
+ });
1022
+ }
1023
+ function forgetWorkspaceGrant(workspaceGrantId) {
1024
+ return withGrantLock(async () => {
1025
+ const state = await readState();
1026
+ state.workspaces = state.workspaces.filter((w) => w.workspaceGrantId !== workspaceGrantId);
1027
+ state.harnessGrants = state.harnessGrants.filter((g) => g.binding.workspaceGrantId !== workspaceGrantId);
1028
+ await writeState(state);
1029
+ });
1030
+ }
1031
+
1032
+ // server/utils/harness/local/availability.ts
1033
+ function isActorEligibleForLocalHarness(actor, scope = "attended") {
1034
+ return !actor.isGuest && !actor.isScenarioSession && (scope === "unattended" || !actor.isJourneySession && (actor.executionScopeKind === void 0 || actor.executionScopeKind === "project"));
1035
+ }
1036
+ function unavailable(status, message) {
1037
+ return { available: false, status, message };
1038
+ }
1039
+ function localHarnessManifestsForDevelopment(manifests = LOCAL_HARNESS_MANIFEST) {
1040
+ const version = process.env.MCPJAM_LOCAL_HARNESS_CONFORMANCE_VERSION?.trim();
1041
+ if (HOSTED_MODE || process.env.ENVIRONMENT !== "dev" || !version) {
1042
+ return manifests;
1043
+ }
1044
+ return Object.fromEntries(
1045
+ Object.entries(manifests).map(([id, manifest]) => [
1046
+ id,
1047
+ manifest.lifecycleConformanceVersion || id !== "claude-code" ? manifest : { ...manifest, lifecycleConformanceVersion: version }
1048
+ ])
1049
+ );
1050
+ }
1051
+ async function resolveLocalHarnessAvailability(query) {
1052
+ const hosted = query.hosted ?? HOSTED_MODE;
1053
+ const enabled = query.killSwitchEnabled ?? LOCAL_HARNESS_ENABLED;
1054
+ const platform = query.platform ?? process.platform;
1055
+ if (hosted) {
1056
+ return unavailable(
1057
+ "hosted",
1058
+ "a hosted Inspector never runs a harness on its own machine"
1059
+ );
1060
+ }
1061
+ if (!enabled) {
1062
+ return unavailable(
1063
+ "server-disabled",
1064
+ "local harness execution is disabled on this server (MCPJAM_LOCAL_HARNESS_ENABLED)"
1065
+ );
1066
+ }
1067
+ if (!isActorEligibleForLocalHarness(query.actor, query.scope)) {
1068
+ return unavailable(
1069
+ "actor-not-eligible",
1070
+ "local execution requires an attended, signed-in member running their own turn. Guests, shared scenario sessions, and swarm-scoped runs run hosted."
1071
+ );
1072
+ }
1073
+ let localMachineId;
1074
+ try {
1075
+ localMachineId = query.localMachineId ?? await getLocalMachineId();
1076
+ } catch (error) {
1077
+ return unavailable(
1078
+ "machine-identity-unavailable",
1079
+ `this Inspector could not establish its local machine identity, so a machine-scoped consent grant cannot be checked: ${error instanceof Error ? error.message : String(error)}`
1080
+ );
1081
+ }
1082
+ if (query.target.machineId !== localMachineId) {
1083
+ return unavailable(
1084
+ "machine-mismatch",
1085
+ "this local execution target was granted on a different machine; consent is per-installation and must be granted again here"
1086
+ );
1087
+ }
1088
+ if (!supportsOwnershipProof(platform)) {
1089
+ return unavailable(
1090
+ "ownership-unprovable",
1091
+ `this Inspector cannot prove ownership of a process tree on ${platform}, so it could not guarantee that stopping a session stops everything it started`
1092
+ );
1093
+ }
1094
+ const target = query.target;
1095
+ const compatibility = resolveLocalCompatibility(
1096
+ {
1097
+ scope: query.scope,
1098
+ harnessId: target.harnessId,
1099
+ platform: currentLocalPlatform(platform),
1100
+ targetKind: target.kind,
1101
+ permissionProfile: target.permissionProfile,
1102
+ ...target.kind === "local-isolated" ? { backend: target.backend } : {},
1103
+ installedAdapterVersion: query.installedAdapterVersion
1104
+ },
1105
+ query.manifests ?? localHarnessManifestsForDevelopment()
1106
+ );
1107
+ if (!compatibility.ok) {
1108
+ return unavailable(compatibility.status, compatibility.message);
1109
+ }
1110
+ const workspace = await resolveWorkspaceGrant(target.workspaceGrantId);
1111
+ if (!workspace.ok) {
1112
+ return unavailable("workspace-grant-invalid", workspace.message);
1113
+ }
1114
+ const runtimeResolution = compatibility.manifest.runtime.source === "managed-bundle" ? await resolveManagedBundle({
1115
+ // Through `manifestWithExpectedBundleDigest`, so this path verifies
1116
+ // against the digest the INSTALLER accepted rather than a second
1117
+ // reading of the same table. They differ only under the documented
1118
+ // development override — and there they differed fatally: a locally
1119
+ // built pack installed and then could never run.
1120
+ manifest: manifestWithExpectedBundleDigest(
1121
+ compatibility.manifest,
1122
+ compatibility.manifest.harnessId,
1123
+ localPackTarget(platform)
1124
+ ),
1125
+ runtimeRoot: query.runtimeRoot,
1126
+ platform: currentLocalPlatform(platform)
1127
+ }) : await resolveSystemInstall({
1128
+ manifest: compatibility.manifest,
1129
+ platform: currentLocalPlatform(platform),
1130
+ // The workspace is writable by the very agent we are about to start,
1131
+ // so a runtime discovered inside it is not a runtime we can hold.
1132
+ forbiddenRoots: [workspace.canonicalPath]
1133
+ });
1134
+ if (!runtimeResolution.ok) {
1135
+ return unavailable("runtime-unavailable", runtimeResolution.message);
1136
+ }
1137
+ const revalidated = await revalidateRuntime(runtimeResolution.runtime);
1138
+ if (!revalidated.ok) {
1139
+ return unavailable("runtime-changed", revalidated.message);
1140
+ }
1141
+ if (runtimeResolution.runtime.runtimeId !== target.runtimeId) {
1142
+ return unavailable(
1143
+ "runtime-changed",
1144
+ "the resolved runtime is not the one this target names; consent is bound to a runtime identity, so it must be re-granted"
1145
+ );
1146
+ }
1147
+ const binding = {
1148
+ scope: query.scope ?? "attended",
1149
+ userId: query.userId,
1150
+ machineId: target.machineId,
1151
+ projectId: query.projectId,
1152
+ workspaceGrantId: target.workspaceGrantId,
1153
+ harnessId: target.harnessId,
1154
+ targetKind: target.kind,
1155
+ ...target.kind === "local-isolated" ? { backend: target.backend } : {},
1156
+ runtimeId: runtimeResolution.runtime.runtimeId,
1157
+ permissionProfile: target.permissionProfile,
1158
+ // Every local target carries the local-harness policy version, because the
1159
+ // argv denylist, environment allowlist, and permission mapping govern an
1160
+ // isolated session as much as a native one. An isolated target ALSO
1161
+ // carries the isolation policy version, so a change to either invalidates
1162
+ // the grants that depend on it — the separation `targets.ts` documents.
1163
+ policyVersion: target.kind === "local-native" ? target.policyVersion : LOCAL_HARNESS_POLICY_VERSION,
1164
+ ...target.kind === "local-isolated" ? { isolationPolicyVersion: target.isolationPolicyVersion } : {}
1165
+ };
1166
+ const consent = await verifyLocalHarnessGrant(query.grantToken, binding);
1167
+ if (!consent.ok) {
1168
+ return unavailable("consent-required", consent.message);
1169
+ }
1170
+ return {
1171
+ available: true,
1172
+ plan: {
1173
+ target,
1174
+ manifest: compatibility.manifest,
1175
+ runtime: runtimeResolution.runtime,
1176
+ workspacePath: workspace.canonicalPath,
1177
+ permissionMode: compatibility.permissionMode,
1178
+ grantId: consent.grantId
1179
+ }
1180
+ };
1181
+ }
1182
+
1183
+ // server/utils/harness/local/node-launcher.ts
1184
+ import { isAbsolute } from "path";
1185
+ var NodeLauncherError = class extends Error {
1186
+ };
1187
+ function resolveNodeLauncher(opts) {
1188
+ const executable = opts.bundledNodePath;
1189
+ if (!executable || !isAbsolute(executable)) {
1190
+ throw new NodeLauncherError(
1191
+ "the harness bridge launcher must be an absolute path to the Node binary inside the verified runtime pack"
1192
+ );
1193
+ }
1194
+ return { executable, requiredEnv: {}, kind: "bundled" };
1195
+ }
1196
+
1197
+ // server/utils/harness/local/model-gateway.ts
1198
+ import { randomBytes, timingSafeEqual } from "crypto";
1199
+ import { execFile as execFile2 } from "child_process";
1200
+ import { readdir as readdir2, readFile as readFile4, readlink as readlink2 } from "fs/promises";
1201
+ import {
1202
+ createServer as createServer2
1203
+ } from "http";
1204
+
1205
+ // server/utils/harness/local/bridge-endpoint.ts
1206
+ import { execFile } from "child_process";
1207
+ import { readdir, readFile as readFile2, readlink } from "fs/promises";
1208
+ import { createConnection, createServer } from "net";
1209
+ import { networkInterfaces } from "os";
1210
+ var LOOPBACK_HOST_V4 = "127.0.0.1";
1211
+ var LOOPBACK_HOST_V6 = "::1";
1212
+ var BridgeExposureError = class extends Error {
1213
+ };
1214
+ function localBridgeUrl(opts) {
1215
+ if (!Number.isInteger(opts.port) || opts.port < 1 || opts.port > 65535) {
1216
+ throw new BridgeExposureError(`implausible bridge port ${opts.port}`);
1217
+ }
1218
+ const scheme = opts.protocol === "ws" ? "ws" : "http";
1219
+ return `${scheme}://${LOOPBACK_HOST_V4}:${opts.port}`;
1220
+ }
1221
+ function nonLoopbackLocalAddresses(interfaces = networkInterfaces()) {
1222
+ const found = [];
1223
+ for (const [name, entries] of Object.entries(interfaces)) {
1224
+ for (const entry of entries ?? []) {
1225
+ if (entry.internal) continue;
1226
+ if (!entry.address) continue;
1227
+ if (!isIpv6LinkLocal(entry.address)) {
1228
+ found.push(entry.address);
1229
+ continue;
1230
+ }
1231
+ if (entry.address.includes("%")) {
1232
+ found.push(entry.address);
1233
+ continue;
1234
+ }
1235
+ if (name) found.push(`${entry.address}%${name}`);
1236
+ }
1237
+ }
1238
+ return found;
1239
+ }
1240
+ function isIpv6LinkLocal(address) {
1241
+ const bare = address.split("%")[0].toLowerCase();
1242
+ return /^fe[89ab][0-9a-f]:/.test(bare);
1243
+ }
1244
+ function canConnect(host, port, timeoutMs) {
1245
+ return new Promise((resolvePromise) => {
1246
+ let done = false;
1247
+ const finish = (reachable) => {
1248
+ if (done) return;
1249
+ done = true;
1250
+ try {
1251
+ socket.destroy();
1252
+ } catch {
1253
+ }
1254
+ resolvePromise(reachable);
1255
+ };
1256
+ const socket = createConnection({ host, port });
1257
+ socket.setTimeout(timeoutMs);
1258
+ socket.once("connect", () => finish(true));
1259
+ socket.once("timeout", () => finish(false));
1260
+ socket.once("error", () => finish(false));
1261
+ });
1262
+ }
1263
+ async function assertBridgePortUnclaimed(args) {
1264
+ const connect = args.connect ?? canConnect;
1265
+ const timeoutMs = args.timeoutMs ?? 500;
1266
+ const hosts = args.hosts ?? [LOOPBACK_HOST_V4, LOOPBACK_HOST_V6];
1267
+ for (const host of hosts) {
1268
+ if (await connect(host, args.port, timeoutMs)) {
1269
+ throw new BridgeExposureError(
1270
+ `port ${args.port} is already accepting connections on ${host} before this session's bridge started, so a listener on it could not be attributed to the bridge. The session stops rather than talking to whatever is there.`
1271
+ );
1272
+ }
1273
+ }
1274
+ }
1275
+ async function reserveLoopbackPort() {
1276
+ return new Promise((resolvePromise, reject) => {
1277
+ const probe = createServer();
1278
+ probe.once("error", reject);
1279
+ probe.listen(0, LOOPBACK_HOST_V4, () => {
1280
+ const address = probe.address();
1281
+ if (address === null || typeof address === "string") {
1282
+ probe.close(() => reject(new Error("could not reserve a bridge port")));
1283
+ return;
1284
+ }
1285
+ const { port } = address;
1286
+ probe.close(() => resolvePromise(port));
1287
+ });
1288
+ });
1289
+ }
1290
+ async function waitForLoopbackListener(args) {
1291
+ const connect = args.connect ?? canConnect;
1292
+ const pollMs = args.pollMs ?? 50;
1293
+ const deadline = Date.now() + (args.timeoutMs ?? 3e4);
1294
+ for (; ; ) {
1295
+ if (await connect(LOOPBACK_HOST_V4, args.port, 500)) return true;
1296
+ if (Date.now() >= deadline) return false;
1297
+ await new Promise((r) => setTimeout(r, pollMs));
1298
+ }
1299
+ }
1300
+ async function assertBridgeLoopbackOnly(args) {
1301
+ const assertAlive = async (when) => {
1302
+ if (args.isBridgeAlive === void 0) return;
1303
+ if (await args.isBridgeAlive()) return;
1304
+ throw new BridgeExposureError(
1305
+ `the supervised harness bridge was no longer running ${when}, so the listener on port ${args.port} cannot be attributed to it`
1306
+ );
1307
+ };
1308
+ await assertAlive("while waiting for it to listen");
1309
+ const ready = await waitForLoopbackListener({
1310
+ port: args.port,
1311
+ ...args.readinessTimeoutMs !== void 0 ? { timeoutMs: args.readinessTimeoutMs } : {},
1312
+ ...args.connect ? { connect: args.connect } : {}
1313
+ });
1314
+ if (!ready) {
1315
+ throw new BridgeExposureError(
1316
+ `the harness bridge never started listening on ${LOOPBACK_HOST_V4}:${args.port}, so its binding could not be verified. The session stops rather than proceeding unverified.`
1317
+ );
1318
+ }
1319
+ await assertBridgeIsLoopbackOnly(args);
1320
+ if (args.bridgePid !== void 0) {
1321
+ await assertBridgeBindingIsLoopback({
1322
+ pid: args.bridgePid,
1323
+ ...args.platform !== void 0 ? { platform: args.platform } : {},
1324
+ ...args.readListenAddresses !== void 0 ? { read: args.readListenAddresses } : {}
1325
+ });
1326
+ }
1327
+ await assertAlive("after its binding was verified");
1328
+ }
1329
+ async function assertBridgeIsLoopbackOnly(args) {
1330
+ const addresses = args.addresses ?? nonLoopbackLocalAddresses();
1331
+ if (addresses.length === 0) return;
1332
+ const timeoutMs = Math.min(args.timeoutMs ?? 500, PROBE_BUDGET_MS);
1333
+ const connect = args.connect ?? canConnect;
1334
+ const results = await Promise.all(
1335
+ addresses.map(async (address) => ({
1336
+ address,
1337
+ reachable: await connect(address, args.port, timeoutMs)
1338
+ }))
1339
+ );
1340
+ const exposed = results.find((result) => result.reachable);
1341
+ if (exposed !== void 0) {
1342
+ throw new BridgeExposureError(
1343
+ `the harness bridge on port ${args.port} accepted a connection on a non-loopback address, so it is reachable from the local network. Local execution stops rather than leaving an agent's control channel published to the LAN.`
1344
+ );
1345
+ }
1346
+ }
1347
+ var PROBE_BUDGET_MS = 1e3;
1348
+ async function readProcessListenAddresses(pid, platform = process.platform) {
1349
+ if (platform === "darwin") return readDarwinListenAddresses(pid);
1350
+ if (platform === "linux") return readLinuxListenAddresses(pid);
1351
+ return null;
1352
+ }
1353
+ function readDarwinListenAddresses(pid) {
1354
+ return new Promise((resolvePromise) => {
1355
+ execFile(
1356
+ "/usr/sbin/lsof",
1357
+ ["-nP", "-a", "-p", String(pid), "-iTCP", "-sTCP:LISTEN"],
1358
+ { timeout: 2e3, maxBuffer: 256 * 1024, encoding: "utf8", env: {} },
1359
+ (error, stdout) => {
1360
+ if (error && error.code !== 1) {
1361
+ resolvePromise(null);
1362
+ return;
1363
+ }
1364
+ const addresses = [];
1365
+ for (const line of String(stdout).split("\n").slice(1)) {
1366
+ const name = line.trim().split(/\s+/).at(-2);
1367
+ if (name === void 0) continue;
1368
+ const host = name.slice(0, name.lastIndexOf(":"));
1369
+ if (host.length > 0) addresses.push(host);
1370
+ }
1371
+ resolvePromise(addresses);
1372
+ }
1373
+ );
1374
+ });
1375
+ }
1376
+ async function readLinuxListenAddresses(pid) {
1377
+ let sockets;
1378
+ try {
1379
+ const fds = await readdir(`/proc/${pid}/fd`);
1380
+ const inodes = await Promise.all(
1381
+ fds.map(async (fd) => {
1382
+ try {
1383
+ const link = await readlink(`/proc/${pid}/fd/${fd}`);
1384
+ const match = /^socket:\[(\d+)\]$/.exec(link);
1385
+ return match?.[1] ?? null;
1386
+ } catch {
1387
+ return null;
1388
+ }
1389
+ })
1390
+ );
1391
+ sockets = new Set(
1392
+ inodes.filter((inode) => inode !== null)
1393
+ );
1394
+ } catch {
1395
+ return null;
1396
+ }
1397
+ if (sockets.size === 0) return [];
1398
+ const addresses = [];
1399
+ for (const table of ["/proc/net/tcp", "/proc/net/tcp6"]) {
1400
+ let raw;
1401
+ try {
1402
+ raw = await readFile2(table, "utf8");
1403
+ } catch {
1404
+ continue;
1405
+ }
1406
+ for (const line of raw.split("\n").slice(1)) {
1407
+ const fields = line.trim().split(/\s+/);
1408
+ if (fields.length < 10) continue;
1409
+ if (fields[3] !== "0A") continue;
1410
+ if (!sockets.has(fields[9])) continue;
1411
+ const host = decodeProcNetAddress(fields[1].split(":")[0]);
1412
+ if (host !== null) addresses.push(host);
1413
+ }
1414
+ }
1415
+ return addresses;
1416
+ }
1417
+ function decodeProcNetAddress(hex) {
1418
+ if (hex.length === 8) {
1419
+ const octets = [6, 4, 2, 0].map((i) => parseInt(hex.slice(i, i + 2), 16));
1420
+ return octets.join(".");
1421
+ }
1422
+ if (hex.length === 32) {
1423
+ const words = [];
1424
+ for (let word = 0; word < 4; word += 1) {
1425
+ const chunk = hex.slice(word * 8, word * 8 + 8);
1426
+ const beChunk = chunk.slice(6, 8) + chunk.slice(4, 6) + chunk.slice(2, 4) + chunk.slice(0, 2);
1427
+ words.push(beChunk.slice(0, 4), beChunk.slice(4, 8));
1428
+ }
1429
+ return words.join(":");
1430
+ }
1431
+ return null;
1432
+ }
1433
+ function normalizeHexGroups(address) {
1434
+ if (!address.includes(":")) return address;
1435
+ return address.split(":").map(
1436
+ (group) => /^[0-9a-f]+$/.test(group) ? group.replace(/^0+(?=.)/, "") : group
1437
+ ).join(":");
1438
+ }
1439
+ function isLoopbackBoundAddress(address) {
1440
+ const bare = normalizeHexGroups(
1441
+ address.split("%")[0].trim().toLowerCase().replace(/^\[|\]$/g, "")
1442
+ );
1443
+ if (bare === "") return false;
1444
+ if (/^127\./.test(bare)) return true;
1445
+ if (bare === "::1" || bare === "0:0:0:0:0:0:0:1") return true;
1446
+ const mapped = /^(?:::ffff:|0:0:0:0:0:ffff:)(.+)$/.exec(bare);
1447
+ if (mapped !== null) {
1448
+ const inner = mapped[1];
1449
+ if (/^127\./.test(inner)) return true;
1450
+ const hexWords = /^([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(inner);
1451
+ if (hexWords !== null && parseInt(hexWords[1], 16) >> 8 === 127)
1452
+ return true;
1453
+ }
1454
+ return false;
1455
+ }
1456
+ async function assertBridgeBindingIsLoopback(args) {
1457
+ const platform = args.platform ?? process.platform;
1458
+ const read = args.read ?? readProcessListenAddresses;
1459
+ const addresses = await read(args.pid, platform);
1460
+ if (addresses === null) return;
1461
+ const offBox = addresses.filter(
1462
+ (address) => !isLoopbackBoundAddress(address)
1463
+ );
1464
+ if (offBox.length > 0) {
1465
+ throw new BridgeExposureError(
1466
+ `the operating system reports the harness bridge listening on ${offBox.join(", ")}, which is not loopback. Local execution stops rather than leaving an agent's control channel published to the LAN.`
1467
+ );
1468
+ }
1469
+ }
1470
+
1471
+ // server/utils/harness/local/instance-key.ts
1472
+ import { createHash, generateKeyPairSync, sign as edSign } from "crypto";
1473
+ import { chmod as chmod2, mkdir as mkdir2, readFile as readFile3, stat as stat2, writeFile as writeFile2 } from "fs/promises";
1474
+ import { join as join2 } from "path";
1475
+ var POP_SCHEME_PREFIX = "mcpjam-pop-v1";
1476
+ var KEY_FILE = "instance-key.json";
1477
+ var KeystoreUnavailableError = class extends Error {
1478
+ };
1479
+ var keystore = null;
1480
+ var cached = null;
1481
+ var cachedKeyId = null;
1482
+ var loading = null;
1483
+ function keyFilePath() {
1484
+ return join2(localHarnessStateRoot(), KEY_FILE);
1485
+ }
1486
+ function toBase64Url(bytes) {
1487
+ return bytes.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
1488
+ }
1489
+ async function loadLocalInstanceKey() {
1490
+ if (cached !== null) return cached;
1491
+ if (loading !== null) return loading;
1492
+ loading = (async () => {
1493
+ const machineId = await getLocalMachineId();
1494
+ const path = keyFilePath();
1495
+ let stored = null;
1496
+ let fileExists = true;
1497
+ try {
1498
+ stored = JSON.parse(await readFile3(path, "utf8"));
1499
+ } catch (error) {
1500
+ fileExists = error?.code !== "ENOENT";
1501
+ stored = null;
1502
+ }
1503
+ if (stored !== null) {
1504
+ try {
1505
+ const privateKeyPem2 = unwrapPrivateKey(stored);
1506
+ await repairKeyFileMode(path);
1507
+ const loaded2 = {
1508
+ machineId,
1509
+ publicKey: stored.publicKey,
1510
+ privateKeyPem: privateKeyPem2
1511
+ };
1512
+ cached = loaded2;
1513
+ return loaded2;
1514
+ } catch (error) {
1515
+ if (error instanceof KeystoreUnavailableError) {
1516
+ throw error;
1517
+ }
1518
+ logger.warn("[local-harness] instance key unreadable; minting a new one", {
1519
+ message: error instanceof Error ? error.message : String(error)
1520
+ });
1521
+ }
1522
+ }
1523
+ const { publicKey, privateKey } = generateKeyPairSync("ed25519");
1524
+ const raw = publicKey.export({ type: "spki", format: "der" });
1525
+ const rawPublic = Buffer.from(raw.subarray(raw.length - 32));
1526
+ const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" }).toString();
1527
+ const next = {
1528
+ protection: "plain",
1529
+ privateKey: Buffer.from(privateKeyPem).toString("base64"),
1530
+ publicKey: toBase64Url(rawPublic),
1531
+ createdAt: Date.now()
1532
+ };
1533
+ if (keystore !== null && keystore.isAvailable()) {
1534
+ next.protection = "os-keystore";
1535
+ next.privateKey = keystore.encrypt(privateKeyPem);
1536
+ }
1537
+ await mkdir2(localHarnessStateRoot(), { recursive: true, mode: 448 });
1538
+ const serialized = `${JSON.stringify(next, null, 2)}
1539
+ `;
1540
+ if (!fileExists) {
1541
+ try {
1542
+ await writeFile2(path, serialized, { mode: 384, flag: "wx" });
1543
+ } catch (error) {
1544
+ if (error?.code !== "EEXIST") throw error;
1545
+ const adopted = await adoptExistingKey(path, machineId);
1546
+ if (adopted !== null) {
1547
+ cached = adopted;
1548
+ return adopted;
1549
+ }
1550
+ await writeFile2(path, serialized, { mode: 384 });
1551
+ }
1552
+ } else {
1553
+ await writeFile2(path, serialized, { mode: 384 });
1554
+ }
1555
+ await repairKeyFileMode(path, true);
1556
+ const loaded = {
1557
+ machineId,
1558
+ publicKey: next.publicKey,
1559
+ privateKeyPem
1560
+ };
1561
+ cached = loaded;
1562
+ return loaded;
1563
+ })();
1564
+ try {
1565
+ return await loading;
1566
+ } finally {
1567
+ loading = null;
1568
+ }
1569
+ }
1570
+ async function adoptExistingKey(path, machineId) {
1571
+ try {
1572
+ const stored = JSON.parse(await readFile3(path, "utf8"));
1573
+ return {
1574
+ machineId,
1575
+ publicKey: stored.publicKey,
1576
+ privateKeyPem: unwrapPrivateKey(stored)
1577
+ };
1578
+ } catch {
1579
+ return null;
1580
+ }
1581
+ }
1582
+ async function repairKeyFileMode(path, quiet = false) {
1583
+ if (process.platform === "win32") return;
1584
+ try {
1585
+ const mode = (await stat2(path)).mode & 511;
1586
+ if ((mode & 63) === 0) return;
1587
+ await chmod2(path, 384);
1588
+ if (!quiet) {
1589
+ logger.warn("[local-harness] instance key file was not owner-only", {
1590
+ // The mode, never the path: the path is a home directory.
1591
+ mode: mode.toString(8)
1592
+ });
1593
+ }
1594
+ } catch {
1595
+ }
1596
+ }
1597
+ function unwrapPrivateKey(stored) {
1598
+ if (stored.protection === "os-keystore") {
1599
+ if (keystore === null || !keystore.isAvailable()) {
1600
+ throw new KeystoreUnavailableError(
1601
+ "this instance key is sealed by the OS keystore, which is not available"
1602
+ );
1603
+ }
1604
+ return keystore.decrypt(stored.privateKey);
1605
+ }
1606
+ return Buffer.from(stored.privateKey, "base64").toString("utf8");
1607
+ }
1608
+ async function readLocalInstanceIdentity() {
1609
+ const key = await loadLocalInstanceKey();
1610
+ return {
1611
+ machineId: key.machineId,
1612
+ publicKey: key.publicKey,
1613
+ keyId: cachedKeyId
1614
+ };
1615
+ }
1616
+ function setRegisteredKeyId(keyId) {
1617
+ cachedKeyId = keyId;
1618
+ }
1619
+ function getRegisteredKeyId() {
1620
+ return cachedKeyId;
1621
+ }
1622
+ async function signProxiedRequest(args) {
1623
+ const key = await loadLocalInstanceKey();
1624
+ const timestampMs = args.timestampMs ?? Date.now();
1625
+ const signingString = [
1626
+ POP_SCHEME_PREFIX,
1627
+ args.method.toUpperCase(),
1628
+ args.path,
1629
+ String(timestampMs),
1630
+ args.nonce,
1631
+ args.jti
1632
+ ].join("\n");
1633
+ const signature = edSign(
1634
+ null,
1635
+ Buffer.from(signingString, "utf8"),
1636
+ key.privateKeyPem
1637
+ );
1638
+ return `${timestampMs}.${args.nonce}.${toBase64Url(signature)}`;
1639
+ }
1640
+ function leaseJti(lease) {
1641
+ const parts = lease.split(".");
1642
+ if (parts.length !== 3) return null;
1643
+ try {
1644
+ const payload = JSON.parse(
1645
+ Buffer.from(parts[1], "base64url").toString("utf8")
1646
+ );
1647
+ return typeof payload.jti === "string" && payload.jti.length > 0 ? payload.jti : null;
1648
+ } catch {
1649
+ return null;
1650
+ }
1651
+ }
1652
+ function instanceKeyFingerprint(publicKey) {
1653
+ return createHash("sha256").update(publicKey).digest("hex").slice(0, 12);
1654
+ }
1655
+
1656
+ // server/utils/harness/local/model-gateway.ts
1657
+ var MAX_REQUEST_BYTES = 24 * 1024 * 1024;
1658
+ var UPSTREAM_TIMEOUT_MS = 10 * 60 * 1e3;
1659
+ var ALLOWED_PATHS = [
1660
+ { method: "POST", prefix: "/v1/messages" },
1661
+ { method: "POST", prefix: "/v1/messages/count_tokens" },
1662
+ { method: "POST", prefix: "/chat/completions" },
1663
+ { method: "POST", prefix: "/responses" }
1664
+ ];
1665
+ var HELLO_PROBE = { method: "HEAD", path: "/api/hello" };
1666
+ async function startLocalModelGateway(options) {
1667
+ const sessionCapability = randomBytes(32).toString("base64url");
1668
+ const capabilityBytes = Buffer.from(sessionCapability, "utf8");
1669
+ const parsedJti = leaseJti(options.lease);
1670
+ if (parsedJti === null) {
1671
+ throw new Error(
1672
+ "the harness model lease is not a token this gateway can bind a proof of possession to"
1673
+ );
1674
+ }
1675
+ const jti = parsedJti;
1676
+ const upstream = new URL(options.upstreamBaseUrl);
1677
+ if (upstream.protocol !== "https:" && !isLoopbackHost(upstream.host)) {
1678
+ throw new Error(
1679
+ "the harness model proxy must be reached over https unless it is on loopback; refusing to put the lease on the wire in the clear"
1680
+ );
1681
+ }
1682
+ const upstreamBasePath = upstream.pathname.replace(/\/+$/, "");
1683
+ const platform = options.platform ?? process.platform;
1684
+ const doFetch = options.fetchImpl ?? fetch;
1685
+ const resolvePeer = options.resolvePeerPid ?? resolvePeerPid;
1686
+ let revoked = false;
1687
+ const stats = {
1688
+ requests: 0,
1689
+ rejected: 0,
1690
+ forwarded: 0,
1691
+ upstreamErrors: 0
1692
+ };
1693
+ const refuse = (res, status, reason) => {
1694
+ stats.rejected += 1;
1695
+ res.writeHead(status, { "content-type": "application/json" });
1696
+ res.end(
1697
+ JSON.stringify({
1698
+ type: "error",
1699
+ error: { type: "authentication_error", message: reason }
1700
+ })
1701
+ );
1702
+ };
1703
+ const server = createServer2((req, res) => {
1704
+ void handleRequest(req, res).catch((error) => {
1705
+ logger.warn("[local-harness] gateway request failed", {
1706
+ message: error instanceof Error ? error.message : String(error)
1707
+ });
1708
+ if (!res.headersSent) refuse(res, 500, "gateway error");
1709
+ else res.end();
1710
+ });
1711
+ });
1712
+ async function handleRequest(req, res) {
1713
+ stats.requests += 1;
1714
+ if (!isLoopbackHost(req.headers.host)) {
1715
+ refuse(res, 403, "host not allowed");
1716
+ return;
1717
+ }
1718
+ if (req.headers.origin !== void 0) {
1719
+ refuse(res, 403, "origin not allowed");
1720
+ return;
1721
+ }
1722
+ const method = (req.method ?? "GET").toUpperCase();
1723
+ const path = req.url ?? "/";
1724
+ if (method === HELLO_PROBE.method && stripQuery(path) === HELLO_PROBE.path) {
1725
+ res.writeHead(200);
1726
+ res.end();
1727
+ return;
1728
+ }
1729
+ if (revoked) {
1730
+ refuse(res, 401, "session ended");
1731
+ return;
1732
+ }
1733
+ if (!isAllowedPath(method, path)) {
1734
+ refuse(res, 404, "endpoint not allowed");
1735
+ return;
1736
+ }
1737
+ if (!capabilityMatches2(req, capabilityBytes)) {
1738
+ refuse(res, 401, "capability rejected");
1739
+ return;
1740
+ }
1741
+ if (options.isSupervisedPid !== void 0) {
1742
+ const peerPid = await resolvePeer(req.socket, platform);
1743
+ if (peerPid !== null && !await options.isSupervisedPid(peerPid)) {
1744
+ refuse(res, 403, "caller not part of this session");
1745
+ return;
1746
+ }
1747
+ }
1748
+ const body = await readBoundedBody(req);
1749
+ if (body === null) {
1750
+ refuse(res, 413, "request too large");
1751
+ return;
1752
+ }
1753
+ const target = new URL(`${upstreamBasePath}${path}`, upstream.origin);
1754
+ if (target.origin !== upstream.origin || !isUnderBasePath(target.pathname, upstreamBasePath)) {
1755
+ refuse(res, 404, "endpoint not allowed");
1756
+ return;
1757
+ }
1758
+ const pop = await signProxiedRequest({
1759
+ method,
1760
+ path: stripQuery(target.pathname),
1761
+ jti,
1762
+ nonce: randomBytes(16).toString("base64url")
1763
+ });
1764
+ let upstreamResponse;
1765
+ try {
1766
+ upstreamResponse = await doFetch(target.toString(), {
1767
+ method,
1768
+ headers: {
1769
+ // Rebuilt, not forwarded: the child's headers are not ours to relay,
1770
+ // and the two that matter here are ours to set.
1771
+ "content-type": "application/json",
1772
+ accept: String(req.headers.accept ?? "application/json"),
1773
+ authorization: `Bearer ${options.lease}`,
1774
+ "x-mcpjam-harness-lease": options.lease,
1775
+ "x-mcpjam-pop": pop,
1776
+ ...typeof req.headers["anthropic-version"] === "string" ? { "anthropic-version": req.headers["anthropic-version"] } : {},
1777
+ ...typeof req.headers["anthropic-beta"] === "string" ? { "anthropic-beta": req.headers["anthropic-beta"] } : {}
1778
+ },
1779
+ body,
1780
+ // A cross-origin redirect strips `Authorization` per the fetch spec and
1781
+ // strips neither `x-mcpjam-harness-lease` nor the proof of possession,
1782
+ // so following one would hand the lease to whoever the upstream named.
1783
+ // Our upstream has no reason to redirect; if it does, that is a 502.
1784
+ redirect: "error",
1785
+ signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS)
1786
+ });
1787
+ } catch (error) {
1788
+ stats.upstreamErrors += 1;
1789
+ logger.warn("[local-harness] gateway upstream error", {
1790
+ message: error instanceof Error ? error.message : String(error)
1791
+ });
1792
+ refuse(res, 502, "upstream unavailable");
1793
+ return;
1794
+ }
1795
+ stats.forwarded += 1;
1796
+ const responseHeaders = {};
1797
+ upstreamResponse.headers.forEach((value, name) => {
1798
+ if (name === "content-encoding" || name === "content-length" || name === "transfer-encoding" || name === "connection") {
1799
+ return;
1800
+ }
1801
+ responseHeaders[name] = value;
1802
+ });
1803
+ res.writeHead(upstreamResponse.status, responseHeaders);
1804
+ if (upstreamResponse.body === null) {
1805
+ res.end();
1806
+ return;
1807
+ }
1808
+ const reader = upstreamResponse.body.getReader();
1809
+ let clientGone = false;
1810
+ const noteClose = () => {
1811
+ if (!res.writableFinished) clientGone = true;
1812
+ };
1813
+ res.on("close", noteClose);
1814
+ let completed = false;
1815
+ try {
1816
+ while (!clientGone) {
1817
+ const { done, value } = await reader.read();
1818
+ if (done) {
1819
+ completed = true;
1820
+ break;
1821
+ }
1822
+ if (value === void 0 || clientGone) continue;
1823
+ if (!res.write(Buffer.from(value))) await drained(res);
1824
+ }
1825
+ } finally {
1826
+ res.off("close", noteClose);
1827
+ if (!completed) await reader.cancel().catch(() => void 0);
1828
+ reader.releaseLock();
1829
+ res.end();
1830
+ }
1831
+ }
1832
+ const port = await new Promise((resolvePromise, reject) => {
1833
+ server.once("error", reject);
1834
+ server.listen(0, LOOPBACK_HOST_V4, () => {
1835
+ const address = server.address();
1836
+ if (address === null || typeof address === "string") {
1837
+ reject(new Error("the local model gateway did not bind a port"));
1838
+ return;
1839
+ }
1840
+ resolvePromise(address.port);
1841
+ });
1842
+ });
1843
+ return {
1844
+ baseUrl: `http://${LOOPBACK_HOST_V4}:${port}`,
1845
+ port,
1846
+ sessionCapability,
1847
+ revoke: () => {
1848
+ revoked = true;
1849
+ },
1850
+ close: async () => {
1851
+ revoked = true;
1852
+ await closeServer(server);
1853
+ },
1854
+ stats: () => ({ ...stats })
1855
+ };
1856
+ }
1857
+ function closeServer(server) {
1858
+ return new Promise((resolvePromise) => {
1859
+ server.close(() => resolvePromise());
1860
+ server.closeAllConnections?.();
1861
+ });
1862
+ }
1863
+ function drained(res) {
1864
+ return new Promise((resolvePromise) => {
1865
+ const finish = () => {
1866
+ res.off("drain", finish);
1867
+ res.off("close", finish);
1868
+ resolvePromise();
1869
+ };
1870
+ res.once("drain", finish);
1871
+ res.once("close", finish);
1872
+ });
1873
+ }
1874
+ function isUnderBasePath(pathname, basePath) {
1875
+ if (basePath === "") return pathname.startsWith("/");
1876
+ return pathname === basePath || pathname.startsWith(`${basePath}/`);
1877
+ }
1878
+ function stripQuery(path) {
1879
+ const index = path.indexOf("?");
1880
+ return index === -1 ? path : path.slice(0, index);
1881
+ }
1882
+ function isLoopbackHost(host) {
1883
+ if (host === void 0) return false;
1884
+ const withoutPort = host.startsWith("[") ? host.slice(1, host.indexOf("]")) : host.split(":")[0] ?? "";
1885
+ return withoutPort === "127.0.0.1" || withoutPort === "localhost" || withoutPort === "::1" || /^127\./.test(withoutPort);
1886
+ }
1887
+ function hasPlainSegments(bare) {
1888
+ let decoded;
1889
+ try {
1890
+ decoded = decodeURIComponent(bare);
1891
+ } catch {
1892
+ return false;
1893
+ }
1894
+ if (/[\\\u0000-\u001f]/.test(decoded)) return false;
1895
+ return decoded.split("/").every((segment) => segment !== "." && segment !== "..");
1896
+ }
1897
+ function isAllowedPath(method, path) {
1898
+ const bare = stripQuery(path);
1899
+ if (!hasPlainSegments(bare)) return false;
1900
+ return ALLOWED_PATHS.some(
1901
+ (entry) => entry.method === method && (bare === entry.prefix || bare.startsWith(`${entry.prefix}/`))
1902
+ );
1903
+ }
1904
+ function capabilityMatches2(req, expected) {
1905
+ const header = req.headers["x-api-key"] ?? req.headers.authorization;
1906
+ const presented = typeof header === "string" ? header.replace(/^Bearer\s+/i, "") : Array.isArray(header) ? "" : "";
1907
+ const presentedBytes = Buffer.from(presented, "utf8");
1908
+ if (presentedBytes.length !== expected.length) return false;
1909
+ return timingSafeEqual(presentedBytes, expected);
1910
+ }
1911
+ async function readBoundedBody(req) {
1912
+ const declared = Number(req.headers["content-length"] ?? "0");
1913
+ if (Number.isFinite(declared) && declared > MAX_REQUEST_BYTES) return null;
1914
+ const chunks = [];
1915
+ let total = 0;
1916
+ for await (const chunk of req) {
1917
+ const buffer = chunk;
1918
+ total += buffer.byteLength;
1919
+ if (total > MAX_REQUEST_BYTES) return null;
1920
+ chunks.push(buffer);
1921
+ }
1922
+ return Buffer.concat(chunks);
1923
+ }
1924
+ async function resolvePeerPid(socket, platform = process.platform) {
1925
+ const remotePort = socket.remotePort;
1926
+ if (remotePort === void 0) return null;
1927
+ if (platform === "darwin") return resolveDarwinPeerPid(remotePort);
1928
+ if (platform === "linux") return resolveLinuxPeerPid(remotePort);
1929
+ return null;
1930
+ }
1931
+ function resolveDarwinPeerPid(remotePort) {
1932
+ return new Promise((resolvePromise) => {
1933
+ execFile2(
1934
+ "/usr/sbin/lsof",
1935
+ ["-nP", "-t", `-iTCP:${remotePort}`, "-sTCP:ESTABLISHED"],
1936
+ { timeout: 1500, maxBuffer: 32 * 1024, encoding: "utf8", env: {} },
1937
+ (error, stdout) => {
1938
+ if (error && error.code !== 1) {
1939
+ resolvePromise(null);
1940
+ return;
1941
+ }
1942
+ const pids = String(stdout).split("\n").map((line) => Number(line.trim())).filter((pid) => Number.isInteger(pid) && pid > 0 && pid !== process.pid);
1943
+ resolvePromise(pids.length === 1 ? pids[0] : null);
1944
+ }
1945
+ );
1946
+ });
1947
+ }
1948
+ async function resolveLinuxPeerPid(remotePort) {
1949
+ const inode = await findLocalSocketInode(remotePort);
1950
+ if (inode === null) return null;
1951
+ let entries;
1952
+ try {
1953
+ entries = await readdir2("/proc");
1954
+ } catch {
1955
+ return null;
1956
+ }
1957
+ for (const entry of entries) {
1958
+ if (!/^\d+$/.test(entry)) continue;
1959
+ const pid = Number(entry);
1960
+ if (pid === process.pid) continue;
1961
+ let fds;
1962
+ try {
1963
+ fds = await readdir2(`/proc/${entry}/fd`);
1964
+ } catch {
1965
+ continue;
1966
+ }
1967
+ for (const fd of fds) {
1968
+ try {
1969
+ const link = await readlink2(`/proc/${entry}/fd/${fd}`);
1970
+ if (link === `socket:[${inode}]`) return pid;
1971
+ } catch {
1972
+ continue;
1973
+ }
1974
+ }
1975
+ }
1976
+ return null;
1977
+ }
1978
+ async function findLocalSocketInode(port) {
1979
+ const hexPort = port.toString(16).toUpperCase().padStart(4, "0");
1980
+ for (const table of ["/proc/net/tcp", "/proc/net/tcp6"]) {
1981
+ let raw;
1982
+ try {
1983
+ raw = await readFile4(table, "utf8");
1984
+ } catch {
1985
+ continue;
1986
+ }
1987
+ for (const line of raw.split("\n").slice(1)) {
1988
+ const fields = line.trim().split(/\s+/);
1989
+ if (fields.length < 10) continue;
1990
+ const local = fields[1] ?? "";
1991
+ if (!local.endsWith(`:${hexPort}`)) continue;
1992
+ return fields[9] ?? null;
1993
+ }
1994
+ }
1995
+ return null;
1996
+ }
1997
+
1998
+ // server/utils/harness/local/supervised-provider.ts
1999
+ import { randomUUID as randomUUID2 } from "crypto";
2000
+ import {
2001
+ lstat as lstat2,
2002
+ mkdir as mkdir3,
2003
+ open,
2004
+ readlink as readlink4,
2005
+ realpath as realpath3,
2006
+ rename as rename2,
2007
+ rm,
2008
+ stat as stat3,
2009
+ symlink
2010
+ } from "fs/promises";
2011
+ import {
2012
+ basename,
2013
+ dirname as dirname2,
2014
+ isAbsolute as isAbsolute3,
2015
+ join as join3,
2016
+ posix as posix4,
2017
+ resolve as resolve2,
2018
+ sep as sep4
2019
+ } from "path";
2020
+
2021
+ // server/utils/harness/local/confine.ts
2022
+ import { lstat, readlink as readlink3, realpath as realpath2 } from "fs/promises";
2023
+ import { dirname, isAbsolute as isAbsolute2, normalize, resolve, sep as sep2 } from "path";
2024
+ var MAX_PATH_LENGTH = 4096;
2025
+ var PathConfinementError = class extends Error {
2026
+ constructor(message, requestedPath) {
2027
+ super(message);
2028
+ this.name = "PathConfinementError";
2029
+ this.requestedPath = requestedPath;
2030
+ }
2031
+ };
2032
+ function isUnder(candidate, root) {
2033
+ return candidate === root || candidate.startsWith(root + sep2);
2034
+ }
2035
+ async function resolveExistingAncestor(path) {
2036
+ const tail = [];
2037
+ let current = path;
2038
+ for (; ; ) {
2039
+ try {
2040
+ return { base: await realpath2(current), tail };
2041
+ } catch {
2042
+ const parent = dirname(current);
2043
+ if (parent === current) {
2044
+ return { base: current, tail };
2045
+ }
2046
+ const cut = parent.endsWith(sep2) ? parent.length : parent.length + 1;
2047
+ tail.unshift(current.slice(cut));
2048
+ current = parent;
2049
+ }
2050
+ }
2051
+ }
2052
+ var MAX_LINK_HOPS = 8;
2053
+ async function resolveDanglingLinks(base, tail, requested) {
2054
+ let current = base;
2055
+ for (let index = 0; index < tail.length; index += 1) {
2056
+ let next = resolve(current, tail[index]);
2057
+ let hops = 0;
2058
+ for (; ; ) {
2059
+ let isLink;
2060
+ try {
2061
+ isLink = (await lstat(next)).isSymbolicLink();
2062
+ } catch (error) {
2063
+ if (error.code !== "ENOENT") {
2064
+ throw new PathConfinementError(
2065
+ "path could not be resolved well enough to confine it",
2066
+ requested
2067
+ );
2068
+ }
2069
+ return resolve(next, ...tail.slice(index + 1));
2070
+ }
2071
+ if (!isLink) {
2072
+ next = await realpath2(next).catch(() => next);
2073
+ break;
2074
+ }
2075
+ if ((hops += 1) > MAX_LINK_HOPS) {
2076
+ throw new PathConfinementError(
2077
+ `path passes through more than ${MAX_LINK_HOPS} symbolic links`,
2078
+ requested
2079
+ );
2080
+ }
2081
+ const link = await readlink3(next);
2082
+ next = isAbsolute2(link) ? normalize(link) : normalize(resolve(dirname(next), link));
2083
+ }
2084
+ current = next;
2085
+ }
2086
+ return current;
2087
+ }
2088
+ async function confinePath(requested, { roots }) {
2089
+ if (typeof requested !== "string" || requested.length === 0) {
2090
+ throw new PathConfinementError("no path was given", String(requested));
2091
+ }
2092
+ if (requested.length > MAX_PATH_LENGTH) {
2093
+ throw new PathConfinementError(
2094
+ `path exceeds ${MAX_PATH_LENGTH} characters`,
2095
+ `${requested.slice(0, 64)}\u2026`
2096
+ );
2097
+ }
2098
+ if (requested.includes("\0")) {
2099
+ throw new PathConfinementError("path contains a NUL byte", "<redacted>");
2100
+ }
2101
+ if (!isAbsolute2(requested)) {
2102
+ throw new PathConfinementError(
2103
+ "path must be absolute; the local provider does not resolve relative paths against an ambient working directory",
2104
+ requested
2105
+ );
2106
+ }
2107
+ if (roots.length === 0) {
2108
+ throw new PathConfinementError(
2109
+ "no writable root is configured for this session",
2110
+ requested
2111
+ );
2112
+ }
2113
+ const normalized = normalize(requested);
2114
+ const { base, tail } = await resolveExistingAncestor(normalized);
2115
+ const canonical = await resolveDanglingLinks(base, tail, requested);
2116
+ for (const root of roots) {
2117
+ if (isUnder(canonical, root)) return canonical;
2118
+ }
2119
+ throw new PathConfinementError(
2120
+ "path resolves outside every directory this session was granted. The workspace grant and the session state directory are the only roots the Inspector file API will touch.",
2121
+ requested
2122
+ );
2123
+ }
2124
+
2125
+ // server/utils/harness/local/adapter-path.ts
2126
+ import { posix, win32 } from "path";
2127
+ var AdapterPathError = class extends Error {
2128
+ };
2129
+ var MSYS_DRIVE_PATH = /^\/([A-Za-z])(?:\/(.*))?$/;
2130
+ var NATIVE_DRIVE_PATH = /^([A-Za-z]):[\\/]/;
2131
+ function toAdapterPath(nativePath, platform = process.platform) {
2132
+ if (platform !== "win32") return nativePath;
2133
+ const normalized = win32.normalize(nativePath);
2134
+ const drive = NATIVE_DRIVE_PATH.exec(normalized);
2135
+ if (drive === null) {
2136
+ throw new AdapterPathError(
2137
+ `cannot present ${JSON.stringify(nativePath)} to the adapter: only a drive-letter absolute path has a POSIX-shaped spelling`
2138
+ );
2139
+ }
2140
+ const rest = normalized.slice(drive[0].length).split(win32.sep).filter((segment) => segment.length > 0).join("/");
2141
+ return rest.length === 0 ? `/${drive[1].toLowerCase()}` : `/${drive[1].toLowerCase()}/${rest}`;
2142
+ }
2143
+ function fromAdapterPath(adapterPath, platform = process.platform) {
2144
+ if (platform !== "win32") return adapterPath;
2145
+ const match = MSYS_DRIVE_PATH.exec(adapterPath);
2146
+ if (match === null) return adapterPath;
2147
+ const drive = `${match[1].toUpperCase()}:`;
2148
+ const rest = match[2] ?? "";
2149
+ const normalizedRest = posix.normalize(`/${rest}`);
2150
+ return win32.normalize(`${drive}${normalizedRest}`);
2151
+ }
2152
+
2153
+ // server/utils/harness/local/command-translation.ts
2154
+ import { normalize as normalize2, posix as posix2, sep as sep3 } from "path";
2155
+
2156
+ // server/utils/harness/local/argv-policy.ts
2157
+ var MAX_ARG_LENGTH = 4096;
2158
+ var MAX_ARG_COUNT = 64;
2159
+ var DENIED_ARGV_FLAG_VALUES = {
2160
+ "--ask-for-approval": ["never"],
2161
+ "--sandbox": ["danger-full-access"],
2162
+ "--sandbox-mode": ["danger-full-access"],
2163
+ "--permission-mode": ["bypasspermissions"],
2164
+ "--approval-policy": ["never"]
2165
+ };
2166
+ var DENIED_ARGV_CAPABILITIES = [
2167
+ // Claude Code
2168
+ "--dangerously-skip-permissions",
2169
+ "--dangerously-allow-browser",
2170
+ "--permission-mode=bypasspermissions",
2171
+ "bypasspermissions",
2172
+ // Codex / OpenAI
2173
+ "--dangerously-bypass-approvals-and-sandbox",
2174
+ "--yolo",
2175
+ "--full-auto",
2176
+ "danger-full-access",
2177
+ "--ask-for-approval=never",
2178
+ // Generic shapes seen across vendor CLIs
2179
+ "--no-sandbox",
2180
+ "--disable-sandbox",
2181
+ "--skip-permissions",
2182
+ "--allow-all",
2183
+ "--trust-all"
2184
+ ];
2185
+ var FORBIDDEN_ARG_CHARS = /[\0\n\r;|&`$<>]/;
2186
+ var ArgvPolicyViolation = class extends Error {
2187
+ constructor(message, rule, argument) {
2188
+ super(message);
2189
+ this.name = "ArgvPolicyViolation";
2190
+ this.rule = rule;
2191
+ this.argument = argument;
2192
+ }
2193
+ };
2194
+ function normalizeForCapabilityCheck(arg) {
2195
+ const lower = arg.toLowerCase();
2196
+ const eq = lower.indexOf("=");
2197
+ return eq === -1 ? [lower] : [lower, lower.slice(0, eq), lower.slice(eq + 1)];
2198
+ }
2199
+ function assertArgumentAllowed(arg) {
2200
+ if (typeof arg !== "string") {
2201
+ throw new ArgvPolicyViolation(
2202
+ "argv entries must be strings",
2203
+ "structure",
2204
+ String(arg)
2205
+ );
2206
+ }
2207
+ if (arg.length === 0) {
2208
+ throw new ArgvPolicyViolation("empty argv entry", "structure", arg);
2209
+ }
2210
+ if (arg.length > MAX_ARG_LENGTH) {
2211
+ throw new ArgvPolicyViolation(
2212
+ `argv entry exceeds ${MAX_ARG_LENGTH} characters`,
2213
+ "structure",
2214
+ `${arg.slice(0, 64)}\u2026`
2215
+ );
2216
+ }
2217
+ const forbidden = FORBIDDEN_ARG_CHARS.exec(arg);
2218
+ if (forbidden) {
2219
+ throw new ArgvPolicyViolation(
2220
+ `argv entry contains shell metacharacter ${JSON.stringify(
2221
+ forbidden[0]
2222
+ )} \u2014 the supervisor never uses a shell, so this argument was built from a command string rather than structured input`,
2223
+ "structure",
2224
+ arg
2225
+ );
2226
+ }
2227
+ const eq = arg.indexOf("=");
2228
+ if (eq !== -1) {
2229
+ const flag = arg.slice(0, eq).toLowerCase();
2230
+ const value = arg.slice(eq + 1).toLowerCase();
2231
+ if (DENIED_ARGV_FLAG_VALUES[flag]?.includes(value)) {
2232
+ throw new ArgvPolicyViolation(
2233
+ `argv entry ${JSON.stringify(
2234
+ arg
2235
+ )} requests a value that disables the vendor permission controls local execution depends on`,
2236
+ "capability",
2237
+ arg
2238
+ );
2239
+ }
2240
+ }
2241
+ const candidates = normalizeForCapabilityCheck(arg);
2242
+ for (const denied of DENIED_ARGV_CAPABILITIES) {
2243
+ if (candidates.includes(denied)) {
2244
+ throw new ArgvPolicyViolation(
2245
+ `argv entry ${JSON.stringify(arg)} requests ${JSON.stringify(
2246
+ denied
2247
+ )}, which disables the vendor permission controls local execution depends on`,
2248
+ "capability",
2249
+ arg
2250
+ );
2251
+ }
2252
+ }
2253
+ }
2254
+ function assertArgvAllowed(args) {
2255
+ if (args.length > MAX_ARG_COUNT) {
2256
+ throw new ArgvPolicyViolation(
2257
+ `argv has ${args.length} entries (max ${MAX_ARG_COUNT})`,
2258
+ "count",
2259
+ ""
2260
+ );
2261
+ }
2262
+ for (const arg of args) assertArgumentAllowed(arg);
2263
+ for (let i = 0; i < args.length - 1; i += 1) {
2264
+ const flag = args[i].toLowerCase();
2265
+ const denied = DENIED_ARGV_FLAG_VALUES[flag];
2266
+ if (denied && denied.includes(args[i + 1].toLowerCase())) {
2267
+ throw new ArgvPolicyViolation(
2268
+ `argv requests ${JSON.stringify(`${args[i]} ${args[i + 1]}`)}, which disables the vendor permission controls local execution depends on`,
2269
+ "capability",
2270
+ args[i]
2271
+ );
2272
+ }
2273
+ }
2274
+ return args;
2275
+ }
2276
+
2277
+ // server/utils/harness/local/command-translation.ts
2278
+ var CommandTranslationError = class extends Error {
2279
+ constructor(message, command) {
2280
+ super(message);
2281
+ this.name = "CommandTranslationError";
2282
+ this.command = command;
2283
+ }
2284
+ };
2285
+ var PWD_PROBE = "pwd";
2286
+ var HOME_PROBE = 'printf "%s" "$HOME"';
2287
+ var ENV_MKDIR_SHAPES = {
2288
+ 'mkdir -p "$BOOTSTRAP_DIR"': "BOOTSTRAP_DIR",
2289
+ 'mkdir -p "$WORK_DIR"': "WORK_DIR"
2290
+ };
2291
+ function splitQuotedTokens(operands, command) {
2292
+ const paths = [];
2293
+ let i = 0;
2294
+ while (i < operands.length) {
2295
+ if (operands[i] === " ") {
2296
+ i += 1;
2297
+ continue;
2298
+ }
2299
+ let token = "";
2300
+ while (i < operands.length && operands[i] !== " ") {
2301
+ if (operands[i] === "'") {
2302
+ const end = operands.indexOf("'", i + 1);
2303
+ if (end === -1) {
2304
+ throw new CommandTranslationError(
2305
+ "unterminated single quote in mkdir operands",
2306
+ command
2307
+ );
2308
+ }
2309
+ const inner = operands.slice(i + 1, end);
2310
+ if (inner.includes("\\")) {
2311
+ throw new CommandTranslationError(
2312
+ "backslash escape in a quoted mkdir operand",
2313
+ command
2314
+ );
2315
+ }
2316
+ token += inner;
2317
+ i = end + 1;
2318
+ continue;
2319
+ }
2320
+ token += operands[i];
2321
+ i += 1;
2322
+ }
2323
+ if (token.length > 0) paths.push(token);
2324
+ }
2325
+ return paths;
2326
+ }
2327
+ function assertPlainPathOperand(path, command) {
2328
+ if (path.length === 0 || path.length > 4096) {
2329
+ throw new CommandTranslationError("implausible path operand", command);
2330
+ }
2331
+ if (/[\0\n\r"`$*?[\]{}\\|&;<>()!~]/.test(path)) {
2332
+ throw new CommandTranslationError(
2333
+ `path operand ${JSON.stringify(
2334
+ path
2335
+ )} contains a shell metacharacter or glob \u2014 the adapters emit literal paths, so this is not a shape we know`,
2336
+ command
2337
+ );
2338
+ }
2339
+ if (!posix2.isAbsolute(path)) {
2340
+ throw new CommandTranslationError(
2341
+ `path operand ${JSON.stringify(path)} is not absolute`,
2342
+ command
2343
+ );
2344
+ }
2345
+ }
2346
+ function underBootstrapDir(path, bootstrapDir) {
2347
+ const p = posix2.normalize(path);
2348
+ const root = posix2.normalize(bootstrapDir);
2349
+ return p === root || p.startsWith(root + "/");
2350
+ }
2351
+ function remapBootstrapPath(path, ctx) {
2352
+ const root = posix2.normalize(ctx.adapterBootstrapDir);
2353
+ const normalizedPath = posix2.normalize(path);
2354
+ if (!underBootstrapDir(normalizedPath, root)) {
2355
+ throw new CommandTranslationError(
2356
+ `path ${JSON.stringify(path)} was given where a path inside the adapter bootstrap directory was expected`,
2357
+ path
2358
+ );
2359
+ }
2360
+ const rest = normalizedPath.slice(root.length).replace(/^\//, "");
2361
+ const mapped = rest ? `${ctx.managedBundleRoot}${sep3}${rest.split("/").join(sep3)}` : ctx.managedBundleRoot;
2362
+ const normalized = normalize2(mapped);
2363
+ if (normalized !== ctx.managedBundleRoot && !normalized.startsWith(ctx.managedBundleRoot + sep3)) {
2364
+ throw new CommandTranslationError(
2365
+ `bootstrap path ${JSON.stringify(path)} escapes the managed bundle root`,
2366
+ path
2367
+ );
2368
+ }
2369
+ return normalized;
2370
+ }
2371
+ var BOOTSTRAP_MARKER = /^\.bootstrap-[A-Za-z0-9._-]{1,128}\.ok$/;
2372
+ function classifyBootstrapPath(path, ctx) {
2373
+ const root = posix2.normalize(ctx.adapterBootstrapDir);
2374
+ const normalized = posix2.normalize(path);
2375
+ if (!underBootstrapDir(normalized, root)) return { kind: "workspace" };
2376
+ const relative = normalized.slice(root.length).replace(/^\//, "");
2377
+ if (relative.length === 0) {
2378
+ throw new CommandTranslationError(
2379
+ `the adapter bootstrap directory itself is not a file`,
2380
+ path
2381
+ );
2382
+ }
2383
+ if (BOOTSTRAP_MARKER.test(relative)) {
2384
+ return {
2385
+ kind: "session-overlay",
2386
+ overlayPath: `${ctx.bootstrapOverlayDir}${sep3}${relative}`
2387
+ };
2388
+ }
2389
+ if (ctx.adapterBootstrapFiles.includes(relative)) {
2390
+ return {
2391
+ kind: "bundle-asset",
2392
+ bundlePath: remapBootstrapPath(normalized, ctx),
2393
+ relativePath: relative
2394
+ };
2395
+ }
2396
+ throw new CommandTranslationError(
2397
+ `file ${JSON.stringify(relative)} is not part of the pinned ${ctx.harnessId} bootstrap recipe, so this session will not create it. An adapter upgrade that adds a bootstrap file needs a manifest review.`,
2398
+ path
2399
+ );
2400
+ }
2401
+ function assertUnderSyntheticHome(path, ctx, command) {
2402
+ const home = posix2.normalize(ctx.syntheticHome);
2403
+ const p = posix2.normalize(path);
2404
+ if (p === home || !p.startsWith(home + "/")) {
2405
+ throw new CommandTranslationError(
2406
+ `path operand ${JSON.stringify(path)} is outside the session's synthetic home; the skills-write shapes are only honoured there`,
2407
+ command
2408
+ );
2409
+ }
2410
+ }
2411
+ async function translateSkillsMove(command, ctx) {
2412
+ const ops = splitQuotedTokens(command.slice("mv -f ".length), command);
2413
+ if (ops.length !== 2) {
2414
+ throw new CommandTranslationError("mv -f needs exactly two operands", command);
2415
+ }
2416
+ for (const p of ops) {
2417
+ assertPlainPathOperand(p, command);
2418
+ assertUnderSyntheticHome(p, ctx, command);
2419
+ }
2420
+ return {
2421
+ kind: "rename",
2422
+ from: await ctx.confine(ops[0]),
2423
+ to: await ctx.confine(ops[1])
2424
+ };
2425
+ }
2426
+ async function translateSkillsProbe(command, ctx) {
2427
+ const ops = splitQuotedTokens(command.slice("test ! -e ".length), command);
2428
+ if (ops.length !== 1) {
2429
+ throw new CommandTranslationError("test ! -e needs exactly one operand", command);
2430
+ }
2431
+ assertPlainPathOperand(ops[0], command);
2432
+ assertUnderSyntheticHome(ops[0], ctx, command);
2433
+ return { kind: "probe-absent", path: await ctx.confine(ops[0]) };
2434
+ }
2435
+ async function translateSkillsRemove(command, ctx) {
2436
+ const ops = splitQuotedTokens(command.slice("rm -rf -- ".length), command);
2437
+ if (ops.length === 0) {
2438
+ throw new CommandTranslationError("rm -rf -- with no operands", command);
2439
+ }
2440
+ const paths = [];
2441
+ for (const p of ops) {
2442
+ assertPlainPathOperand(p, command);
2443
+ assertUnderSyntheticHome(p, ctx, command);
2444
+ paths.push(await ctx.confine(p));
2445
+ }
2446
+ return { kind: "remove", paths };
2447
+ }
2448
+ async function translateMkdir(command, ctx) {
2449
+ const raw = splitQuotedTokens(command.slice("mkdir -p ".length), command);
2450
+ if (raw.length === 0) {
2451
+ throw new CommandTranslationError("mkdir -p with no operands", command);
2452
+ }
2453
+ const paths = [];
2454
+ for (const path of raw) {
2455
+ assertPlainPathOperand(path, command);
2456
+ if (underBootstrapDir(path, posix2.normalize(ctx.adapterBootstrapDir))) {
2457
+ continue;
2458
+ }
2459
+ paths.push(await ctx.confine(path));
2460
+ }
2461
+ if (paths.length === 0) {
2462
+ return {
2463
+ kind: "noop",
2464
+ reason: "mkdir targeted only the managed runtime bundle, which exists"
2465
+ };
2466
+ }
2467
+ return { kind: "mkdir", paths };
2468
+ }
2469
+ function matchBootstrapInstall(command, ctx) {
2470
+ if (command === "pnpm install --frozen-lockfile --store-dir .pnpm-store") {
2471
+ return {
2472
+ kind: "noop",
2473
+ reason: "vendor dependency graph is supplied by the digest-verified managed runtime bundle; no package manager runs during a session"
2474
+ };
2475
+ }
2476
+ if (ctx.harnessId === "claude-code" && command === "./node_modules/.bin/claude --version") {
2477
+ return {
2478
+ kind: "noop",
2479
+ reason: "vendor CLI is installed and version-verified in the managed runtime bundle at build time; the session does not re-run its version probe"
2480
+ };
2481
+ }
2482
+ return null;
2483
+ }
2484
+ async function matchBridgeLaunch(command, ctx) {
2485
+ if (!command.startsWith("node ")) return null;
2486
+ const tokens = splitQuotedTokens(command.slice("node ".length), command);
2487
+ const expected = {
2488
+ "claude-code": ["--workdir", "--bridge-state-dir"],
2489
+ codex: ["--workdir", "--bridge-state-dir", "--cli-shim-dir"]
2490
+ };
2491
+ const flags = expected[ctx.harnessId];
2492
+ if (tokens.length !== 1 + flags.length * 2) {
2493
+ throw new CommandTranslationError(
2494
+ `the ${ctx.harnessId} bridge launch must be the bridge path followed by exactly ${flags.join(", ")}, but ${tokens.length} tokens were given`,
2495
+ command
2496
+ );
2497
+ }
2498
+ const bridgeToken = tokens[0];
2499
+ assertPlainPathOperand(bridgeToken, command);
2500
+ const expectedBridge = `${ctx.adapterBootstrapDir}/bridge.mjs`;
2501
+ if (posix2.normalize(bridgeToken) !== posix2.normalize(expectedBridge)) {
2502
+ throw new CommandTranslationError(
2503
+ `bridge launch names ${JSON.stringify(bridgeToken)}, but the only bridge this session may run is the one in its managed bundle`,
2504
+ command
2505
+ );
2506
+ }
2507
+ const args = [
2508
+ ctx.bridgeLauncherPath ?? remapBootstrapPath(expectedBridge, ctx)
2509
+ ];
2510
+ for (let i = 0; i < flags.length; i += 1) {
2511
+ const flag = tokens[1 + i * 2];
2512
+ const value = tokens[2 + i * 2];
2513
+ if (flag !== flags[i]) {
2514
+ throw new CommandTranslationError(
2515
+ `expected bridge flag ${JSON.stringify(flags[i])} at position ${i + 1}, found ${JSON.stringify(flag)}`,
2516
+ command
2517
+ );
2518
+ }
2519
+ assertPlainPathOperand(value, command);
2520
+ args.push(flag, await ctx.confine(value));
2521
+ }
2522
+ assertArgvAllowed(args);
2523
+ return {
2524
+ kind: "exec",
2525
+ executable: ctx.nodeExecutable,
2526
+ args,
2527
+ workingDirectory: await ctx.confine(ctx.sessionRoot)
2528
+ };
2529
+ }
2530
+ async function translateAdapterCommand(invocation, ctx) {
2531
+ const { command } = invocation;
2532
+ if (typeof command !== "string" || command.length === 0) {
2533
+ throw new CommandTranslationError("empty command", String(command));
2534
+ }
2535
+ if (command.length > 8192) {
2536
+ throw new CommandTranslationError(
2537
+ "command exceeds the length any pinned adapter shape can reach",
2538
+ `${command.slice(0, 120)}\u2026`
2539
+ );
2540
+ }
2541
+ if (command !== command.trim()) {
2542
+ throw new CommandTranslationError(
2543
+ "command has leading or trailing whitespace, which no pinned shape has",
2544
+ command
2545
+ );
2546
+ }
2547
+ if (command === PWD_PROBE) {
2548
+ return { kind: "reply", stdout: ctx.sessionRoot };
2549
+ }
2550
+ if (command === HOME_PROBE) {
2551
+ return { kind: "reply", stdout: ctx.syntheticHome };
2552
+ }
2553
+ const envVar = ENV_MKDIR_SHAPES[command];
2554
+ if (envVar !== void 0) {
2555
+ const target = invocation.env?.[envVar];
2556
+ if (typeof target !== "string" || target.length === 0) {
2557
+ throw new CommandTranslationError(
2558
+ `${command} was issued without a ${envVar} value to create`,
2559
+ command
2560
+ );
2561
+ }
2562
+ assertPlainPathOperand(target, command);
2563
+ if (underBootstrapDir(target, posix2.normalize(ctx.adapterBootstrapDir))) {
2564
+ return {
2565
+ kind: "noop",
2566
+ reason: "mkdir targeted the adapter bootstrap directory, which is served by the read-only managed runtime bundle"
2567
+ };
2568
+ }
2569
+ return { kind: "mkdir", paths: [await ctx.confine(target)] };
2570
+ }
2571
+ const bootstrapNoop = matchBootstrapInstall(command, ctx);
2572
+ if (bootstrapNoop) {
2573
+ const cwd = invocation.workingDirectory;
2574
+ if (cwd !== void 0 && posix2.normalize(cwd) !== posix2.normalize(ctx.adapterBootstrapDir)) {
2575
+ throw new CommandTranslationError(
2576
+ `a bootstrap command was issued from ${JSON.stringify(cwd)} rather than the adapter's bootstrap directory`,
2577
+ command
2578
+ );
2579
+ }
2580
+ return bootstrapNoop;
2581
+ }
2582
+ const bridge = await matchBridgeLaunch(command, ctx);
2583
+ if (bridge) return bridge;
2584
+ if (command.startsWith("mkdir -p ")) {
2585
+ return translateMkdir(command, ctx);
2586
+ }
2587
+ if (command.startsWith("mv -f ")) return translateSkillsMove(command, ctx);
2588
+ if (command.startsWith("test ! -e ")) return translateSkillsProbe(command, ctx);
2589
+ if (command.startsWith("rm -rf -- ")) return translateSkillsRemove(command, ctx);
2590
+ throw new CommandTranslationError(
2591
+ `command is not one of the ${ctx.harnessId} adapter shapes this Inspector is pinned to translate. Local execution never falls back to a shell, so the session fails closed. If this arrived from an adapter upgrade, the compatibility manifest and conformance suite must be re-reviewed before the new shape is accepted.`,
2592
+ command
2593
+ );
2594
+ }
2595
+
2596
+ // server/utils/harness/local/session-env.ts
2597
+ import { readdirSync } from "fs";
2598
+ import { posix as posix3, win32 as win322 } from "path";
2599
+ function pathFor(platform) {
2600
+ return platform === "win32" ? win322 : posix3;
2601
+ }
2602
+ var LOCAL_HARNESS_ENV_ALLOWLIST = [
2603
+ // Locale and terminal shape. Vendor CLIs render differently without these
2604
+ // and some refuse to start.
2605
+ "LANG",
2606
+ "LC_ALL",
2607
+ "LC_CTYPE",
2608
+ "TERM",
2609
+ "TZ",
2610
+ "COLORTERM",
2611
+ // Windows needs these for any process to start at all.
2612
+ "SYSTEMROOT",
2613
+ "SYSTEMDRIVE",
2614
+ "WINDIR",
2615
+ "COMSPEC",
2616
+ "PATHEXT",
2617
+ "NUMBER_OF_PROCESSORS",
2618
+ "PROCESSOR_ARCHITECTURE"
2619
+ ];
2620
+ var SYSTEM_PATH_POSIX = ["/usr/bin", "/bin", "/usr/sbin", "/sbin"];
2621
+ function systemPath(platform, base) {
2622
+ if (platform === "win32") {
2623
+ const root = base.SYSTEMROOT ?? base.WINDIR ?? "C:\\Windows";
2624
+ return [
2625
+ root,
2626
+ win322.join(root, "System32"),
2627
+ win322.join(root, "System32", "Wbem")
2628
+ ].join(win322.delimiter);
2629
+ }
2630
+ return SYSTEM_PATH_POSIX.join(posix3.delimiter);
2631
+ }
2632
+ var SCOPED_NAME_DENYLIST = /* @__PURE__ */ new Set([
2633
+ // Executable, library, and config resolution.
2634
+ "PATH",
2635
+ "HOME",
2636
+ "USERPROFILE",
2637
+ "NODE_OPTIONS",
2638
+ "NODE_PATH",
2639
+ "LD_PRELOAD",
2640
+ "LD_LIBRARY_PATH",
2641
+ "DYLD_INSERT_LIBRARIES",
2642
+ "DYLD_LIBRARY_PATH",
2643
+ // Every generated path this module sets. Scoped values are applied LAST, so
2644
+ // without these a caller could overwrite the synthetic config, cache, and
2645
+ // temp roots and point the child back at the user's real configuration —
2646
+ // undoing the whole reason a synthetic home exists.
2647
+ "TMPDIR",
2648
+ "TMP",
2649
+ "TEMP",
2650
+ // Same reason as TMPDIR, and the reason it is set at all: the vendor CLI
2651
+ // extracts its native binary here, so a scoped override would put that
2652
+ // extraction back outside the session's disposable state.
2653
+ "CLAUDE_CODE_TMPDIR",
2654
+ "XDG_CONFIG_HOME",
2655
+ "XDG_CACHE_HOME",
2656
+ "XDG_DATA_HOME",
2657
+ "XDG_STATE_HOME",
2658
+ "APPDATA",
2659
+ "LOCALAPPDATA",
2660
+ "PWD",
2661
+ "MCPJAM_LOCAL_CONTROL_ROOT",
2662
+ "MCPJAM_LOCAL_DENIED_ROOTS",
2663
+ // Names the shell the vendor CLI runs commands through. A scoped override
2664
+ // would point it at any executable; the provider sets it from a path it
2665
+ // has verified exists.
2666
+ "CLAUDE_CODE_GIT_BASH_PATH"
2667
+ ]);
2668
+ var LocalHarnessEnvError = class extends Error {
2669
+ };
2670
+ function buildLocalHarnessEnv(opts) {
2671
+ const platform = opts.platform ?? process.platform;
2672
+ const base = opts.base ?? process.env;
2673
+ const path = pathFor(platform);
2674
+ if (!path.isAbsolute(opts.syntheticHome)) {
2675
+ throw new LocalHarnessEnvError("syntheticHome must be an absolute path");
2676
+ }
2677
+ if (!path.isAbsolute(opts.sessionRoot)) {
2678
+ throw new LocalHarnessEnvError("sessionRoot must be an absolute path");
2679
+ }
2680
+ const env = {};
2681
+ for (const name of LOCAL_HARNESS_ENV_ALLOWLIST) {
2682
+ const value = base[name];
2683
+ if (typeof value === "string" && value.length > 0) env[name] = value;
2684
+ }
2685
+ env.PATH = platform === "win32" && opts.gitBashPath ? `${path.dirname(opts.gitBashPath)}${path.delimiter}${systemPath(platform, base)}` : systemPath(platform, base);
2686
+ env.HOME = opts.syntheticHome;
2687
+ env.PWD = opts.sessionRoot;
2688
+ env.MCPJAM_LOCAL_CONTROL_ROOT = path.dirname(path.dirname(opts.syntheticHome)).replace(/-sessions$/, "");
2689
+ const deniedRoots = [];
2690
+ const controlRoot = env.MCPJAM_LOCAL_CONTROL_ROOT;
2691
+ for (const parent of [`${controlRoot}-sessions`, path.join(path.dirname(controlRoot), "harness-workspaces"), path.join(path.dirname(controlRoot), "harness-workspaces", "scratch")]) {
2692
+ try {
2693
+ for (const entry of readdirSync(parent, { withFileTypes: true })) {
2694
+ const root = path.join(parent, entry.name);
2695
+ if (entry.isDirectory() && ![opts.syntheticHome, opts.sessionRoot].some((active) => active === root || active.startsWith(root + path.sep))) deniedRoots.push(root);
2696
+ }
2697
+ } catch {
2698
+ }
2699
+ }
2700
+ env.MCPJAM_LOCAL_DENIED_ROOTS = JSON.stringify(deniedRoots);
2701
+ env.TMPDIR = path.join(opts.syntheticHome, "tmp");
2702
+ env.CLAUDE_CODE_TMPDIR = env.TMPDIR;
2703
+ env.XDG_CONFIG_HOME = path.join(opts.syntheticHome, ".config");
2704
+ env.XDG_CACHE_HOME = path.join(opts.syntheticHome, ".cache");
2705
+ env.XDG_DATA_HOME = path.join(opts.syntheticHome, ".local", "share");
2706
+ env.XDG_STATE_HOME = path.join(opts.syntheticHome, ".local", "state");
2707
+ if (platform === "win32") {
2708
+ env.USERPROFILE = opts.syntheticHome;
2709
+ env.APPDATA = path.join(opts.syntheticHome, "AppData", "Roaming");
2710
+ env.LOCALAPPDATA = path.join(opts.syntheticHome, "AppData", "Local");
2711
+ env.TEMP = env.TMPDIR;
2712
+ env.TMP = env.TMPDIR;
2713
+ if (opts.gitBashPath !== void 0) {
2714
+ if (!path.isAbsolute(opts.gitBashPath)) {
2715
+ throw new LocalHarnessEnvError("gitBashPath must be an absolute path");
2716
+ }
2717
+ env.CLAUDE_CODE_GIT_BASH_PATH = opts.gitBashPath;
2718
+ }
2719
+ }
2720
+ env.CI = "1";
2721
+ env.NO_COLOR = "1";
2722
+ validateLocalHarnessScopedEnv(opts.scoped ?? {});
2723
+ Object.assign(env, opts.scoped);
2724
+ return env;
2725
+ }
2726
+ function validateLocalHarnessScopedEnv(scoped) {
2727
+ for (const [name, value] of Object.entries(scoped)) {
2728
+ if (SCOPED_NAME_DENYLIST.has(name.toUpperCase())) {
2729
+ throw new LocalHarnessEnvError(
2730
+ `scoped environment entry ${name} is not allowed: it would redirect executable, library, or config resolution for the child`
2731
+ );
2732
+ }
2733
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
2734
+ throw new LocalHarnessEnvError(
2735
+ `scoped environment name ${JSON.stringify(name)} is not a valid environment variable name`
2736
+ );
2737
+ }
2738
+ if (/[\0\n\r]/.test(value)) {
2739
+ throw new LocalHarnessEnvError(
2740
+ `scoped environment value for ${name} contains a control character`
2741
+ );
2742
+ }
2743
+ }
2744
+ }
2745
+ function validateLocalHarnessSecretEnv(scoped) {
2746
+ validateLocalHarnessScopedEnv(scoped);
2747
+ for (const name of Object.keys(scoped)) {
2748
+ if (/^(ANTHROPIC_|AI_GATEWAY_|BRIDGE_|CLAUDE_CODE_)/i.test(name)) {
2749
+ throw new LocalHarnessEnvError(
2750
+ `Project secret ${name} conflicts with the local harness runtime.`
2751
+ );
2752
+ }
2753
+ }
2754
+ }
2755
+ var BRIDGE_SUPPLIED_ENV_ALLOWLIST = [
2756
+ // Bridge control channel.
2757
+ "BRIDGE_CHANNEL_TOKEN",
2758
+ "BRIDGE_WS_PORT",
2759
+ "BRIDGE_REPLAY_FROM_DISK",
2760
+ // Vendor model endpoints and credentials, as computed by the adapter from
2761
+ // the explicit auth Inspector hands it.
2762
+ "ANTHROPIC_API_KEY",
2763
+ "ANTHROPIC_AUTH_TOKEN",
2764
+ "ANTHROPIC_BASE_URL",
2765
+ "CODEX_API_KEY",
2766
+ "OPENAI_API_KEY",
2767
+ "OPENAI_BASE_URL",
2768
+ "AI_GATEWAY_API_KEY",
2769
+ "AI_GATEWAY_BASE_URL"
2770
+ ];
2771
+ function filterBridgeSuppliedEnv(env) {
2772
+ const filtered = {};
2773
+ if (!env) return filtered;
2774
+ for (const name of BRIDGE_SUPPLIED_ENV_ALLOWLIST) {
2775
+ const value = env[name];
2776
+ if (typeof value === "string" && !/[\0\n\r]/.test(value)) {
2777
+ filtered[name] = value;
2778
+ }
2779
+ }
2780
+ return filtered;
2781
+ }
2782
+ function syntheticHomeDirectories(syntheticHome, platform = process.platform) {
2783
+ const path = pathFor(platform);
2784
+ const dirs = [
2785
+ syntheticHome,
2786
+ path.join(syntheticHome, "tmp"),
2787
+ path.join(syntheticHome, ".config"),
2788
+ path.join(syntheticHome, ".cache"),
2789
+ path.join(syntheticHome, ".local", "share"),
2790
+ path.join(syntheticHome, ".local", "state")
2791
+ ];
2792
+ if (platform === "win32") {
2793
+ dirs.push(
2794
+ path.join(syntheticHome, "AppData", "Roaming"),
2795
+ path.join(syntheticHome, "AppData", "Local")
2796
+ );
2797
+ }
2798
+ return dirs;
2799
+ }
2800
+
2801
+ // server/utils/harness/local/supervised-provider.ts
2802
+ var encoder = new TextEncoder();
2803
+ var DEFAULT_LOCAL_HARNESS_FILE_LIMIT_BYTES = 16 * 1024 * 1024;
2804
+ var LocalHarnessFileLimitError = class extends Error {
2805
+ };
2806
+ function completedProcess(stdout, exitCode = 0) {
2807
+ const make = (text) => new ReadableStream({
2808
+ start(controller) {
2809
+ if (text.length > 0) controller.enqueue(encoder.encode(text));
2810
+ controller.close();
2811
+ }
2812
+ });
2813
+ return {
2814
+ pid: void 0,
2815
+ stdout: make(stdout),
2816
+ stderr: make(""),
2817
+ wait: async () => ({ exitCode }),
2818
+ kill: async () => {
2819
+ }
2820
+ };
2821
+ }
2822
+ async function collectStreamBytes(stream, abortSignal, maxBytes = Number.MAX_SAFE_INTEGER) {
2823
+ const reader = stream.getReader();
2824
+ const chunks = [];
2825
+ const abortError = () => abortSignal?.reason instanceof Error ? abortSignal.reason : new Error("aborted");
2826
+ let removeAbortListener = () => {
2827
+ };
2828
+ let abortRace = null;
2829
+ if (abortSignal !== void 0) {
2830
+ abortRace = new Promise((_resolve, reject) => {
2831
+ const onAbort = () => reject(abortError());
2832
+ abortSignal.addEventListener("abort", onAbort, { once: true });
2833
+ removeAbortListener = () => abortSignal.removeEventListener("abort", onAbort);
2834
+ });
2835
+ abortRace.catch(() => {
2836
+ });
2837
+ }
2838
+ try {
2839
+ let total2 = 0;
2840
+ for (; ; ) {
2841
+ if (abortSignal?.aborted) throw abortError();
2842
+ const pending = reader.read();
2843
+ pending.catch(() => {
2844
+ });
2845
+ const { done, value } = abortRace === null ? await pending : await Promise.race([pending, abortRace]);
2846
+ if (done) break;
2847
+ if (value) {
2848
+ if (value.byteLength > maxBytes - total2) {
2849
+ throw new LocalHarnessFileLimitError(
2850
+ `file operation exceeds the ${maxBytes}-byte local harness limit`
2851
+ );
2852
+ }
2853
+ total2 += value.byteLength;
2854
+ chunks.push(value);
2855
+ }
2856
+ }
2857
+ } catch (error) {
2858
+ void reader.cancel(error).catch(() => {
2859
+ });
2860
+ throw error;
2861
+ } finally {
2862
+ removeAbortListener();
2863
+ reader.releaseLock();
2864
+ }
2865
+ const total = chunks.reduce((n, c) => n + c.byteLength, 0);
2866
+ const merged = new Uint8Array(total);
2867
+ let offset = 0;
2868
+ for (const chunk of chunks) {
2869
+ merged.set(chunk, offset);
2870
+ offset += chunk.byteLength;
2871
+ }
2872
+ return merged;
2873
+ }
2874
+ async function openBoundedFileStream(path, maxBytes, abortSignal) {
2875
+ let handle;
2876
+ try {
2877
+ handle = await open(path, "r");
2878
+ } catch (error) {
2879
+ if (error.code === "ENOENT") return null;
2880
+ throw error;
2881
+ }
2882
+ try {
2883
+ const info = await handle.stat();
2884
+ if (!info.isFile()) {
2885
+ throw new Error(`refusing to read non-regular file ${path}`);
2886
+ }
2887
+ if (info.size > maxBytes) {
2888
+ throw new LocalHarnessFileLimitError(
2889
+ `file ${path} is ${info.size} bytes; the local harness limit is ${maxBytes} bytes`
2890
+ );
2891
+ }
2892
+ } catch (error) {
2893
+ await handle.close().catch(() => {
2894
+ });
2895
+ throw error;
2896
+ }
2897
+ let position = 0;
2898
+ let closed = false;
2899
+ const close = async () => {
2900
+ if (closed) return;
2901
+ closed = true;
2902
+ await handle.close().catch(() => {
2903
+ });
2904
+ };
2905
+ const abortError = () => abortSignal?.reason instanceof Error ? abortSignal.reason : new Error("aborted");
2906
+ return new ReadableStream({
2907
+ async pull(controller) {
2908
+ try {
2909
+ if (abortSignal?.aborted) throw abortError();
2910
+ const room = Math.max(maxBytes - position, 0);
2911
+ const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, room + 1));
2912
+ const { bytesRead } = await handle.read(
2913
+ buffer,
2914
+ 0,
2915
+ buffer.byteLength,
2916
+ position
2917
+ );
2918
+ if (abortSignal?.aborted) throw abortError();
2919
+ if (bytesRead === 0) {
2920
+ await close();
2921
+ controller.close();
2922
+ return;
2923
+ }
2924
+ if (bytesRead > room) {
2925
+ throw new LocalHarnessFileLimitError(
2926
+ `file ${path} grew beyond the ${maxBytes}-byte local harness limit`
2927
+ );
2928
+ }
2929
+ position += bytesRead;
2930
+ controller.enqueue(
2931
+ new Uint8Array(buffer.buffer, buffer.byteOffset, bytesRead)
2932
+ );
2933
+ } catch (error) {
2934
+ await close();
2935
+ controller.error(error);
2936
+ }
2937
+ },
2938
+ async cancel() {
2939
+ await close();
2940
+ }
2941
+ });
2942
+ }
2943
+ async function readBoundedFileBytes(path, maxBytes, abortSignal) {
2944
+ const stream = await openBoundedFileStream(path, maxBytes, abortSignal);
2945
+ if (stream === null) return null;
2946
+ return collectStreamBytes(stream, abortSignal, maxBytes);
2947
+ }
2948
+ async function writeBytesAtomically(path, bytes, abortSignal) {
2949
+ const abortError = () => abortSignal?.reason instanceof Error ? abortSignal.reason : new Error("aborted");
2950
+ if (abortSignal?.aborted) throw abortError();
2951
+ const parent = dirname2(path);
2952
+ await mkdir3(parent, { recursive: true, mode: 448 });
2953
+ let mode = 384;
2954
+ try {
2955
+ mode = (await stat3(path)).mode & 511;
2956
+ } catch (error) {
2957
+ if (error.code !== "ENOENT") throw error;
2958
+ }
2959
+ const temp = join3(parent, `.${basename(path)}.${randomUUID2()}.tmp`);
2960
+ const handle = await open(temp, "wx", mode);
2961
+ let closed = false;
2962
+ try {
2963
+ await handle.writeFile(
2964
+ bytes,
2965
+ abortSignal === void 0 ? void 0 : { signal: abortSignal }
2966
+ );
2967
+ await handle.sync();
2968
+ if (abortSignal?.aborted) throw abortError();
2969
+ await handle.close();
2970
+ closed = true;
2971
+ await rename2(temp, path);
2972
+ } catch (error) {
2973
+ if (!closed) await handle.close().catch(() => {
2974
+ });
2975
+ await rm(temp, { force: true }).catch(() => {
2976
+ });
2977
+ throw error;
2978
+ }
2979
+ }
2980
+ async function resolveGitBashPath(platform) {
2981
+ if (platform !== "win32") return void 0;
2982
+ const configured = process.env.CLAUDE_CODE_GIT_BASH_PATH;
2983
+ const candidates = [
2984
+ ...configured && isAbsolute3(configured) ? [configured] : [],
2985
+ join3(
2986
+ process.env.ProgramFiles ?? "C:\\Program Files",
2987
+ "Git",
2988
+ "bin",
2989
+ "bash.exe"
2990
+ )
2991
+ ];
2992
+ for (const candidate of candidates) {
2993
+ try {
2994
+ if ((await stat3(candidate)).isFile()) return candidate;
2995
+ } catch {
2996
+ }
2997
+ }
2998
+ return void 0;
2999
+ }
3000
+ async function readHead(stream, maxBytes, budgetMs) {
3001
+ const decoder = new TextDecoder();
3002
+ let head = "";
3003
+ const deadline = Date.now() + budgetMs;
3004
+ const reader = stream.getReader();
3005
+ try {
3006
+ while (head.length < maxBytes) {
3007
+ const remaining = deadline - Date.now();
3008
+ if (remaining <= 0) break;
3009
+ const next = await Promise.race([
3010
+ reader.read(),
3011
+ new Promise(
3012
+ (resolveRace) => setTimeout(() => resolveRace({ done: true }), remaining)
3013
+ )
3014
+ ]);
3015
+ if (next.done || next.value === void 0) break;
3016
+ head += decoder.decode(next.value, { stream: true });
3017
+ }
3018
+ } catch {
3019
+ } finally {
3020
+ reader.releaseLock();
3021
+ }
3022
+ return head.slice(0, maxBytes);
3023
+ }
3024
+ async function describeFailedBridge(handle, exitBeforeStop) {
3025
+ const exit = exitBeforeStop !== null ? `exited on its own with code ${exitBeforeStop.exitCode}` : "was still running when the session was stopped";
3026
+ const keptErr = handle.stderrHead?.() ?? "";
3027
+ const keptOut = handle.stdoutHead?.() ?? "";
3028
+ const [out, err] = await Promise.all([
3029
+ keptOut.length > 0 ? Promise.resolve(keptOut) : readHead(handle.stdout, 1024, 500),
3030
+ keptErr.length > 0 ? Promise.resolve(keptErr) : readHead(handle.stderr, 2048, 500)
3031
+ ]);
3032
+ const parts = [exit];
3033
+ if (err.trim().length > 0) parts.push(`stderr: ${JSON.stringify(err.trim())}`);
3034
+ if (out.trim().length > 0) parts.push(`stdout: ${JSON.stringify(out.trim())}`);
3035
+ return parts.join("; ");
3036
+ }
3037
+ async function collectStream(stream) {
3038
+ return new TextDecoder().decode(await collectStreamBytes(stream));
3039
+ }
3040
+ function createSupervisedLocalHarnessProvider(opts) {
3041
+ const maxFileBytes = opts.maxFileBytes ?? DEFAULT_LOCAL_HARNESS_FILE_LIMIT_BYTES;
3042
+ if (!Number.isSafeInteger(maxFileBytes) || maxFileBytes <= 0) {
3043
+ throw new Error("maxFileBytes must be a positive safe integer");
3044
+ }
3045
+ const assertFileSize = (bytes) => {
3046
+ if (bytes > maxFileBytes) {
3047
+ throw new LocalHarnessFileLimitError(
3048
+ `file operation is ${bytes} bytes; the local harness limit is ${maxFileBytes} bytes`
3049
+ );
3050
+ }
3051
+ };
3052
+ const syntheticHome = join3(opts.sessionStateDir, "home");
3053
+ const bootstrapOverlay = join3(opts.sessionStateDir, "bootstrap");
3054
+ const workRoot = join3(opts.sessionStateDir, "work");
3055
+ const WORKSPACE_LINK_NAME = "project";
3056
+ const platform = opts.platform ?? process.platform;
3057
+ const adapterWorkRoot = toAdapterPath(workRoot, platform);
3058
+ const adapterHome = toAdapterPath(syntheticHome, platform);
3059
+ const linkWorkspace = async (link) => {
3060
+ await symlink(
3061
+ opts.workspacePath,
3062
+ link,
3063
+ platform === "win32" ? "junction" : void 0
3064
+ );
3065
+ };
3066
+ const buildSession = async (sessionId, abortSignal) => {
3067
+ for (const dir of syntheticHomeDirectories(syntheticHome, platform)) {
3068
+ await mkdir3(dir, { recursive: true, mode: 448 });
3069
+ }
3070
+ await mkdir3(bootstrapOverlay, { recursive: true, mode: 448 });
3071
+ await mkdir3(workRoot, { recursive: true, mode: 448 });
3072
+ const workspaceLink = join3(workRoot, WORKSPACE_LINK_NAME);
3073
+ try {
3074
+ const existing = await lstat2(workspaceLink);
3075
+ if (!existing.isSymbolicLink()) {
3076
+ throw new Error(`${workspaceLink} exists and is not the workspace link`);
3077
+ }
3078
+ const current = await readlink4(workspaceLink);
3079
+ const resolved = await realpath3(workspaceLink).catch(
3080
+ (error) => {
3081
+ if (error.code === "ENOENT") return null;
3082
+ throw error;
3083
+ }
3084
+ );
3085
+ const textAgrees = platform === "win32" ? true : current === opts.workspacePath;
3086
+ if (!textAgrees || resolved !== opts.workspacePath) {
3087
+ logger.warn("[local-harness] repointing a stale workspace link", {
3088
+ sessionStateDir: opts.sessionStateDir
3089
+ });
3090
+ await rm(workspaceLink, { force: true });
3091
+ await linkWorkspace(workspaceLink);
3092
+ }
3093
+ } catch (error) {
3094
+ if (error.code !== "ENOENT") throw error;
3095
+ await linkWorkspace(workspaceLink);
3096
+ }
3097
+ const roots = [await realpath3(opts.sessionStateDir), opts.workspacePath];
3098
+ const confine = (path) => confinePath(fromAdapterPath(path, platform), { roots });
3099
+ const gitBashPath = await resolveGitBashPath(platform);
3100
+ const env = {
3101
+ ...buildLocalHarnessEnv({
3102
+ syntheticHome,
3103
+ sessionRoot: opts.workspacePath,
3104
+ platform,
3105
+ ...opts.scopedEnv ? { scoped: opts.scopedEnv } : {},
3106
+ ...gitBashPath !== void 0 ? { gitBashPath } : {}
3107
+ }),
3108
+ ...opts.launcher.requiredEnv
3109
+ };
3110
+ const translationContext = {
3111
+ harnessId: opts.harnessId,
3112
+ // Resolved exactly as the framework resolves it — against the session's
3113
+ // default working directory — so the translator matches the string the
3114
+ // adapters will actually emit.
3115
+ adapterBootstrapDir: posix4.resolve(
3116
+ adapterWorkRoot,
3117
+ opts.manifest.adapterBootstrapDir
3118
+ ),
3119
+ managedBundleRoot: opts.runtime.rootPath,
3120
+ adapterBootstrapFiles: opts.manifest.adapterBootstrapFiles,
3121
+ bootstrapOverlayDir: bootstrapOverlay,
3122
+ nodeExecutable: opts.launcher.executable,
3123
+ // Launch the pack's loopback wrapper rather than the remapped
3124
+ // `bridge.mjs`, which stays byte-identical so the recipe compare holds.
3125
+ bridgeLauncherPath: opts.runtime.launcherPath,
3126
+ // The framework's default working directory (and the bridge's cwd), in
3127
+ // the shape the adapter composes with. `confine` turns it native.
3128
+ sessionRoot: adapterWorkRoot,
3129
+ syntheticHome: adapterHome,
3130
+ // The same symlink-aware check the file API uses. The translator awaits
3131
+ // it for every session-scoped operand, including the bridge's own
3132
+ // `--workdir` and `--bridge-state-dir`, which nothing downstream would
3133
+ // otherwise check: they are consumed by the child, not by a filesystem
3134
+ // call we make.
3135
+ confine
3136
+ };
3137
+ const resolveForRead = async (path) => {
3138
+ const target = classifyBootstrapPath(path, translationContext);
3139
+ if (target.kind === "bundle-asset") return target.bundlePath;
3140
+ if (target.kind === "session-overlay") return confine(target.overlayPath);
3141
+ return confine(path);
3142
+ };
3143
+ const resolveForWrite = async (path, bytes, signal) => {
3144
+ const target = classifyBootstrapPath(path, translationContext);
3145
+ if (target.kind === "session-overlay") {
3146
+ return confine(target.overlayPath);
3147
+ }
3148
+ if (target.kind === "workspace") return confine(path);
3149
+ const existing = await readBoundedFileBytes(
3150
+ target.bundlePath,
3151
+ maxFileBytes,
3152
+ signal
3153
+ );
3154
+ if (existing === null) {
3155
+ throw new Error(
3156
+ `the managed ${opts.harnessId} bundle does not contain ${target.relativePath}, which the pinned adapter's bootstrap recipe writes. The bundle and the pinned adapter disagree; refusing rather than installing it into the workspace.`
3157
+ );
3158
+ }
3159
+ if (!Buffer.from(existing).equals(Buffer.from(bytes))) {
3160
+ throw new Error(
3161
+ `the pinned adapter's ${target.relativePath} differs from the copy in the verified managed bundle. Local sessions run the bundle's copy, so this session would not be running what the adapter bootstrapped; refusing until the bundle is rebuilt for this adapter version.`
3162
+ );
3163
+ }
3164
+ logger.debug("[local-harness] bootstrap file satisfied by bundle", {
3165
+ file: target.relativePath
3166
+ });
3167
+ return null;
3168
+ };
3169
+ const ports = [opts.bridgePort];
3170
+ let bridgeClaimed = false;
3171
+ const assertPortLeased = (port) => {
3172
+ if (!ports.includes(port)) {
3173
+ throw new Error(
3174
+ `port ${port} is not leased to this session; the local provider only resolves ports it opened`
3175
+ );
3176
+ }
3177
+ };
3178
+ const assertRuntimeUnchanged = async () => {
3179
+ const result = await revalidateRuntime(opts.runtime);
3180
+ if (!result.ok) {
3181
+ throw new Error(
3182
+ `refusing to launch: ${result.message}. Local execution is bound to a runtime identity, so a changed runtime needs fresh consent.`
3183
+ );
3184
+ }
3185
+ };
3186
+ const envFor = (supplied) => ({
3187
+ ...env,
3188
+ ...filterBridgeSuppliedEnv(supplied)
3189
+ });
3190
+ const runTranslated = async (translated, abort, suppliedEnv) => {
3191
+ switch (translated.kind) {
3192
+ case "reply":
3193
+ return {
3194
+ exitCode: translated.exitCode ?? 0,
3195
+ stdout: translated.stdout,
3196
+ stderr: ""
3197
+ };
3198
+ // The framework's skills writer, performed with `node:fs`. Operands
3199
+ // were confined to the synthetic home by the translator; no shell is
3200
+ // involved at any point.
3201
+ case "rename": {
3202
+ await rename2(translated.from, translated.to);
3203
+ return { exitCode: 0, stdout: "", stderr: "" };
3204
+ }
3205
+ case "remove": {
3206
+ for (const path of translated.paths) {
3207
+ await rm(path, { recursive: true, force: true });
3208
+ }
3209
+ return { exitCode: 0, stdout: "", stderr: "" };
3210
+ }
3211
+ case "probe-absent": {
3212
+ try {
3213
+ await stat3(translated.path);
3214
+ return { exitCode: 1, stdout: "", stderr: "" };
3215
+ } catch (error) {
3216
+ if (error.code === "ENOENT") {
3217
+ return { exitCode: 0, stdout: "", stderr: "" };
3218
+ }
3219
+ throw error;
3220
+ }
3221
+ }
3222
+ case "noop":
3223
+ logger.debug("[local-harness] adapter command satisfied by bundle", {
3224
+ reason: translated.reason
3225
+ });
3226
+ return { exitCode: 0, stdout: "", stderr: "" };
3227
+ case "mkdir": {
3228
+ for (const path of translated.paths) {
3229
+ await mkdir3(path, { recursive: true, mode: 448 });
3230
+ }
3231
+ return { exitCode: 0, stdout: "", stderr: "" };
3232
+ }
3233
+ case "exec": {
3234
+ await assertRuntimeUnchanged();
3235
+ const handle = await opts.supervisor.spawnSupervised({
3236
+ sessionId,
3237
+ executable: translated.executable,
3238
+ args: translated.args,
3239
+ workingDirectory: translated.workingDirectory,
3240
+ env: envFor(suppliedEnv),
3241
+ runtimeId: opts.runtime.runtimeId,
3242
+ workspaceGrantId: opts.workspaceGrantId,
3243
+ targetKind: opts.targetKind,
3244
+ sessionStateDir: opts.sessionStateDir,
3245
+ ...opts.runtime.jobLauncherPath !== void 0 ? { jobLauncherPath: opts.runtime.jobLauncherPath } : {},
3246
+ role: "helper",
3247
+ ...abort ? { abortSignal: abort } : {}
3248
+ });
3249
+ const [stdout, stderr, result] = await Promise.all([
3250
+ collectStream(handle.stdout),
3251
+ collectStream(handle.stderr),
3252
+ handle.wait()
3253
+ ]);
3254
+ return { exitCode: result.exitCode, stdout, stderr };
3255
+ }
3256
+ }
3257
+ };
3258
+ let restrictedSession;
3259
+ const session = {
3260
+ id: sessionId,
3261
+ // The granted workspace is the session's root. The harness framework
3262
+ // nests `<workspace>/<harnessId>-<sessionId>` beneath it, which is where
3263
+ // the agent actually works — the same shape as the hosted path, so a
3264
+ // session's artefacts stay identifiable inside the user's own checkout.
3265
+ // Staged materialization with diff-based apply-back is a separate step
3266
+ // and is not pretended here.
3267
+ defaultWorkingDirectory: adapterWorkRoot,
3268
+ description: `Supervised local ${opts.harnessId} process on this machine. Working directory ${opts.workspacePath}. Bridge on loopback port ${opts.bridgePort}. This is NOT an isolated sandbox: the process runs with this operating-system user's authority, narrowed only by the harness's own permission settings.`,
3269
+ // ── file I/O (confined to the granted roots) ──────────────────────
3270
+ readTextFile: async ({
3271
+ path,
3272
+ abortSignal: signal,
3273
+ encoding,
3274
+ startLine,
3275
+ endLine
3276
+ }) => {
3277
+ const canonical = await resolveForRead(path);
3278
+ const bytes = await readBoundedFileBytes(
3279
+ canonical,
3280
+ maxFileBytes,
3281
+ signal ?? abortSignal
3282
+ );
3283
+ if (bytes === null) return null;
3284
+ const text = Buffer.from(bytes).toString(
3285
+ encoding ?? "utf8"
3286
+ );
3287
+ if (startLine === void 0 && endLine === void 0) return text;
3288
+ const lines = text.split("\n");
3289
+ const from = Math.max((startLine ?? 1) - 1, 0);
3290
+ const to = endLine === void 0 ? lines.length : Math.min(endLine, lines.length);
3291
+ return lines.slice(from, to).join("\n");
3292
+ },
3293
+ readBinaryFile: async ({ path, abortSignal: signal }) => {
3294
+ const canonical = await resolveForRead(path);
3295
+ return readBoundedFileBytes(
3296
+ canonical,
3297
+ maxFileBytes,
3298
+ signal ?? abortSignal
3299
+ );
3300
+ },
3301
+ readFile: async ({ path, abortSignal: signal }) => {
3302
+ const canonical = await resolveForRead(path);
3303
+ return openBoundedFileStream(
3304
+ canonical,
3305
+ maxFileBytes,
3306
+ signal ?? abortSignal
3307
+ );
3308
+ },
3309
+ writeTextFile: async ({
3310
+ path,
3311
+ content,
3312
+ encoding,
3313
+ abortSignal: signal
3314
+ }) => {
3315
+ const charset = encoding ?? "utf8";
3316
+ const bytes = Buffer.from(content, charset);
3317
+ assertFileSize(bytes.byteLength);
3318
+ const writeSignal = signal ?? abortSignal;
3319
+ const canonical = await resolveForWrite(path, bytes, writeSignal);
3320
+ if (canonical === null) return;
3321
+ await writeBytesAtomically(canonical, bytes, writeSignal);
3322
+ },
3323
+ writeBinaryFile: async ({ path, content, abortSignal: signal }) => {
3324
+ assertFileSize(content.byteLength);
3325
+ const writeSignal = signal ?? abortSignal;
3326
+ const canonical = await resolveForWrite(path, content, writeSignal);
3327
+ if (canonical === null) return;
3328
+ await writeBytesAtomically(canonical, content, writeSignal);
3329
+ },
3330
+ writeFile: async ({ path, content, abortSignal: signal }) => {
3331
+ const bytes = await collectStreamBytes(
3332
+ content,
3333
+ signal ?? abortSignal,
3334
+ maxFileBytes
3335
+ );
3336
+ const writeSignal = signal ?? abortSignal;
3337
+ const canonical = await resolveForWrite(path, bytes, writeSignal);
3338
+ if (canonical === null) return;
3339
+ await writeBytesAtomically(canonical, bytes, writeSignal);
3340
+ },
3341
+ // ── exec ──────────────────────────────────────────────────────────
3342
+ run: async ({
3343
+ command,
3344
+ workingDirectory,
3345
+ abortSignal: signal,
3346
+ env: suppliedEnv
3347
+ }) => {
3348
+ const translated = await translateAdapterCommand(
3349
+ { command, workingDirectory, env: suppliedEnv },
3350
+ translationContext
3351
+ );
3352
+ return runTranslated(translated, signal ?? abortSignal, suppliedEnv);
3353
+ },
3354
+ spawn: async ({
3355
+ command,
3356
+ workingDirectory,
3357
+ abortSignal: signal,
3358
+ env: suppliedEnv
3359
+ }) => {
3360
+ const translated = await translateAdapterCommand(
3361
+ { command, workingDirectory, env: suppliedEnv },
3362
+ translationContext
3363
+ );
3364
+ if (translated.kind !== "exec") {
3365
+ const result = await runTranslated(
3366
+ translated,
3367
+ signal ?? abortSignal,
3368
+ suppliedEnv
3369
+ );
3370
+ return completedProcess(result.stdout, result.exitCode);
3371
+ }
3372
+ const isBridge = !bridgeClaimed;
3373
+ if (isBridge) bridgeClaimed = true;
3374
+ const releaseBridgeClaim = () => {
3375
+ if (isBridge) bridgeClaimed = false;
3376
+ };
3377
+ let handle;
3378
+ try {
3379
+ await assertRuntimeUnchanged();
3380
+ if (isBridge) {
3381
+ await assertBridgePortUnclaimed({ port: opts.bridgePort });
3382
+ }
3383
+ handle = await opts.supervisor.spawnSupervised({
3384
+ sessionId,
3385
+ executable: translated.executable,
3386
+ args: translated.args,
3387
+ workingDirectory: translated.workingDirectory,
3388
+ env: envFor(suppliedEnv),
3389
+ runtimeId: opts.runtime.runtimeId,
3390
+ workspaceGrantId: opts.workspaceGrantId,
3391
+ targetKind: opts.targetKind,
3392
+ sessionStateDir: opts.sessionStateDir,
3393
+ ...opts.runtime.jobLauncherPath !== void 0 ? { jobLauncherPath: opts.runtime.jobLauncherPath } : {},
3394
+ // The first spawned process is the session's root: killing it must
3395
+ // take the whole tree, and it is the record the janitor reclaims.
3396
+ role: isBridge ? "root" : "helper",
3397
+ ...signal ?? abortSignal ? { abortSignal: signal ?? abortSignal } : {}
3398
+ });
3399
+ } catch (error) {
3400
+ releaseBridgeClaim();
3401
+ throw error;
3402
+ }
3403
+ if (isBridge) {
3404
+ try {
3405
+ await assertBridgeLoopbackOnly({
3406
+ port: opts.bridgePort,
3407
+ ...opts.bridgeReadinessTimeoutMs !== void 0 ? { readinessTimeoutMs: opts.bridgeReadinessTimeoutMs } : {},
3408
+ // Corroborates the connect probe by reading the binding out of
3409
+ // the kernel, which catches an address the machine has but this
3410
+ // process cannot route to — unreachable from here, still bound.
3411
+ bridgePid: handle.pid,
3412
+ // Ties the listener to the process we started: a port answering
3413
+ // after our bridge died is somebody else's.
3414
+ isBridgeAlive: async () => opts.supervisor.liveProcessCount(sessionId) > 0
3415
+ });
3416
+ if (opts.onBridgeStarted) {
3417
+ await opts.onBridgeStarted({
3418
+ pid: handle.pid,
3419
+ port: opts.bridgePort
3420
+ });
3421
+ }
3422
+ } catch (error) {
3423
+ releaseBridgeClaim();
3424
+ const exitBeforeStop = handle.exited?.() ?? null;
3425
+ await opts.supervisor.stopSession(sessionId).catch(() => {
3426
+ });
3427
+ if (error instanceof Error) {
3428
+ const said = await describeFailedBridge(
3429
+ handle,
3430
+ exitBeforeStop
3431
+ ).catch(() => "no diagnosis could be read");
3432
+ error.message = `${error.message} (bridge: ${said})`;
3433
+ }
3434
+ throw error;
3435
+ }
3436
+ }
3437
+ return handle;
3438
+ },
3439
+ // ── infra surface ─────────────────────────────────────────────────
3440
+ ports,
3441
+ // Both resolvers answer from the same loopback authority. The stable
3442
+ // contract made `getPortEndpoint` the required one and left `getPortUrl`
3443
+ // in place as deprecated; a local provider must never let either return
3444
+ // an address reachable from off-box.
3445
+ getPortEndpoint: async ({ port, protocol }) => {
3446
+ assertPortLeased(port);
3447
+ return {
3448
+ url: localBridgeUrl({ port, ...protocol ? { protocol } : {} })
3449
+ };
3450
+ },
3451
+ getPortUrl: async ({ port, protocol }) => {
3452
+ assertPortLeased(port);
3453
+ return localBridgeUrl({ port, ...protocol ? { protocol } : {} });
3454
+ },
3455
+ setPorts: async (next) => {
3456
+ ports.splice(0, ports.length, ...next);
3457
+ },
3458
+ // setNetworkPolicy is intentionally NOT implemented. A native provider
3459
+ // has no primitive that would enforce one, and the contract treats a
3460
+ // missing optional method as a no-op — so a misleading empty
3461
+ // implementation would be strictly worse than its absence.
3462
+ stop: async () => {
3463
+ const result = await opts.supervisor.stopSession(sessionId);
3464
+ if (!result.stopped) {
3465
+ throw new Error(
3466
+ `${result.escaped} supervised process tree(s) survived termination for session ${sessionId}; the session is NOT reported stopped`
3467
+ );
3468
+ }
3469
+ },
3470
+ destroy: async () => {
3471
+ const result = await opts.supervisor.stopSession(sessionId);
3472
+ if (!result.stopped) {
3473
+ throw new Error(
3474
+ `${result.escaped} supervised process tree(s) survived termination for session ${sessionId}`
3475
+ );
3476
+ }
3477
+ await removeSessionStateDir(opts.sessionStateDir);
3478
+ },
3479
+ // A TypeScript annotation does not remove properties at runtime. Return
3480
+ // a separately constructed capability object so user-tool code cannot
3481
+ // cast its way to stop/destroy, port mutation, or endpoint resolution.
3482
+ restricted: () => restrictedSession
3483
+ };
3484
+ restrictedSession = Object.freeze({
3485
+ description: session.description,
3486
+ readFile: session.readFile,
3487
+ readBinaryFile: session.readBinaryFile,
3488
+ readTextFile: session.readTextFile,
3489
+ writeFile: session.writeFile,
3490
+ writeBinaryFile: session.writeBinaryFile,
3491
+ writeTextFile: session.writeTextFile,
3492
+ spawn: session.spawn,
3493
+ run: session.run
3494
+ });
3495
+ return session;
3496
+ };
3497
+ return {
3498
+ specificationVersion: "harness-sandbox-v1",
3499
+ providerId: "mcpjam-local-supervised",
3500
+ // `bridgePorts` is gone from the stable contract: port discovery is the
3501
+ // adapter's job now, reading `session.ports` and resolving through
3502
+ // `getPortEndpoint`.
3503
+ createSession: async (options) => {
3504
+ const sessionId = options?.sessionId ?? `local-${Date.now()}`;
3505
+ return buildSession(sessionId, options?.abortSignal);
3506
+ },
3507
+ // Resume reattaches to the SAME session state directory. Whether the
3508
+ // bridge process is still alive is the supervisor's registry to answer;
3509
+ // the adapter's own resume path reconnects to it when it is, and respawns
3510
+ // when it is not.
3511
+ resumeSession: async (options) => buildSession(options.sessionId, options.abortSignal)
3512
+ };
3513
+ }
3514
+ async function removeSessionStateDir(dir) {
3515
+ const root = resolve2(`${localHarnessStateRoot()}-sessions`);
3516
+ const target = resolve2(dir);
3517
+ if (target === root || !target.startsWith(root + sep4)) {
3518
+ logger.warn("[local-harness] refusing to remove state outside the root", {
3519
+ dir
3520
+ });
3521
+ return;
3522
+ }
3523
+ await rm(target, { recursive: true, force: true });
3524
+ }
3525
+ function sessionStateDirFor(stateRoot, sessionId) {
3526
+ if (!/^[A-Za-z0-9_-]{1,128}$/.test(sessionId)) {
3527
+ throw new Error(
3528
+ `session id ${JSON.stringify(
3529
+ sessionId
3530
+ )} is not a single safe path segment`
3531
+ );
3532
+ }
3533
+ return join3(`${stateRoot}-sessions`, sessionId);
3534
+ }
3535
+
3536
+ // server/utils/harness/local/supervisor.ts
3537
+ import { spawn } from "child_process";
3538
+ import { mkdir as mkdir5, chmod as chmod4 } from "fs/promises";
3539
+ import { isAbsolute as isAbsolute4 } from "path";
3540
+
3541
+ // server/utils/harness/local/process-registry.ts
3542
+ import { randomUUID as randomUUID3 } from "crypto";
3543
+ import {
3544
+ mkdir as mkdir4,
3545
+ readFile as readFile5,
3546
+ rename as rename3,
3547
+ writeFile as writeFile3,
3548
+ chmod as chmod3,
3549
+ rm as rm2
3550
+ } from "fs/promises";
3551
+ import { join as join4, resolve as resolve3, sep as sep5 } from "path";
3552
+ function registryPath() {
3553
+ return join4(localHarnessStateRoot(), "processes.json");
3554
+ }
3555
+ var withRegistryLock = createLocalStateMutationLock({
3556
+ rootDir: localHarnessStateRoot,
3557
+ lockFileName: "processes.lock",
3558
+ resourceLabel: "process registry"
3559
+ });
3560
+ var RegistryUnreadableError = class extends Error {
3561
+ };
3562
+ async function readRegistry() {
3563
+ let raw;
3564
+ try {
3565
+ raw = await readFile5(registryPath(), "utf8");
3566
+ } catch (error) {
3567
+ if (error.code === "ENOENT") {
3568
+ return { version: 1, records: [] };
3569
+ }
3570
+ throw new RegistryUnreadableError(
3571
+ `the local harness process registry could not be read: ${error instanceof Error ? error.message : String(error)}`
3572
+ );
3573
+ }
3574
+ let parsed;
3575
+ try {
3576
+ parsed = JSON.parse(raw);
3577
+ } catch {
3578
+ throw new RegistryUnreadableError(
3579
+ "the local harness process registry is corrupt; refusing to overwrite it, because it may name process trees that are still running"
3580
+ );
3581
+ }
3582
+ if (!parsed || typeof parsed !== "object") {
3583
+ throw new RegistryUnreadableError(
3584
+ "the local harness process registry is not an object"
3585
+ );
3586
+ }
3587
+ const record = parsed;
3588
+ if (record.version !== 1 || !Array.isArray(record.records)) {
3589
+ throw new RegistryUnreadableError(
3590
+ "the local harness process registry has an unrecognized shape"
3591
+ );
3592
+ }
3593
+ return { version: 1, records: record.records };
3594
+ }
3595
+ async function writeRegistry(registry) {
3596
+ const dir = localHarnessStateRoot();
3597
+ await mkdir4(dir, { recursive: true, mode: 448 });
3598
+ await chmod3(dir, 448).catch(() => {
3599
+ });
3600
+ const file = registryPath();
3601
+ const tmp = `${file}.tmp`;
3602
+ await writeFile3(tmp, JSON.stringify(registry), { mode: 384 });
3603
+ await rename3(tmp, file);
3604
+ }
3605
+ function mintSupervisorNonce() {
3606
+ return `sup_${randomUUID3()}`;
3607
+ }
3608
+ function recordProcess(record) {
3609
+ return withRegistryLock(async () => {
3610
+ const registry = await readRegistry();
3611
+ registry.records = registry.records.filter(
3612
+ (r) => r.sessionId !== record.sessionId
3613
+ );
3614
+ registry.records.push(record);
3615
+ await writeRegistry(registry);
3616
+ });
3617
+ }
3618
+ function updateLifecycleState(sessionId, lifecycleState) {
3619
+ return withRegistryLock(async () => {
3620
+ const registry = await readRegistry();
3621
+ const record = registry.records.find((r) => r.sessionId === sessionId);
3622
+ if (!record) return;
3623
+ record.lifecycleState = lifecycleState;
3624
+ await writeRegistry(registry);
3625
+ });
3626
+ }
3627
+ function forgetProcess(sessionId) {
3628
+ return withRegistryLock(async () => {
3629
+ const registry = await readRegistry();
3630
+ const before = registry.records.length;
3631
+ registry.records = registry.records.filter(
3632
+ (r) => r.sessionId !== sessionId
3633
+ );
3634
+ if (registry.records.length !== before) await writeRegistry(registry);
3635
+ });
3636
+ }
3637
+ async function listProcessRecords() {
3638
+ return (await readRegistry()).records;
3639
+ }
3640
+ async function owningSupervisorProvablyExited(record, platform) {
3641
+ if (record.supervisorPid === void 0 || record.supervisorBirthIdentity === void 0) {
3642
+ return false;
3643
+ }
3644
+ const probe = await probeProcess(record.supervisorPid, platform);
3645
+ if (probe.state === "gone") return true;
3646
+ if (probe.state === "unknown") return false;
3647
+ return probe.identity !== record.supervisorBirthIdentity;
3648
+ }
3649
+ function janitorOutcomeForUnsettled(outcome) {
3650
+ if (outcome === "escaped") return "escaped";
3651
+ if (outcome === "unknown") return "skipped-unprovable";
3652
+ return "not-owned";
3653
+ }
3654
+ async function removeSessionState(dir) {
3655
+ const root = resolve3(`${localHarnessStateRoot()}-sessions`);
3656
+ const target = resolve3(dir);
3657
+ if (target === root || !target.startsWith(root + sep5)) {
3658
+ logger.warn("[local-harness] refusing to clean state outside the root", {
3659
+ dir
3660
+ });
3661
+ return;
3662
+ }
3663
+ await rm2(target, { recursive: true, force: true }).catch(() => {
3664
+ });
3665
+ }
3666
+ function reclaimAbandonedProcesses(args) {
3667
+ return withRegistryLock(async () => {
3668
+ const platform = args.platform ?? process.platform;
3669
+ const registry = await readRegistry();
3670
+ const results = [];
3671
+ const survivors = [];
3672
+ for (const record of registry.records) {
3673
+ if (record.supervisorNonce === args.liveNonce) {
3674
+ survivors.push(record);
3675
+ results.push({
3676
+ sessionId: record.sessionId,
3677
+ outcome: "skipped-live-supervisor"
3678
+ });
3679
+ continue;
3680
+ }
3681
+ if (!supportsOwnershipProof(platform)) {
3682
+ survivors.push(record);
3683
+ results.push({
3684
+ sessionId: record.sessionId,
3685
+ outcome: "skipped-unprovable"
3686
+ });
3687
+ continue;
3688
+ }
3689
+ if (!await owningSupervisorProvablyExited(record, platform)) {
3690
+ survivors.push(record);
3691
+ results.push({
3692
+ sessionId: record.sessionId,
3693
+ outcome: "skipped-live-supervisor"
3694
+ });
3695
+ continue;
3696
+ }
3697
+ const rootProbe = await probeProcess(record.rootPid, platform);
3698
+ if (rootProbe.state === "unknown") {
3699
+ survivors.push(record);
3700
+ results.push({
3701
+ sessionId: record.sessionId,
3702
+ outcome: "skipped-unprovable"
3703
+ });
3704
+ continue;
3705
+ }
3706
+ if (rootProbe.state === "gone") {
3707
+ const group = await probeProcessGroup(record.rootPid, platform);
3708
+ if (group === "live") {
3709
+ survivors.push(record);
3710
+ results.push({ sessionId: record.sessionId, outcome: "escaped" });
3711
+ logger.warn(
3712
+ "[local-harness] descendants outlived their root; record kept",
3713
+ { sessionId: record.sessionId }
3714
+ );
3715
+ continue;
3716
+ }
3717
+ if (group === "unknown") {
3718
+ survivors.push(record);
3719
+ results.push({
3720
+ sessionId: record.sessionId,
3721
+ outcome: "skipped-unprovable"
3722
+ });
3723
+ continue;
3724
+ }
3725
+ await removeSessionState(record.sessionStateDir);
3726
+ results.push({ sessionId: record.sessionId, outcome: "already-gone" });
3727
+ continue;
3728
+ }
3729
+ if (rootProbe.identity !== record.processBirthIdentity) {
3730
+ survivors.push(record);
3731
+ results.push({ sessionId: record.sessionId, outcome: "not-owned" });
3732
+ logger.warn("[local-harness] refusing to reclaim a reused pid", {
3733
+ sessionId: record.sessionId
3734
+ });
3735
+ continue;
3736
+ }
3737
+ const outcome = await terminateOwnedProcessGroup({
3738
+ pid: record.rootPid,
3739
+ birthIdentity: record.processBirthIdentity,
3740
+ graceMs: args.graceMs ?? 5e3,
3741
+ platform
3742
+ });
3743
+ if (outcome.outcome === "graceful" || outcome.outcome === "forced") {
3744
+ await removeSessionState(record.sessionStateDir);
3745
+ results.push({ sessionId: record.sessionId, outcome: "terminated" });
3746
+ continue;
3747
+ }
3748
+ if (outcome.outcome === "already-gone") {
3749
+ await removeSessionState(record.sessionStateDir);
3750
+ results.push({ sessionId: record.sessionId, outcome: "already-gone" });
3751
+ continue;
3752
+ }
3753
+ survivors.push(record);
3754
+ results.push({
3755
+ sessionId: record.sessionId,
3756
+ outcome: janitorOutcomeForUnsettled(outcome.outcome)
3757
+ });
3758
+ }
3759
+ if (survivors.length !== registry.records.length) {
3760
+ await writeRegistry({ version: 1, records: survivors });
3761
+ }
3762
+ return results;
3763
+ });
3764
+ }
3765
+
3766
+ // server/utils/harness/local/supervisor.ts
3767
+ var DEFAULT_SUPERVISOR_LIMITS = {
3768
+ maxWallClockMs: 30 * 6e4,
3769
+ maxOutputBytesPerStream: 8 * 1024 * 1024,
3770
+ maxConcurrentProcesses: 4,
3771
+ terminationGraceMs: 5e3
3772
+ };
3773
+ var SupervisorError = class extends Error {
3774
+ };
3775
+ var EXIT_SNAPSHOT_GRACE_MS = 250;
3776
+ function bufferedStream(maxBytes) {
3777
+ let controller;
3778
+ let written = 0;
3779
+ let closed = false;
3780
+ let truncated = false;
3781
+ const stream = new ReadableStream({
3782
+ start: (c) => {
3783
+ controller = c;
3784
+ },
3785
+ // The consumer may stop reading (an aborted turn). Swallow the cancel so
3786
+ // the push path below does not throw into the child's data handler.
3787
+ cancel: () => {
3788
+ closed = true;
3789
+ }
3790
+ });
3791
+ return {
3792
+ stream,
3793
+ push: (chunk) => {
3794
+ if (closed) return;
3795
+ if (written >= maxBytes) {
3796
+ truncated = true;
3797
+ return;
3798
+ }
3799
+ const room = maxBytes - written;
3800
+ const slice = chunk.byteLength <= room ? chunk : chunk.subarray(0, room);
3801
+ if (slice.byteLength < chunk.byteLength) truncated = true;
3802
+ written += slice.byteLength;
3803
+ try {
3804
+ controller.enqueue(slice);
3805
+ } catch {
3806
+ closed = true;
3807
+ }
3808
+ },
3809
+ close: () => {
3810
+ if (closed) return;
3811
+ closed = true;
3812
+ try {
3813
+ controller.close();
3814
+ } catch {
3815
+ }
3816
+ },
3817
+ truncated: () => truncated
3818
+ };
3819
+ }
3820
+ var LocalHarnessSupervisor = class {
3821
+ constructor(opts) {
3822
+ this.live = /* @__PURE__ */ new Map();
3823
+ /**
3824
+ * How many times each session has been stopped.
3825
+ *
3826
+ * Bumped SYNCHRONOUSLY when a stop begins. A launch reads it at entry and
3827
+ * again before it spawns: a different value means a stop landed while this
3828
+ * launch was in flight, so it refuses rather than starting a process the
3829
+ * stop could not see and did not report.
3830
+ *
3831
+ * A COUNTER, not a tombstone. An earlier draft marked the id stopped
3832
+ * forever, which also refused every legitimate later launch — including the
3833
+ * provider's own bridge retry, whose whole purpose is that a failed first
3834
+ * spawn (which stops the session on its way out) can be tried again and be
3835
+ * re-checked as a bridge. Only launches that straddle a stop are refused.
3836
+ */
3837
+ this.stopGenerations = /* @__PURE__ */ new Map();
3838
+ /**
3839
+ * Launches that have reserved a slot but not yet registered a process.
3840
+ *
3841
+ * The reserved bucket is empty until the child exists, so without this the
3842
+ * ceiling is computed from a count that has not caught up yet.
3843
+ */
3844
+ this.pendingLaunches = /* @__PURE__ */ new Map();
3845
+ /**
3846
+ * This Inspector process's own birth identity, recorded on every process it
3847
+ * owns.
3848
+ *
3849
+ * A nonce says which supervisor created a record; it does NOT say whether
3850
+ * that supervisor is still running. Without this, a second Inspector opened
3851
+ * alongside the first would see the first's records under a foreign nonce
3852
+ * and reclaim them — killing live sessions belonging to a healthy process.
3853
+ * The janitor uses pid + birth identity to require that the owner is
3854
+ * genuinely gone before it touches anything.
3855
+ */
3856
+ this.supervisorBirthIdentity = null;
3857
+ /**
3858
+ * Process-group cache for `ownsPid`.
3859
+ *
3860
+ * A group id does not change for the life of a process, and the gateway asks
3861
+ * this on the model hot path — so the probe runs once per pid rather than
3862
+ * once per request. Bounded by pruning entries for pids the session no longer
3863
+ * has, on stop.
3864
+ */
3865
+ this.pidGroups = /* @__PURE__ */ new Map();
3866
+ this.nonce = mintSupervisorNonce();
3867
+ this.limits = { ...DEFAULT_SUPERVISOR_LIMITS, ...opts?.limits ?? {} };
3868
+ this.platform = opts?.platform ?? process.platform;
3869
+ this.supervisorIdentityReady = readProcessBirthIdentity(
3870
+ process.pid,
3871
+ this.platform
3872
+ ).then((identity) => {
3873
+ this.supervisorBirthIdentity = identity;
3874
+ });
3875
+ }
3876
+ /**
3877
+ * Reclaim trees abandoned by a previous Inspector process. Safe to call at
3878
+ * startup and idempotent; records belonging to THIS supervisor are skipped.
3879
+ */
3880
+ async reclaimOrphans() {
3881
+ return reclaimAbandonedProcesses({
3882
+ liveNonce: this.nonce,
3883
+ graceMs: this.limits.terminationGraceMs,
3884
+ platform: this.platform
3885
+ });
3886
+ }
3887
+ /** Create a directory inside the session's own state, owner-only. */
3888
+ async ensureDirectory(path) {
3889
+ await mkdir5(path, { recursive: true, mode: 448 });
3890
+ await chmod4(path, 448).catch(() => {
3891
+ });
3892
+ }
3893
+ /**
3894
+ * Start a supervised process.
3895
+ *
3896
+ * Every rejection here is a refusal to start, never a downgrade: a launch
3897
+ * that cannot satisfy the invariants does not happen.
3898
+ */
3899
+ async spawnSupervised(request) {
3900
+ if (!isAbsolute4(request.executable)) {
3901
+ throw new SupervisorError(
3902
+ `refusing to spawn ${JSON.stringify(request.executable)}: the supervisor launches absolute, pre-verified paths only. A bare name would be resolved through a mutable PATH at spawn time, which is not the runtime consent was granted for.`
3903
+ );
3904
+ }
3905
+ if (!isAbsolute4(request.workingDirectory)) {
3906
+ throw new SupervisorError(
3907
+ "refusing to spawn with a relative working directory"
3908
+ );
3909
+ }
3910
+ if (request.role === "root" && !supportsOwnershipProof(this.platform)) {
3911
+ throw new SupervisorError(
3912
+ `local harness execution is not available on ${this.platform}: this Inspector cannot prove ownership of a process tree here, so it could not guarantee that stopping a session stops everything it started.`
3913
+ );
3914
+ }
3915
+ assertArgvAllowed(request.args);
3916
+ const jobLauncher = this.platform === "win32" ? request.jobLauncherPath : void 0;
3917
+ if (this.platform === "win32" && jobLauncher === void 0) {
3918
+ if (request.role === "root") {
3919
+ throw new SupervisorError(
3920
+ "refusing to start a root process on win32 without the verified Job Object launcher: without it, stopping the session could not be guaranteed to stop everything it started"
3921
+ );
3922
+ }
3923
+ }
3924
+ if (jobLauncher !== void 0 && !isAbsolute4(jobLauncher)) {
3925
+ throw new SupervisorError(
3926
+ "the Job Object launcher must be an absolute path inside the verified runtime pack"
3927
+ );
3928
+ }
3929
+ const stopGenerationAtEntry = this.stopGenerations.get(request.sessionId) ?? 0;
3930
+ const bucket = this.live.get(request.sessionId) ?? /* @__PURE__ */ new Set();
3931
+ const pending = this.pendingLaunches.get(request.sessionId) ?? 0;
3932
+ const active = [...bucket].filter((entry2) => !entry2.exited).length;
3933
+ const admitted = active + pending;
3934
+ if (admitted >= this.limits.maxConcurrentProcesses) {
3935
+ throw new SupervisorError(
3936
+ `session ${request.sessionId} already has ${admitted} supervised processes (ceiling ${this.limits.maxConcurrentProcesses})`
3937
+ );
3938
+ }
3939
+ this.live.set(request.sessionId, bucket);
3940
+ this.pendingLaunches.set(request.sessionId, pending + 1);
3941
+ let pendingReleased = false;
3942
+ const releasePending = () => {
3943
+ if (pendingReleased) return;
3944
+ pendingReleased = true;
3945
+ const now = (this.pendingLaunches.get(request.sessionId) ?? 1) - 1;
3946
+ if (now <= 0) this.pendingLaunches.delete(request.sessionId);
3947
+ else this.pendingLaunches.set(request.sessionId, now);
3948
+ };
3949
+ const releaseReservation = () => {
3950
+ releasePending();
3951
+ if (bucket.size === 0 && this.live.get(request.sessionId) === bucket) {
3952
+ this.live.delete(request.sessionId);
3953
+ }
3954
+ };
3955
+ let ownerBirthIdentity;
3956
+ if (request.role === "root") {
3957
+ await this.supervisorIdentityReady;
3958
+ if (this.supervisorBirthIdentity === null) {
3959
+ releaseReservation();
3960
+ throw new SupervisorError(
3961
+ "this Inspector could not read its own process identity, so a supervised root would be recorded without a reclaimable owner; refusing to start it"
3962
+ );
3963
+ }
3964
+ ownerBirthIdentity = this.supervisorBirthIdentity;
3965
+ }
3966
+ if ((this.stopGenerations.get(request.sessionId) ?? 0) !== stopGenerationAtEntry) {
3967
+ releaseReservation();
3968
+ throw new SupervisorError(
3969
+ `session ${request.sessionId} was stopped while this launch was starting; refusing to spawn a process the stop cannot account for`
3970
+ );
3971
+ }
3972
+ const [spawnExecutable, spawnArgs] = jobLauncher !== void 0 ? [jobLauncher, [request.executable, ...request.args]] : [request.executable, [...request.args]];
3973
+ const child = spawn(spawnExecutable, spawnArgs, {
3974
+ cwd: request.workingDirectory,
3975
+ env: request.env,
3976
+ stdio: [jobLauncher !== void 0 ? "pipe" : "ignore", "pipe", "pipe"],
3977
+ // POSIX: become a process-group leader so the whole tree can be signalled.
3978
+ detached: this.platform !== "win32",
3979
+ // Belt and braces — the default is already false, but this is the single
3980
+ // most important property in the file and it should be visible here.
3981
+ shell: false,
3982
+ windowsHide: true
3983
+ });
3984
+ const out = bufferedStream(this.limits.maxOutputBytesPerStream);
3985
+ const err = bufferedStream(this.limits.maxOutputBytesPerStream);
3986
+ let stdoutHead = "";
3987
+ child.stdout?.on("data", (chunk) => {
3988
+ if (stdoutHead.length < 1024) {
3989
+ stdoutHead += chunk.toString(
3990
+ "utf8",
3991
+ 0,
3992
+ Math.min(chunk.length, 1024 - stdoutHead.length)
3993
+ );
3994
+ }
3995
+ out.push(new Uint8Array(chunk));
3996
+ });
3997
+ let stderrHead = "";
3998
+ child.stderr?.on("data", (chunk) => {
3999
+ if (stderrHead.length < 2048) {
4000
+ stderrHead += chunk.toString(
4001
+ "utf8",
4002
+ 0,
4003
+ Math.min(chunk.length, 2048 - stderrHead.length)
4004
+ );
4005
+ }
4006
+ err.push(new Uint8Array(chunk));
4007
+ });
4008
+ let exitResult = null;
4009
+ let notifyExit = null;
4010
+ let entry;
4011
+ const recordExit = (exitCode) => {
4012
+ if (exitResult !== null) return;
4013
+ exitResult = { exitCode };
4014
+ if (entry !== void 0) entry.exited = true;
4015
+ notifyExit?.(exitResult);
4016
+ };
4017
+ child.on("error", (error) => {
4018
+ logger.warn("[local-harness] supervised process error", {
4019
+ sessionId: request.sessionId,
4020
+ error: error.message
4021
+ });
4022
+ recordExit(-1);
4023
+ });
4024
+ child.on("exit", () => {
4025
+ if (request.role === "root" && entry !== void 0 && entry.orphanSnapshot === null) {
4026
+ entry.orphanSnapshot = listGroupMembers(entry.pid, this.platform).catch(
4027
+ () => null
4028
+ );
4029
+ }
4030
+ });
4031
+ child.on("close", (code, signal) => {
4032
+ recordExit(code ?? (signal ? 124 : 1));
4033
+ });
4034
+ const pid = child.pid;
4035
+ if (pid === void 0) {
4036
+ releaseReservation();
4037
+ throw new SupervisorError(
4038
+ `the ${request.role} process failed to start (no pid was assigned)`
4039
+ );
4040
+ }
4041
+ entry = {
4042
+ child,
4043
+ pid,
4044
+ birthIdentity: null,
4045
+ role: request.role,
4046
+ killed: false,
4047
+ exited: exitResult !== null,
4048
+ orphanSnapshot: null
4049
+ };
4050
+ bucket.add(entry);
4051
+ releasePending();
4052
+ const abandon = async () => {
4053
+ if (entry.birthIdentity !== null) {
4054
+ await terminateOwnedProcessGroup({
4055
+ pid,
4056
+ birthIdentity: entry.birthIdentity,
4057
+ graceMs: this.limits.terminationGraceMs,
4058
+ platform: this.platform
4059
+ }).catch(() => void 0);
4060
+ } else {
4061
+ try {
4062
+ child.kill("SIGKILL");
4063
+ } catch {
4064
+ }
4065
+ }
4066
+ bucket.delete(entry);
4067
+ if (bucket.size === 0 && this.live.get(request.sessionId) === bucket) {
4068
+ this.live.delete(request.sessionId);
4069
+ }
4070
+ };
4071
+ const probe = await probeProcess(pid, this.platform);
4072
+ const birthIdentity = probe.state === "alive" ? probe.identity : null;
4073
+ entry.birthIdentity = birthIdentity;
4074
+ if (request.role === "root" && birthIdentity === null) {
4075
+ await abandon();
4076
+ const recordedExit = () => exitResult;
4077
+ const exit = recordedExit();
4078
+ const why = probe.state === "gone" ? "the process had already exited" : probe.state === "unknown" ? `the probe could not look (${probe.reason})` : "the process could not be identified";
4079
+ const exited2 = exit !== null ? `exit code ${exit.exitCode}` : "no exit recorded yet";
4080
+ const said = stderrHead.trim().length > 0 ? `; stderr: ${JSON.stringify(stderrHead.trim().slice(0, 1024))}` : "";
4081
+ throw new SupervisorError(
4082
+ `could not read the process birth identity for the harness root \u2014 ${why} (${exited2})${said}; refusing to run a tree this Inspector cannot prove it owns`
4083
+ );
4084
+ }
4085
+ if (request.role === "root") {
4086
+ const record = {
4087
+ sessionId: request.sessionId,
4088
+ supervisorNonce: this.nonce,
4089
+ supervisorPid: process.pid,
4090
+ supervisorBirthIdentity: ownerBirthIdentity,
4091
+ runtimeId: request.runtimeId,
4092
+ rootPid: pid,
4093
+ processBirthIdentity: birthIdentity,
4094
+ processGroupIdentity: this.platform === "win32" ? void 0 : String(pid),
4095
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
4096
+ workspaceGrantId: request.workspaceGrantId,
4097
+ targetKind: request.targetKind,
4098
+ lifecycleState: "starting",
4099
+ sessionStateDir: request.sessionStateDir
4100
+ };
4101
+ try {
4102
+ await recordProcess(record);
4103
+ await updateLifecycleState(request.sessionId, "running");
4104
+ } catch (error) {
4105
+ await abandon();
4106
+ throw new SupervisorError(
4107
+ `could not record the supervised process for session ${request.sessionId}, so it was terminated rather than left running unrecoverably: ${error instanceof Error ? error.message : String(error)}`
4108
+ );
4109
+ }
4110
+ }
4111
+ const killTree = async () => {
4112
+ if (entry.killed) return;
4113
+ entry.killed = true;
4114
+ if (entry.birthIdentity === null) {
4115
+ try {
4116
+ child.kill("SIGKILL");
4117
+ } catch {
4118
+ }
4119
+ return;
4120
+ }
4121
+ const outcome = await terminateOwnedProcessGroup({
4122
+ pid,
4123
+ birthIdentity: entry.birthIdentity,
4124
+ graceMs: this.limits.terminationGraceMs,
4125
+ platform: this.platform
4126
+ });
4127
+ if (outcome.outcome === "escaped") {
4128
+ logger.error("[local-harness] process tree survived SIGKILL", {
4129
+ sessionId: request.sessionId,
4130
+ pid
4131
+ });
4132
+ }
4133
+ if (outcome.outcome === "unknown") {
4134
+ logger.warn("[local-harness] tree not proven stopped; will retry", {
4135
+ sessionId: request.sessionId,
4136
+ pid,
4137
+ reason: outcome.reason
4138
+ });
4139
+ }
4140
+ if (outcome.outcome === "escaped" || outcome.outcome === "unknown") {
4141
+ entry.killed = false;
4142
+ }
4143
+ };
4144
+ const onAbort = () => {
4145
+ void killTree();
4146
+ };
4147
+ request.abortSignal?.addEventListener("abort", onAbort, { once: true });
4148
+ const wallClockTimer = setTimeout(() => {
4149
+ logger.warn("[local-harness] wall-clock ceiling reached", {
4150
+ sessionId: request.sessionId,
4151
+ pid
4152
+ });
4153
+ void killTree();
4154
+ }, this.limits.maxWallClockMs);
4155
+ wallClockTimer.unref?.();
4156
+ const exited = new Promise((resolvePromise) => {
4157
+ if (exitResult !== null) {
4158
+ resolvePromise(exitResult);
4159
+ return;
4160
+ }
4161
+ notifyExit = resolvePromise;
4162
+ });
4163
+ void exited.then(() => {
4164
+ clearTimeout(wallClockTimer);
4165
+ request.abortSignal?.removeEventListener("abort", onAbort);
4166
+ if (out.truncated() || err.truncated()) {
4167
+ logger.warn("[local-harness] output ceiling reached; output dropped", {
4168
+ sessionId: request.sessionId,
4169
+ pid,
4170
+ stdout: out.truncated(),
4171
+ stderr: err.truncated()
4172
+ });
4173
+ }
4174
+ out.close();
4175
+ err.close();
4176
+ });
4177
+ return {
4178
+ pid,
4179
+ stdout: out.stream,
4180
+ stderr: err.stream,
4181
+ wait: async () => {
4182
+ const result = await exited;
4183
+ return result;
4184
+ },
4185
+ kill: async () => {
4186
+ await killTree();
4187
+ await exited;
4188
+ },
4189
+ stderrHead: () => stderrHead,
4190
+ stdoutHead: () => stdoutHead,
4191
+ exited: () => exitResult
4192
+ };
4193
+ }
4194
+ /**
4195
+ * Stop everything this supervisor started for a session, and only report
4196
+ * success once the trees are gone.
4197
+ *
4198
+ * Idempotent. Called on stop, abort, timeout, lease expiry, and destroy —
4199
+ * the lifecycle contract's terminal paths all converge here so none of them
4200
+ * can forget a helper.
4201
+ */
4202
+ /**
4203
+ * Give an already-dead root's `exit` handler the moment it needs to run.
4204
+ *
4205
+ * Node delivers `exit` asynchronously. A stop that arrives between the
4206
+ * kernel reaping the root and that delivery sees `orphanSnapshot === null`
4207
+ * and no live root, so the group is unanchored and correctly refuses to be
4208
+ * signalled — leaving descendants running and the session reported
4209
+ * `unknown`. Safe, but the tree survives, which is what the snapshot exists
4210
+ * to prevent.
4211
+ *
4212
+ * Bounded and best-effort: if the root is genuinely still alive, there is
4213
+ * nothing to wait for and this returns at once; if the event never arrives,
4214
+ * the stop proceeds exactly as it did before.
4215
+ */
4216
+ async awaitPendingExitSnapshots(bucket) {
4217
+ const pending = [...bucket].filter(
4218
+ (entry) => entry.role === "root" && entry.orphanSnapshot === null && entry.child.exitCode === null && entry.child.signalCode === null
4219
+ );
4220
+ if (pending.length === 0) return;
4221
+ const deadline = Date.now() + EXIT_SNAPSHOT_GRACE_MS;
4222
+ while (Date.now() < deadline) {
4223
+ const states = await Promise.all(
4224
+ pending.map(
4225
+ async (entry) => entry.orphanSnapshot !== null ? "settled" : (await probeProcess(entry.pid, this.platform)).state
4226
+ )
4227
+ );
4228
+ if (!states.includes("gone")) return;
4229
+ await new Promise((resolve4) => setTimeout(resolve4, 10));
4230
+ }
4231
+ }
4232
+ async stopSession(sessionId) {
4233
+ this.stopGenerations.set(
4234
+ sessionId,
4235
+ (this.stopGenerations.get(sessionId) ?? 0) + 1
4236
+ );
4237
+ const bucket = this.live.get(sessionId);
4238
+ await updateLifecycleState(sessionId, "stopping");
4239
+ let escaped = 0;
4240
+ if (bucket) {
4241
+ await this.awaitPendingExitSnapshots(bucket);
4242
+ const outcomes = await Promise.all(
4243
+ [...bucket].map(async (entry) => {
4244
+ if (entry.birthIdentity === null) {
4245
+ entry.child.kill("SIGKILL");
4246
+ return {
4247
+ entry,
4248
+ result: {
4249
+ outcome: "unknown",
4250
+ reason: "the process was never identified, so only its own handle could be signalled"
4251
+ }
4252
+ };
4253
+ }
4254
+ return {
4255
+ entry,
4256
+ result: await terminateOwnedProcessGroup({
4257
+ pid: entry.pid,
4258
+ birthIdentity: entry.birthIdentity,
4259
+ graceMs: this.limits.terminationGraceMs,
4260
+ platform: this.platform
4261
+ })
4262
+ };
4263
+ })
4264
+ );
4265
+ for (const { entry, result: initialResult } of outcomes) {
4266
+ let result = initialResult;
4267
+ if (result.outcome === "unknown" && entry.orphanSnapshot !== null) {
4268
+ const members = await entry.orphanSnapshot ?? [];
4269
+ const settled = [];
4270
+ for (const member of members) {
4271
+ settled.push({
4272
+ pid: member.pid,
4273
+ outcome: await terminateOwnedProcess({
4274
+ pid: member.pid,
4275
+ identity: member.identity,
4276
+ graceMs: this.limits.terminationGraceMs,
4277
+ platform: this.platform
4278
+ })
4279
+ });
4280
+ }
4281
+ const after = await probeProcessGroup(entry.pid, this.platform);
4282
+ logger.debug("[local-harness] settled an unanchored process group", {
4283
+ sessionId,
4284
+ members: settled.length,
4285
+ outcomes: settled.map((s) => s.outcome),
4286
+ groupAfter: after
4287
+ });
4288
+ if (after === "empty") result = { outcome: "forced" };
4289
+ }
4290
+ if (result.outcome === "already-gone" || result.outcome === "graceful" || result.outcome === "forced") {
4291
+ bucket.delete(entry);
4292
+ } else {
4293
+ escaped += 1;
4294
+ }
4295
+ }
4296
+ if (bucket.size === 0 && this.live.get(sessionId) === bucket) {
4297
+ this.live.delete(sessionId);
4298
+ }
4299
+ }
4300
+ if (escaped === 0) {
4301
+ await forgetProcess(sessionId);
4302
+ return { stopped: true, escaped };
4303
+ }
4304
+ await updateLifecycleState(sessionId, "failed");
4305
+ return { stopped: false, escaped };
4306
+ }
4307
+ /** Live supervised process count for a session (tests and diagnostics). */
4308
+ liveProcessCount(sessionId) {
4309
+ const bucket = this.live.get(sessionId);
4310
+ if (!bucket) return 0;
4311
+ return [...bucket].filter((entry) => !entry.exited).length;
4312
+ }
4313
+ /**
4314
+ * Is `pid` a process this session started, or a descendant of one?
4315
+ *
4316
+ * The loopback model gateway asks this before serving a connection, so a
4317
+ * process that learned the session capability — it reaches the child in its
4318
+ * environment and is written to the bridge's start config — still cannot use
4319
+ * it unless it is part of this tree.
4320
+ *
4321
+ * Descendants count, and are matched by process GROUP rather than by walking
4322
+ * a parent chain: the vendor CLI is spawned by the bridge, not by us, so it
4323
+ * is never in `live`, but the supervisor puts every root in its own group and
4324
+ * the CLI inherits it. A platform that cannot report a group id answers
4325
+ * `false` for the descendant half and the direct check still holds.
4326
+ */
4327
+ async ownsPid(sessionId, pid) {
4328
+ const bucket = this.live.get(sessionId);
4329
+ if (!bucket) return false;
4330
+ const roots = [];
4331
+ for (const entry of bucket) {
4332
+ if (entry.exited) continue;
4333
+ if (entry.pid === pid) return true;
4334
+ if (entry.role === "root") roots.push(entry.pid);
4335
+ }
4336
+ if (roots.length === 0) return false;
4337
+ const cached2 = this.pidGroups.get(pid);
4338
+ if (cached2 !== void 0) return roots.includes(cached2);
4339
+ const pgid = await readProcessGroupId(pid, this.platform);
4340
+ if (pgid === null) return false;
4341
+ this.pidGroups.set(pid, pgid);
4342
+ return roots.includes(pgid);
4343
+ }
4344
+ };
4345
+
4346
+ // server/utils/harness/local/session-registry.ts
4347
+ var sessions = /* @__PURE__ */ new Map();
4348
+ var unstopped = /* @__PURE__ */ new Set();
4349
+ function registerLocalHarnessSession(record) {
4350
+ sessions.set(record.sessionId, record);
4351
+ }
4352
+ function getLocalHarnessSession(sessionId) {
4353
+ return sessions.get(sessionId);
4354
+ }
4355
+ function forgetLocalHarnessSession(sessionId) {
4356
+ sessions.delete(sessionId);
4357
+ }
4358
+ function forgetLocalHarnessSessionRecord(record) {
4359
+ unstopped.delete(record);
4360
+ if (sessions.get(record.sessionId) !== record) return false;
4361
+ sessions.delete(record.sessionId);
4362
+ return true;
4363
+ }
4364
+ function claimRecord(record) {
4365
+ const claimed = sessions.get(record.sessionId) === record || unstopped.has(record);
4366
+ if (!claimed) return false;
4367
+ if (sessions.get(record.sessionId) === record) {
4368
+ sessions.delete(record.sessionId);
4369
+ }
4370
+ unstopped.delete(record);
4371
+ return true;
4372
+ }
4373
+ async function endRecord(record) {
4374
+ const sessionId = record.sessionId;
4375
+ const errors = [];
4376
+ try {
4377
+ record.gateway?.revoke();
4378
+ } catch (error) {
4379
+ errors.push(`gateway revoke: ${messageOf(error)}`);
4380
+ }
4381
+ try {
4382
+ await record.gateway?.close();
4383
+ } catch (error) {
4384
+ errors.push(`gateway close: ${messageOf(error)}`);
4385
+ }
4386
+ try {
4387
+ await record.revokeLease?.();
4388
+ } catch (error) {
4389
+ errors.push(`lease revoke: ${messageOf(error)}`);
4390
+ }
4391
+ let stopped = true;
4392
+ try {
4393
+ const outcome = await record.stop();
4394
+ if (outcome?.stopped !== true) {
4395
+ stopped = false;
4396
+ errors.push(
4397
+ `stop: ${outcome?.escaped ?? "some"} process(es) escaped the session`
4398
+ );
4399
+ }
4400
+ } catch (error) {
4401
+ stopped = false;
4402
+ errors.push(`stop: ${messageOf(error)}`);
4403
+ }
4404
+ if (stopped) {
4405
+ unstopped.delete(record);
4406
+ try {
4407
+ await record.releaseRuntime?.();
4408
+ } catch (error) {
4409
+ errors.push(`runtime release: ${messageOf(error)}`);
4410
+ }
4411
+ } else {
4412
+ unstopped.add(record);
4413
+ if (!sessions.has(sessionId)) sessions.set(sessionId, record);
4414
+ }
4415
+ if (errors.length > 0) {
4416
+ logger.warn("[local-harness] session teardown had failures", {
4417
+ sessionId,
4418
+ errors
4419
+ });
4420
+ }
4421
+ return { stopped, errors };
4422
+ }
4423
+ async function stopAllLocalHarnessSessions() {
4424
+ const candidates = [.../* @__PURE__ */ new Set([...sessions.values(), ...unstopped])];
4425
+ const mine = candidates.filter(claimRecord);
4426
+ const results = await Promise.all(
4427
+ mine.map(
4428
+ (record) => endRecord(record).catch(() => ({
4429
+ stopped: false,
4430
+ errors: ["unexpected"]
4431
+ }))
4432
+ )
4433
+ );
4434
+ const failed = results.filter((result) => !result.stopped).length;
4435
+ return { ok: failed === 0, stopped: results.length - failed, failed };
4436
+ }
4437
+ function messageOf(error) {
4438
+ return error instanceof Error ? error.message : String(error);
4439
+ }
4440
+ async function stopLocalHarnessWorkspace(workspaceGrantId) {
4441
+ const records = new Set([...sessions.values(), ...unstopped].filter((record) => record.workspaceGrantId === workspaceGrantId));
4442
+ let stopped = true;
4443
+ for (const record of records) {
4444
+ if (claimRecord(record)) stopped = (await endRecord(record)).stopped && stopped;
4445
+ else stopped = false;
4446
+ }
4447
+ return stopped;
4448
+ }
4449
+ async function stopLocalHarnessProject(userId, projectId) {
4450
+ const records = new Set([...sessions.values(), ...unstopped].filter((record) => record.userId === userId && (projectId === void 0 || record.projectId === projectId)));
4451
+ await Promise.all([...records].filter(claimRecord).map(endRecord));
4452
+ }
4453
+
4454
+ // server/utils/harness/local/local-turn.ts
4455
+ import { join as join5 } from "path";
4456
+ import { mkdir as mkdir6, stat as stat4 } from "fs/promises";
4457
+ import { createRequire } from "module";
4458
+ var sharedSupervisor = null;
4459
+ function localHarnessSupervisor() {
4460
+ sharedSupervisor ??= new LocalHarnessSupervisor();
4461
+ return sharedSupervisor;
4462
+ }
4463
+ async function prepareLocalHarnessTurn(args) {
4464
+ validateLocalHarnessSecretEnv(args.scopedEnv ?? {});
4465
+ const verifyStartedAt = Date.now();
4466
+ const runtimeStatus = await readRuntimeInstallStatus({
4467
+ harnessId: "claude-code"
4468
+ });
4469
+ if (runtimeStatus.state !== "ready") {
4470
+ return {
4471
+ ok: false,
4472
+ status: "runtime-unavailable",
4473
+ message: runtimeStatus.state === "absent" ? "The local Claude Code runtime is not installed on this machine yet." : `The local Claude Code runtime is not usable (${runtimeStatus.state}).`
4474
+ };
4475
+ }
4476
+ const target = localPackTarget();
4477
+ const lifecycleKey = target === null ? null : {
4478
+ runtimeRoot: runtimeInstallRoot(),
4479
+ harnessId: "claude-code",
4480
+ target,
4481
+ packVersion: runtimeStatus.packVersion,
4482
+ treeDigest: runtimeStatus.digest
4483
+ };
4484
+ let runtimeUse = null;
4485
+ if (lifecycleKey !== null) {
4486
+ try {
4487
+ runtimeUse = await reserveRuntimeUse({
4488
+ key: lifecycleKey,
4489
+ runtimeRoot: runtimeStatus.runtimeRoot,
4490
+ label: args.sessionId
4491
+ });
4492
+ } catch (error) {
4493
+ return {
4494
+ ok: false,
4495
+ status: "runtime-unavailable",
4496
+ message: `This Inspector could not reserve the local runtime, so it will not start a session that another process could replace underneath: ${error instanceof Error ? error.message : String(error)}`
4497
+ };
4498
+ }
4499
+ }
4500
+ const releaseRuntimeUse = async () => {
4501
+ const held = runtimeUse;
4502
+ runtimeUse = null;
4503
+ await held?.release();
4504
+ };
4505
+ try {
4506
+ const prepared = await prepareWithReservedRuntime({
4507
+ args,
4508
+ runtimeStatus,
4509
+ verifyStartedAt,
4510
+ releaseRuntimeUse
4511
+ });
4512
+ if (!prepared.ok) await releaseRuntimeUse();
4513
+ return prepared;
4514
+ } catch (error) {
4515
+ await releaseRuntimeUse();
4516
+ throw error;
4517
+ }
4518
+ }
4519
+ async function prepareWithReservedRuntime(outer) {
4520
+ const args = outer.args;
4521
+ const runtimeStatus = outer.runtimeStatus;
4522
+ const verifyStartedAt = outer.verifyStartedAt;
4523
+ const sessionStateDir = sessionStateDirFor(
4524
+ localHarnessStateRoot(),
4525
+ args.sessionId
4526
+ );
4527
+ if (getLocalHarnessSession(args.sessionId)) {
4528
+ return {
4529
+ ok: false,
4530
+ status: "session-still-running",
4531
+ message: "The previous local turn is still stopping. Try again once it has stopped."
4532
+ };
4533
+ }
4534
+ const availability = await resolveLocalHarnessAvailability({
4535
+ target: {
4536
+ kind: "local-native",
4537
+ harnessId: args.harnessId,
4538
+ machineId: args.target.machineId,
4539
+ workspaceGrantId: args.target.workspaceGrantId,
4540
+ runtimeId: args.target.runtimeId,
4541
+ permissionProfile: args.target.permissionProfile,
4542
+ policyVersion: args.target.policyVersion
4543
+ },
4544
+ actor: args.actor,
4545
+ scope: args.scope,
4546
+ // Server-derived, never from a request body: consent binds to a user, and
4547
+ // a user the caller names is a user the caller chose.
4548
+ userId: args.target.actingUserId,
4549
+ projectId: args.projectId,
4550
+ grantToken: args.target.grantToken,
4551
+ runtimeRoot: runtimeStatus.runtimeRoot,
4552
+ installedAdapterVersion: args.installedAdapterVersion ?? await readInstalledAdapterVersion()
4553
+ });
4554
+ if (!availability.available) {
4555
+ return {
4556
+ ok: false,
4557
+ status: availability.status,
4558
+ message: availability.message
4559
+ };
4560
+ }
4561
+ const plan = availability.plan;
4562
+ const localRuntimeVerifyMs = Date.now() - verifyStartedAt;
4563
+ const permissionMode = args.requireToolApproval ? "allow-reads" : plan.permissionMode;
4564
+ const identity = await readLocalInstanceIdentity();
4565
+ const keyId = identity.keyId ?? getRegisteredKeyId();
4566
+ if (keyId === null) {
4567
+ return {
4568
+ ok: false,
4569
+ status: "consent-required",
4570
+ message: "This installation is not registered for local execution yet. Re-authorize local execution on this machine."
4571
+ };
4572
+ }
4573
+ const broker = await startLoopbackModelBroker({
4574
+ projectId: args.projectId,
4575
+ evalIterationId: args.evalIterationId,
4576
+ journeyRunId: args.journeyRunId,
4577
+ targetId: args.targetId,
4578
+ sessionIdx: args.sessionIdx,
4579
+ hostId: args.hostId,
4580
+ harnessId: args.harnessId,
4581
+ modelId: args.modelId,
4582
+ machineId: identity.machineId,
4583
+ keyId,
4584
+ runId: args.runId,
4585
+ ...args.maxOutputTokens !== void 0 ? { maxOutputTokens: args.maxOutputTokens } : {},
4586
+ bearer: args.bearer,
4587
+ ...args.signal ? { signal: args.signal } : {}
4588
+ });
4589
+ if (!broker.ok) {
4590
+ return {
4591
+ ok: false,
4592
+ status: "broker-unavailable",
4593
+ message: broker.error
4594
+ };
4595
+ }
4596
+ const supervisor = localHarnessSupervisor();
4597
+ let gateway = null;
4598
+ try {
4599
+ const sessionStateExists = await stat4(sessionStateDir).then((entry) => entry.isDirectory()).catch((error) => {
4600
+ if (error.code === "ENOENT") return false;
4601
+ throw error;
4602
+ });
4603
+ await mkdir6(sessionStateDir, { recursive: true, mode: 448 });
4604
+ const hooksPath = join5(sessionStateDir, "empty-git-hooks");
4605
+ if (args.scope === "unattended") await mkdir6(hooksPath, { recursive: true, mode: 448 });
4606
+ const gatewayStartedAt = Date.now();
4607
+ try {
4608
+ gateway = await startLocalModelGateway({
4609
+ lease: broker.lease,
4610
+ upstreamBaseUrl: broker.proxyBaseUrl,
4611
+ // The gateway serves only processes in this session's supervised tree.
4612
+ // Asked of the supervisor rather than captured as a pid set, because the
4613
+ // tree grows: the bridge spawns the vendor CLI after the gateway is
4614
+ // already listening.
4615
+ isSupervisedPid: (pid) => supervisor.ownsPid(args.sessionId, pid)
4616
+ });
4617
+ } catch (error) {
4618
+ await revokeLease(broker.runId, args.bearer);
4619
+ return {
4620
+ ok: false,
4621
+ status: "gateway-unavailable",
4622
+ message: `The local model gateway could not start: ${error instanceof Error ? error.message : String(error)}`
4623
+ };
4624
+ }
4625
+ const started = gateway;
4626
+ const localGatewayReadyMs = Date.now() - gatewayStartedAt;
4627
+ const bridgePort = await reserveLoopbackPort();
4628
+ const launcher = resolveNodeLauncher({
4629
+ // Inside the tree the digest just covered. The npx server's own execPath
4630
+ // is not, and Electron cannot be a launcher at all.
4631
+ bundledNodePath: plan.runtime.nodePath ?? ""
4632
+ });
4633
+ const sandbox = createSupervisedLocalHarnessProvider({
4634
+ harnessId: "claude-code",
4635
+ manifest: plan.manifest,
4636
+ runtime: plan.runtime,
4637
+ supervisor,
4638
+ launcher,
4639
+ workspacePath: plan.workspacePath,
4640
+ workspaceGrantId: plan.target.workspaceGrantId,
4641
+ sessionStateDir,
4642
+ ...args.onSecretEnvDelivered && Object.keys(args.scopedEnv ?? {}).length ? {
4643
+ onBridgeStarted: async () => {
4644
+ args.onSecretEnvDelivered?.();
4645
+ }
4646
+ } : {},
4647
+ targetKind: "local-native",
4648
+ bridgePort,
4649
+ scopedEnv: {
4650
+ ...args.scopedEnv ?? {},
4651
+ ...args.scope === "unattended" ? { GIT_CONFIG_NOSYSTEM: "1", GIT_TERMINAL_PROMPT: "0", GCM_INTERACTIVE: "never", GIT_CONFIG_COUNT: "2", GIT_CONFIG_KEY_0: "core.hooksPath", GIT_CONFIG_VALUE_0: hooksPath, GIT_CONFIG_KEY_1: "credential.helper", GIT_CONFIG_VALUE_1: "" } : {}
4652
+ }
4653
+ });
4654
+ const sessionRecord = {
4655
+ userId: args.target.actingUserId,
4656
+ projectId: args.projectId,
4657
+ sessionId: args.sessionId,
4658
+ runtimeId: plan.runtime.runtimeId,
4659
+ workspaceGrantId: plan.target.workspaceGrantId,
4660
+ brokerRunId: broker.runId,
4661
+ gateway: started,
4662
+ stop: async () => await supervisor.stopSession(args.sessionId),
4663
+ revokeLease: () => revokeLease(broker.runId, args.bearer),
4664
+ releaseRuntime: outer.releaseRuntimeUse,
4665
+ startedAt: Date.now()
4666
+ };
4667
+ let treeStopped = false;
4668
+ const teardownOnce = onceAsync(async () => {
4669
+ try {
4670
+ started.revoke();
4671
+ await started.close();
4672
+ } finally {
4673
+ await revokeLease(broker.runId, args.bearer);
4674
+ const stop = await supervisor.stopSession(args.sessionId);
4675
+ if (stop.stopped) {
4676
+ treeStopped = true;
4677
+ forgetLocalHarnessSessionRecord(sessionRecord);
4678
+ await outer.releaseRuntimeUse();
4679
+ }
4680
+ }
4681
+ });
4682
+ registerLocalHarnessSession(sessionRecord);
4683
+ logger.info("[local-harness] turn prepared", {
4684
+ sessionId: args.sessionId,
4685
+ runtimeId: plan.runtime.runtimeId,
4686
+ permissionMode,
4687
+ localRuntimeVerifyMs,
4688
+ localGatewayReadyMs
4689
+ });
4690
+ return {
4691
+ ok: true,
4692
+ prepared: {
4693
+ plan,
4694
+ sandbox,
4695
+ // The child's credential: the gateway, and a capability that means
4696
+ // nothing anywhere else. The lease is not in here and never will be.
4697
+ auth: {
4698
+ ANTHROPIC_AUTH_TOKEN: started.sessionCapability,
4699
+ ANTHROPIC_API_KEY: started.sessionCapability,
4700
+ ANTHROPIC_BASE_URL: started.baseUrl
4701
+ },
4702
+ sandboxWorkDir: "project",
4703
+ skillsBaseDir: toAdapterPath(
4704
+ join5(sessionStateDir, "home", ".claude", "skills")
4705
+ ),
4706
+ sessionStateExists,
4707
+ permissionMode,
4708
+ brokerRunId: broker.runId,
4709
+ timings: { localRuntimeVerifyMs, localGatewayReadyMs },
4710
+ teardown: teardownOnce,
4711
+ discardState: onceAsync(async () => {
4712
+ await teardownOnce();
4713
+ if (treeStopped) await removeSessionStateDir(sessionStateDir);
4714
+ })
4715
+ }
4716
+ };
4717
+ } catch (error) {
4718
+ await abandonLocalSetup({
4719
+ gateway,
4720
+ sessionId: args.sessionId,
4721
+ runId: broker.runId,
4722
+ bearer: args.bearer
4723
+ });
4724
+ throw error;
4725
+ }
4726
+ }
4727
+ async function abandonLocalSetup(args) {
4728
+ try {
4729
+ args.gateway?.revoke();
4730
+ await args.gateway?.close();
4731
+ } catch (error) {
4732
+ logger.warn("[local-harness] gateway close during abandoned setup failed", {
4733
+ message: error instanceof Error ? error.message : String(error)
4734
+ });
4735
+ }
4736
+ forgetLocalHarnessSession(args.sessionId);
4737
+ await revokeLease(args.runId, args.bearer);
4738
+ }
4739
+ async function revokeLease(runId, bearer) {
4740
+ try {
4741
+ await revokeHarnessModelBroker({ runId, bearer });
4742
+ } catch (error) {
4743
+ logger.warn("[local-harness] lease revoke failed", {
4744
+ message: error instanceof Error ? error.message : String(error)
4745
+ });
4746
+ }
4747
+ }
4748
+ var cachedAdapterVersion = null;
4749
+ async function readInstalledAdapterVersion() {
4750
+ if (cachedAdapterVersion !== null) return cachedAdapterVersion;
4751
+ if (typeof __MCPJAM_CLAUDE_ADAPTER_VERSION__ === "string") return __MCPJAM_CLAUDE_ADAPTER_VERSION__;
4752
+ try {
4753
+ const required = createRequire(import.meta.url);
4754
+ const pkg = required("@ai-sdk/harness-claude-code/package.json");
4755
+ cachedAdapterVersion = typeof pkg.version === "string" ? pkg.version : "";
4756
+ } catch {
4757
+ cachedAdapterVersion = "";
4758
+ }
4759
+ return cachedAdapterVersion;
4760
+ }
4761
+ function onceAsync(fn) {
4762
+ let started = null;
4763
+ return () => {
4764
+ started ??= fn();
4765
+ return started;
4766
+ };
4767
+ }
4768
+
4769
+ export {
4770
+ LOCAL_HARNESS_GRANT_HEADER,
4771
+ localHarnessStateRoot,
4772
+ getLocalMachineId,
4773
+ validateWorkspaceCandidate,
4774
+ registerWorkspaceGrant,
4775
+ resolveWorkspaceGrant,
4776
+ grantLocalHarnessConsent,
4777
+ revokeLocalHarnessGrants,
4778
+ pruneExpiredHarnessGrants,
4779
+ forgetWorkspaceGrant,
4780
+ listProcessRecords,
4781
+ harnessPinnedVersion,
4782
+ harnessModelSupport,
4783
+ startHarnessModelBroker,
4784
+ registerLocalInstance,
4785
+ revokeHarnessModelBroker,
4786
+ reserveHarnessBox,
4787
+ renewHarnessBoxReservation,
4788
+ releaseHarnessBoxReservation,
4789
+ localHarnessManifestsForDevelopment,
4790
+ readLocalInstanceIdentity,
4791
+ setRegisteredKeyId,
4792
+ instanceKeyFingerprint,
4793
+ resolveGitBashPath,
4794
+ sessionStateDirFor,
4795
+ stopAllLocalHarnessSessions,
4796
+ stopLocalHarnessWorkspace,
4797
+ stopLocalHarnessProject,
4798
+ localHarnessSupervisor,
4799
+ prepareLocalHarnessTurn
4800
+ };
4801
+ //# sourceMappingURL=chunk-3EDQFXTF.js.map