machine-bridge-mcp 0.6.2 → 0.8.0

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.
@@ -1,10 +1,11 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { createHash, randomBytes } from "node:crypto";
3
- import { chmodSync, closeSync, constants as fsConstants, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
3
+ import { chmodSync, closeSync, constants as fsConstants, existsSync, fstatSync, ftruncateSync, lstatSync, mkdirSync, openSync, readSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
4
4
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { ensureOwnerOnlyDir, ownerOnlyFile } from "./state.mjs";
7
7
  import { replaceFileSync } from "./atomic-fs.mjs";
8
+ import { readBoundedRegularFileSync, readBoundedRegularFileWithInfoSync } from "./secure-file.mjs";
8
9
 
9
10
  const RESOURCE_NAME = /^[a-z][a-z0-9._-]{0,63}$/;
10
11
  const JOB_ID = /^job_[A-Za-z0-9_-]{24,}$/;
@@ -95,36 +96,44 @@ export class ManagedJobManager {
95
96
  approve(args = {}, { localOperator = false } = {}) {
96
97
  if (!localOperator) this.assertEnabled("approve_job");
97
98
  const dir = this.jobDir(args.job_id);
98
- const statusFile = join(dir, "status.json");
99
- const status = readRequiredJson(statusFile, 256 * 1024, "job status");
100
- if (status.status !== "staged") throw new Error(`job is not staged: ${status.status}`);
101
- readRequiredJson(join(dir, "plan.json"), MAX_PLAN_BYTES, "job plan");
102
- status.status = "queued";
103
- status.updated_at = new Date().toISOString();
104
- status.approved_at = status.updated_at;
105
- status.approval = localOperator ? "local-operator" : "mcp";
106
- status.cleanup_guarantee = "best-effort-finally-and-recovery";
107
- atomicWriteJson(statusFile, status, 256 * 1024);
99
+ const transition = acquireJobTransitionLock(dir);
100
+ if (!transition) throw new Error("job state is being modified by another process; retry after inspecting its current status");
108
101
  try {
109
- launchRunner(dir);
110
- } catch (error) {
111
- failRunnerLaunch(dir, status, error);
112
- throw error;
102
+ const statusFile = join(dir, "status.json");
103
+ const status = readRequiredJson(statusFile, 256 * 1024, "job status");
104
+ if (status.status !== "staged") throw new Error(`job is not staged: ${status.status}`);
105
+ const plan = readRequiredJson(join(dir, "plan.json"), MAX_PLAN_BYTES, "job plan");
106
+ assertPlanIntegrity(plan, status);
107
+ status.status = "queued";
108
+ status.updated_at = new Date().toISOString();
109
+ status.approved_at = status.updated_at;
110
+ status.approval = localOperator ? "local-operator" : "mcp";
111
+ status.cleanup_guarantee = "best-effort-finally-and-recovery";
112
+ atomicWriteJson(statusFile, status, 256 * 1024);
113
+ try {
114
+ launchRunner(dir);
115
+ } catch (error) {
116
+ failRunnerLaunch(dir, status, error);
117
+ throw error;
118
+ }
119
+ return {
120
+ accepted: true,
121
+ job_id: status.job_id,
122
+ name: status.name,
123
+ status: "queued",
124
+ detached: true,
125
+ continues_without_mcp_connection: true,
126
+ approval: status.approval,
127
+ plan_sha256: status.plan_sha256,
128
+ cleanup: {
129
+ resource_copies: "best-effort",
130
+ finally_steps: "best-effort-if-declared",
131
+ restart_recovery: "best-effort-on-next-runtime-or-cli-start",
132
+ },
133
+ };
134
+ } finally {
135
+ transition.release();
113
136
  }
114
- return {
115
- accepted: true,
116
- job_id: status.job_id,
117
- name: status.name,
118
- status: "queued",
119
- detached: true,
120
- continues_without_mcp_connection: true,
121
- approval: status.approval,
122
- cleanup: {
123
- resource_copies: "best-effort",
124
- finally_steps: "best-effort-if-declared",
125
- restart_recovery: "best-effort-on-next-runtime-or-cli-start",
126
- },
127
- };
128
137
  }
129
138
 
130
139
  createJob(args, { launch }) {
@@ -225,48 +234,55 @@ export class ManagedJobManager {
225
234
  this.reconcileStatus(dir);
226
235
  const status = readRequiredJson(join(dir, "status.json"), 256 * 1024, "job status");
227
236
  const plan = readJson(join(dir, "plan.json"), MAX_PLAN_BYTES);
237
+ if (plan) assertPlanIntegrity(plan, status);
228
238
  return {
229
239
  ...publicStatus(status),
240
+ plan_integrity_verified: Boolean(plan),
230
241
  ...(plan ? { review_plan: reviewablePlan(plan) } : {}),
231
242
  };
232
243
  }
233
244
 
234
245
  cancel(args = {}) {
235
246
  const dir = this.jobDir(args.job_id);
236
- this.reconcileStatus(dir);
237
- const status = readRequiredJson(join(dir, "status.json"), 256 * 1024, "job status");
238
- if (status.status === "staged") {
239
- const now = new Date().toISOString();
240
- status.status = "cancelled_before_start";
241
- status.updated_at = now;
242
- status.finished_at = now;
243
- status.error_class = "cancelled";
244
- status.cleanup_guarantee = "not-started";
245
- atomicWriteJson(join(dir, "status.json"), status, 256 * 1024);
246
- atomicWriteJson(join(dir, "result.json"), {
247
- job_id: status.job_id,
248
- name: status.name,
249
- status: status.status,
250
- steps: [],
251
- finally_steps: [],
252
- error_class: "cancelled",
253
- cleanup_error_class: null,
254
- finished_at: now,
255
- }, 4 * 1024 * 1024);
256
- scrubFinishedPlan(dir, status);
257
- return { ...publicStatus(status), cancellation_requested: true, cleanup_will_run: false, execution_started: false };
258
- }
259
- if (!ACTIVE_JOB_STATES.has(status.status)) {
260
- return { ...publicStatus(status), cancellation_requested: false, already_finished: true };
247
+ const transition = acquireJobTransitionLock(dir);
248
+ if (!transition) throw new Error("job state is being modified by another process; retry after inspecting its current status");
249
+ try {
250
+ this.reconcileStatus(dir);
251
+ const status = readRequiredJson(join(dir, "status.json"), 256 * 1024, "job status");
252
+ if (status.status === "staged") {
253
+ const now = new Date().toISOString();
254
+ status.status = "cancelled_before_start";
255
+ status.updated_at = now;
256
+ status.finished_at = now;
257
+ status.error_class = "cancelled";
258
+ status.cleanup_guarantee = "not-started";
259
+ atomicWriteJson(join(dir, "status.json"), status, 256 * 1024);
260
+ atomicWriteJson(join(dir, "result.json"), {
261
+ job_id: status.job_id,
262
+ name: status.name,
263
+ status: status.status,
264
+ steps: [],
265
+ finally_steps: [],
266
+ error_class: "cancelled",
267
+ cleanup_error_class: null,
268
+ finished_at: now,
269
+ }, 4 * 1024 * 1024);
270
+ scrubFinishedPlan(dir, status);
271
+ return { ...publicStatus(status), cancellation_requested: true, cleanup_will_run: false, execution_started: false };
272
+ }
273
+ if (!ACTIVE_JOB_STATES.has(status.status)) {
274
+ return { ...publicStatus(status), cancellation_requested: false, already_finished: true };
275
+ }
276
+ writeFileSync(join(dir, "cancel"), `${new Date().toISOString()}\n`, { mode: 0o600 });
277
+ return {
278
+ ...publicStatus(status),
279
+ cancellation_requested: true,
280
+ cancellation_delivery: "runner-poll",
281
+ cleanup_will_run: true,
282
+ };
283
+ } finally {
284
+ transition.release();
261
285
  }
262
- writeFileSync(join(dir, "cancel"), `${new Date().toISOString()}\n`, { mode: 0o600 });
263
- return {
264
- ...publicStatus(status),
265
- cancellation_requested: true,
266
- cancellation_delivery: "runner-poll",
267
- cleanup_will_run: true,
268
- };
269
-
270
286
  }
271
287
 
272
288
  currentResources() {
@@ -312,38 +328,10 @@ export class ManagedJobManager {
312
328
  if (Number.isInteger(pid) && pid > 0 && isPidAlive(pid)) return;
313
329
  const recoveryAttempts = Number(status.recovery_attempts || 0);
314
330
  if (recoveryAttempts >= MAX_RECOVERY_ATTEMPTS) {
315
- const now = new Date().toISOString();
316
- status.status = "recovery_exhausted";
317
- status.updated_at = now;
318
- status.finished_at = now;
319
- status.error_class = "recovery_exhausted";
320
- status.current_phase = null;
321
- status.current_step = null;
322
- atomicWriteJson(file, status, 256 * 1024);
323
- atomicWriteJson(join(dir, "result.json"), {
324
- job_id: status.job_id,
325
- name: status.name,
326
- status: status.status,
327
- recovered: true,
328
- steps: [],
329
- finally_steps: [],
330
- error_class: "recovery_exhausted",
331
- cleanup_error_class: "recovery_exhausted",
332
- recovery_attempts: recoveryAttempts,
333
- finished_at: now,
334
- }, 4 * 1024 * 1024);
335
- rmSync(join(dir, "runtime"), { recursive: true, force: true });
336
- scrubFinishedPlan(dir, status);
331
+ markRecoveryExhausted(dir, file, status, recoveryAttempts);
337
332
  return;
338
333
  }
339
- status.status = "interrupted";
340
- status.updated_at = new Date().toISOString();
341
- status.finished_at = status.updated_at;
342
- status.error_class = "runner_interrupted";
343
- status.recovery_attempts = recoveryAttempts + 1;
344
- atomicWriteJson(file, status, 256 * 1024);
345
- rmSync(join(dir, "runtime"), { recursive: true, force: true });
346
- const runnerPid = launchRunner(dir, true);
334
+ const runnerPid = relaunchInterruptedJob(dir, file, status, recoveryAttempts);
347
335
  recoveryLock.handoff(runnerPid);
348
336
  handedOff = true;
349
337
  } finally {
@@ -438,13 +426,14 @@ export function validateResourceName(value) {
438
426
  export function inspectResourceFile(inputPath, { allowInsecurePermissions = false, includeHash = false } = {}) {
439
427
  const path = resolve(String(inputPath || ""));
440
428
  const canonical = realpathFile(path);
441
- const { buffer: content, info } = readBoundedFileWithInfo(canonical, MAX_RESOURCE_BYTES);
429
+ const { buffer: content, info } = readBoundedRegularFileWithInfoSync(canonical, MAX_RESOURCE_BYTES);
442
430
  if (process.platform !== "win32" && !allowInsecurePermissions && (info.mode & 0o077) !== 0) {
443
431
  throw new Error("resource file is readable by group or others; restrict permissions or use --allow-insecure-permissions");
444
432
  }
445
433
  return {
446
434
  kind: "file",
447
435
  path: canonical,
436
+ pathAliases: normalizeResourcePathAliases([path, canonical]),
448
437
  size: info.size,
449
438
  mode: process.platform === "win32" ? null : `0${(info.mode & 0o777).toString(8)}`,
450
439
  updatedAt: new Date().toISOString(),
@@ -453,15 +442,16 @@ export function inspectResourceFile(inputPath, { allowInsecurePermissions = fals
453
442
  };
454
443
  }
455
444
 
456
- export function publicResourceRegistry(resources = {}) {
445
+ export function publicResourceRegistry(resources = {}, { includePaths = false } = {}) {
457
446
  const normalized = normalizeResourceRegistry(resources);
458
447
  return Object.fromEntries(Object.entries(normalized).map(([name, value]) => [name, {
459
448
  kind: value.kind,
460
- path: value.path,
461
449
  size: value.size ?? null,
462
450
  mode: value.mode ?? null,
463
451
  updatedAt: value.updatedAt ?? null,
464
452
  allowInsecurePermissions: value.allowInsecurePermissions === true,
453
+ paths_exposed: includePaths,
454
+ ...(includePaths ? { path: value.path } : {}),
465
455
  }]));
466
456
  }
467
457
 
@@ -580,7 +570,11 @@ function referencedResources(steps, registry) {
580
570
  const inspected = inspectResourceFile(resource.path, { allowInsecurePermissions: resource.allowInsecurePermissions === true, includeHash: true });
581
571
  totalBytes += inspected.size;
582
572
  if (totalBytes > MAX_JOB_RESOURCE_BYTES) throw new Error(`job resources exceed ${MAX_JOB_RESOURCE_BYTES} bytes`);
583
- out[name] = { ...inspected, allowInsecurePermissions: resource.allowInsecurePermissions === true };
573
+ out[name] = {
574
+ ...inspected,
575
+ pathAliases: normalizeResourcePathAliases([...(resource.pathAliases || []), ...(inspected.pathAliases || [])]),
576
+ allowInsecurePermissions: resource.allowInsecurePermissions === true,
577
+ };
584
578
  }
585
579
  return out;
586
580
  }
@@ -594,6 +588,7 @@ function normalizeResourceRegistry(resources) {
594
588
  out[name] = {
595
589
  kind: "file",
596
590
  path: resolve(rawValue.path),
591
+ pathAliases: normalizeResourcePathAliases([rawValue.path, ...(Array.isArray(rawValue.pathAliases) ? rawValue.pathAliases : [])]),
597
592
  size: Number.isFinite(Number(rawValue.size)) ? Number(rawValue.size) : null,
598
593
  mode: rawValue.mode ?? null,
599
594
  updatedAt: rawValue.updatedAt ?? null,
@@ -603,6 +598,17 @@ function normalizeResourceRegistry(resources) {
603
598
  return out;
604
599
  }
605
600
 
601
+ function normalizeResourcePathAliases(values) {
602
+ const aliases = [];
603
+ for (const value of values) {
604
+ if (typeof value !== "string" || !value || value.includes("\0") || value.length > 4096) continue;
605
+ const absolute = resolve(value);
606
+ if (!aliases.includes(absolute)) aliases.push(absolute);
607
+ if (aliases.length >= 8) break;
608
+ }
609
+ return aliases;
610
+ }
611
+
606
612
  function resolveJobCwd(value, workspace, unrestrictedPaths) {
607
613
  const raw = boundedString(value, 4096, "cwd");
608
614
  const candidate = isAbsolute(raw) ? resolve(raw) : resolve(workspace, raw);
@@ -640,15 +646,76 @@ function failRunnerLaunch(dir, status, error) {
640
646
  scrubFinishedPlan(dir, failed);
641
647
  }
642
648
 
649
+ function markRecoveryExhausted(dir, statusFile, status, recoveryAttempts) {
650
+ const now = new Date().toISOString();
651
+ Object.assign(status, {
652
+ status: "recovery_exhausted",
653
+ updated_at: now,
654
+ finished_at: now,
655
+ error_class: "recovery_exhausted",
656
+ current_phase: null,
657
+ current_step: null,
658
+ });
659
+ atomicWriteJson(statusFile, status, 256 * 1024);
660
+ atomicWriteJson(join(dir, "result.json"), {
661
+ job_id: status.job_id,
662
+ name: status.name,
663
+ status: status.status,
664
+ recovered: true,
665
+ steps: [],
666
+ finally_steps: [],
667
+ error_class: "recovery_exhausted",
668
+ cleanup_error_class: "recovery_exhausted",
669
+ recovery_attempts: recoveryAttempts,
670
+ finished_at: now,
671
+ }, 4 * 1024 * 1024);
672
+ rmSync(join(dir, "runtime"), { recursive: true, force: true });
673
+ scrubFinishedPlan(dir, status);
674
+ }
675
+
676
+ function relaunchInterruptedJob(dir, statusFile, status, recoveryAttempts) {
677
+ status.status = "interrupted";
678
+ status.updated_at = new Date().toISOString();
679
+ status.finished_at = status.updated_at;
680
+ status.error_class = "runner_interrupted";
681
+ status.recovery_attempts = recoveryAttempts + 1;
682
+ atomicWriteJson(statusFile, status, 256 * 1024);
683
+ rmSync(join(dir, "runtime"), { recursive: true, force: true });
684
+ rmSync(join(dir, "runner.pid"), { force: true });
685
+ return launchRunner(dir, true);
686
+ }
687
+
643
688
  function acquireRecoveryLock(dir) {
644
- const file = join(dir, "recovery.lock");
689
+ return acquirePidLock(join(dir, "recovery.lock"), { allowHandoff: true });
690
+ }
691
+
692
+ function planSha256(plan) {
693
+ return createHash("sha256").update(JSON.stringify(plan)).digest("hex");
694
+ }
695
+
696
+ function assertPlanIntegrity(plan, status) {
697
+ const expected = String(status?.plan_sha256 || "");
698
+ const actual = planSha256(plan);
699
+ if (!/^[a-f0-9]{64}$/.test(expected) || actual !== expected) {
700
+ throw new Error("managed job plan integrity check failed; inspect the plan and do not approve it");
701
+ }
702
+ return actual;
703
+ }
704
+
705
+ function acquireJobTransitionLock(dir) {
706
+ return acquirePidLock(join(dir, "transition.lock"));
707
+ }
708
+
709
+ function acquirePidLock(file, { allowHandoff = false } = {}) {
645
710
  for (let attempt = 0; attempt < 2; attempt += 1) {
646
711
  try {
647
712
  writeFileSync(file, `${process.pid}\n`, { mode: 0o600, flag: "wx" });
648
713
  return {
649
- handoff(pid) {
650
- if (Number.isInteger(pid) && pid > 0) writeFileSync(file, `${pid}\n`, { mode: 0o600 });
651
- },
714
+ ...(allowHandoff ? {
715
+ handoff(pid) {
716
+ if (Number.isInteger(pid) && pid > 0) writeFileSync(file, `${pid}\n`, { mode: 0o600 });
717
+ },
718
+ } : {}),
652
719
  release() { rmSync(file, { force: true }); },
653
720
  };
654
721
  } catch (error) {
@@ -656,7 +723,9 @@ function acquireRecoveryLock(dir) {
656
723
  let owner = 0;
657
724
  try { owner = Number.parseInt(readBoundedFile(file, 64).toString("utf8").trim(), 10); } catch {}
658
725
  const age = Date.now() - safeMtime(file);
659
- if ((owner > 0 && isPidAlive(owner)) || age < 60_000) return null;
726
+ const ownerAlive = owner > 0 && isPidAlive(owner);
727
+ const definitelyStale = age >= 5 * 60_000 || (!ownerAlive && age >= 60_000);
728
+ if (!definitelyStale) return null;
660
729
  rmSync(file, { force: true });
661
730
  }
662
731
  }
@@ -670,18 +739,20 @@ function launchRunner(dir, recover = false) {
670
739
  const stderrFile = join(dir, "runner.err.log");
671
740
  trimDiagnosticFile(stdoutFile);
672
741
  trimDiagnosticFile(stderrFile);
673
- const stdoutFd = openSync(stdoutFile, "a", 0o600);
674
- const stderrFd = openSync(stderrFile, "a", 0o600);
742
+ let stdoutFd;
743
+ let stderrFd;
675
744
  let child;
676
745
  try {
746
+ stdoutFd = openPrivateAppendFile(stdoutFile);
747
+ stderrFd = openPrivateAppendFile(stderrFile);
677
748
  child = spawn(process.execPath, args, {
678
749
  detached: true,
679
750
  stdio: ["ignore", stdoutFd, stderrFd],
680
751
  windowsHide: true,
681
752
  });
682
753
  } finally {
683
- closeSync(stdoutFd);
684
- closeSync(stderrFd);
754
+ if (stdoutFd !== undefined) closeSync(stdoutFd);
755
+ if (stderrFd !== undefined) closeSync(stderrFd);
685
756
  }
686
757
  ownerOnlyFile(stdoutFile);
687
758
  ownerOnlyFile(stderrFile);
@@ -704,6 +775,7 @@ function scrubFinishedPlan(dir, status) {
704
775
  rmSync(join(dir, "plan.json"), { force: true });
705
776
  rmSync(join(dir, "runner.pid"), { force: true });
706
777
  rmSync(join(dir, "recovery.lock"), { force: true });
778
+ rmSync(join(dir, "transition.lock"), { force: true });
707
779
  }
708
780
 
709
781
  function reviewablePlan(plan) {
@@ -763,33 +835,30 @@ function readRequiredJson(file, maxBytes, label) {
763
835
  }
764
836
 
765
837
  function readBoundedFile(file, maxBytes) {
766
- return readBoundedFileWithInfo(file, maxBytes).buffer;
838
+ return readBoundedRegularFileSync(file, maxBytes);
767
839
  }
768
840
 
769
- function readBoundedFileWithInfo(file, maxBytes) {
770
- const flags = Number(fsConstants.O_RDONLY) | Number(fsConstants.O_NOFOLLOW || 0);
771
- const fd = openSync(file, flags);
841
+ function openPrivateAppendFile(file) {
842
+ if (existsSync(file)) {
843
+ const info = lstatSync(file);
844
+ if (info.isSymbolicLink() || !info.isFile()) throw new Error("runner diagnostic path must be a regular file and not a symbolic link");
845
+ }
846
+ const flags = Number(fsConstants.O_WRONLY) | Number(fsConstants.O_CREAT) | Number(fsConstants.O_APPEND) | Number(fsConstants.O_NOFOLLOW || 0);
847
+ const fd = openSync(file, flags, 0o600);
772
848
  try {
773
- const info = fstatSync(fd);
774
- if (!info.isFile()) throw new Error("path is not a regular file");
775
- if (info.size > maxBytes) throw new Error(`file exceeds ${maxBytes} bytes`);
776
- const buffer = Buffer.alloc(info.size);
777
- let offset = 0;
778
- while (offset < buffer.length) {
779
- const count = readSync(fd, buffer, offset, buffer.length - offset, offset);
780
- if (!count) break;
781
- offset += count;
782
- }
783
- return { buffer: buffer.subarray(0, offset), info };
784
- } finally {
849
+ if (!fstatSync(fd).isFile()) throw new Error("runner diagnostic path is not a regular file");
850
+ chmodSync(file, 0o600);
851
+ return fd;
852
+ } catch (error) {
785
853
  closeSync(fd);
854
+ throw error;
786
855
  }
787
856
  }
788
857
 
789
858
  function trimDiagnosticFile(file, maxBytes = 64 * 1024, keepBytes = 32 * 1024) {
790
859
  let fd;
791
860
  try {
792
- const flags = Number(fsConstants.O_RDONLY) | Number(fsConstants.O_NOFOLLOW || 0);
861
+ const flags = Number(fsConstants.O_RDWR) | Number(fsConstants.O_NOFOLLOW || 0);
793
862
  fd = openSync(file, flags);
794
863
  const info = fstatSync(fd);
795
864
  if (!info.isFile() || info.size <= maxBytes) return;
@@ -804,9 +873,9 @@ function trimDiagnosticFile(file, maxBytes = 64 * 1024, keepBytes = 32 * 1024) {
804
873
  let tail = buffer.subarray(0, offset);
805
874
  const newline = tail.indexOf(0x0a);
806
875
  if (newline >= 0 && newline < tail.length - 1) tail = tail.subarray(newline + 1);
807
- closeSync(fd);
808
- fd = undefined;
809
- writeFileSync(file, tail, { mode: 0o600 });
876
+ ftruncateSync(fd, 0);
877
+ if (tail.length) writeSync(fd, tail, 0, tail.length, 0);
878
+ try { chmodSync(file, 0o600); } catch {}
810
879
  } catch {
811
880
  } finally {
812
881
  if (fd !== undefined) try { closeSync(fd); } catch {}
@@ -4,11 +4,11 @@ import { basename } from "node:path";
4
4
  import { executionEnv } from "./shell.mjs";
5
5
 
6
6
  export const MAX_COMMAND_BYTES = 64 * 1024;
7
- export const MAX_ARGV_ITEMS = 256;
8
- export const MAX_PROCESS_SESSIONS = 8;
9
- export const MAX_SESSION_OUTPUT_BYTES = 1024 * 1024;
10
- export const MAX_PROCESS_STDIN_BYTES = 64 * 1024;
11
- export const PROCESS_SESSION_RETENTION_MS = 30 * 60 * 1000;
7
+ const MAX_ARGV_ITEMS = 256;
8
+ const MAX_PROCESS_SESSIONS = 8;
9
+ const MAX_SESSION_OUTPUT_BYTES = 1024 * 1024;
10
+ const MAX_PROCESS_STDIN_BYTES = 64 * 1024;
11
+ const PROCESS_SESSION_RETENTION_MS = 30 * 60 * 1000;
12
12
 
13
13
  export class ProcessSessionManager {
14
14
  constructor({ workspace, policy, runtimeDir, activeProcesses, callProcesses, resolveCwd, displayPath, throwIfCancelled }) {