@agent-compose/sdk 0.7.0 → 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.
Files changed (116) hide show
  1. package/README.md +66 -39
  2. package/dist/agent/__tests__/runtime-json-schema.test.d.ts +10 -0
  3. package/dist/agent/agent-context.d.ts +21 -1
  4. package/dist/agent/agent-loop.d.ts +24 -1
  5. package/dist/client.d.ts +338 -534
  6. package/dist/directives.d.ts +112 -0
  7. package/dist/display.d.ts +242 -0
  8. package/dist/errors.d.ts +24 -1
  9. package/dist/index.d.ts +24 -12
  10. package/dist/index.js +3545 -1667
  11. package/dist/pause/wrappers.d.ts +31 -9
  12. package/dist/runtimes/_acp-client.d.ts +46 -1
  13. package/dist/runtimes/_cli-agent.d.ts +49 -4
  14. package/dist/runtimes/_jsonl-guard.d.ts +103 -0
  15. package/dist/runtimes/amp.d.ts +2 -2
  16. package/dist/runtimes/claude-code.d.ts +59 -0
  17. package/dist/runtimes/claude-code.test.d.ts +14 -0
  18. package/dist/runtimes/claude.d.ts +16 -0
  19. package/dist/runtimes/claude.test.d.ts +8 -0
  20. package/dist/runtimes/codex.d.ts +9 -3
  21. package/dist/runtimes/cursor.d.ts +2 -2
  22. package/dist/runtimes/droid.d.ts +2 -2
  23. package/dist/runtimes/jsonl-guard.test.d.ts +19 -0
  24. package/dist/runtimes/openai-desktop.js +2691 -864
  25. package/dist/runtimes/opencode.d.ts +2 -2
  26. package/dist/runtimes/vercel.js +12 -1
  27. package/dist/sandbox/devbox.d.ts +42 -0
  28. package/dist/sandbox/exec-stream.d.ts +14 -0
  29. package/dist/sandbox/network-policy.d.ts +100 -0
  30. package/dist/sandbox/provider-def.d.ts +79 -0
  31. package/dist/sandbox/providers/desktop.d.ts +10 -0
  32. package/dist/sandbox/providers/e2b.d.ts +17 -0
  33. package/dist/sandbox/providers/local.d.ts +11 -0
  34. package/dist/sandbox/providers/vercel.d.ts +18 -0
  35. package/dist/sandbox/registry.d.ts +45 -0
  36. package/dist/sandbox/sizes.d.ts +68 -0
  37. package/dist/sandbox.d.ts +24 -299
  38. package/dist/step-invocation/__tests__/foreground-recovery.test.d.ts +1 -0
  39. package/dist/step-invocation/invoker.d.ts +10 -0
  40. package/dist/step-invocation/protocol.d.ts +5 -0
  41. package/dist/types/api-compliance.d.ts +71 -0
  42. package/dist/types/api-conversations.d.ts +492 -0
  43. package/dist/types/api-factory.d.ts +309 -0
  44. package/dist/types/api-projects.d.ts +131 -0
  45. package/dist/types/api-runs.d.ts +377 -0
  46. package/dist/types/api-scopes.d.ts +102 -0
  47. package/dist/types/conversation-stream.d.ts +191 -0
  48. package/dist/types/execution-context.d.ts +12 -2
  49. package/dist/types/protocol.d.ts +30 -1
  50. package/dist/types/sandbox-environment.d.ts +8 -5
  51. package/dist/types/sandbox.d.ts +74 -4
  52. package/dist/types/workflow-metadata.d.ts +33 -8
  53. package/dist/types/workflow-plan.d.ts +10 -0
  54. package/dist/types/workflow.d.ts +18 -205
  55. package/dist/utils/bundler.d.ts +12 -1
  56. package/dist/workflow-steps/index.d.ts +1 -1
  57. package/dist/workflow-steps/observability.d.ts +8 -1
  58. package/dist/workflow-steps/runner.d.ts +3 -3
  59. package/dist/workflow-steps/step.d.ts +15 -1
  60. package/dist/workflow-steps/types.d.ts +19 -5
  61. package/dist/workflow-steps/workflow.d.ts +22 -1
  62. package/dist/workflows/engine.d.ts +3 -2
  63. package/dist/workflows/invoke-child.d.ts +2 -2
  64. package/package.json +1 -1
  65. package/src/agent/agent-context.ts +186 -3
  66. package/src/agent/agent-loop.ts +31 -2
  67. package/src/client.ts +909 -621
  68. package/src/directives.ts +184 -0
  69. package/src/display.ts +788 -0
  70. package/src/errors.ts +39 -0
  71. package/src/index.ts +104 -10
  72. package/src/pause/wrappers.ts +44 -9
  73. package/src/runtimes/_acp-client.ts +72 -3
  74. package/src/runtimes/_cli-agent.ts +159 -36
  75. package/src/runtimes/_jsonl-guard.ts +219 -0
  76. package/src/runtimes/claude-code.ts +246 -0
  77. package/src/runtimes/claude.ts +32 -2
  78. package/src/runtimes/codex.ts +55 -3
  79. package/src/runtimes/openai-desktop.ts +59 -14
  80. package/src/sandbox/devbox.ts +48 -0
  81. package/src/sandbox/exec-stream.ts +48 -0
  82. package/src/sandbox/network-policy.ts +181 -0
  83. package/src/sandbox/provider-def.ts +94 -0
  84. package/src/sandbox/providers/desktop.ts +57 -0
  85. package/src/sandbox/providers/e2b.ts +354 -0
  86. package/src/sandbox/providers/local.ts +106 -0
  87. package/src/sandbox/providers/vercel.ts +331 -0
  88. package/src/sandbox/registry.ts +198 -0
  89. package/src/sandbox/sizes.ts +95 -0
  90. package/src/sandbox.ts +59 -1275
  91. package/src/step-invocation/invoker.ts +151 -28
  92. package/src/step-invocation/protocol.ts +8 -0
  93. package/src/types/api-compliance.ts +79 -0
  94. package/src/types/api-conversations.ts +522 -0
  95. package/src/types/api-factory.ts +336 -0
  96. package/src/types/api-projects.ts +140 -0
  97. package/src/types/api-runs.ts +412 -0
  98. package/src/types/api-scopes.ts +102 -0
  99. package/src/types/conversation-stream.ts +231 -0
  100. package/src/types/execution-context.ts +10 -2
  101. package/src/types/protocol.ts +33 -0
  102. package/src/types/sandbox-environment.ts +28 -9
  103. package/src/types/sandbox.ts +73 -4
  104. package/src/types/workflow-metadata.ts +35 -8
  105. package/src/types/workflow-plan.ts +11 -0
  106. package/src/types/workflow.ts +25 -292
  107. package/src/utils/bundler.ts +32 -5
  108. package/src/utils/errors.ts +16 -1
  109. package/src/workflow-steps/index.ts +1 -0
  110. package/src/workflow-steps/observability.ts +19 -8
  111. package/src/workflow-steps/runner.ts +4 -4
  112. package/src/workflow-steps/step.ts +49 -1
  113. package/src/workflow-steps/types.ts +20 -5
  114. package/src/workflow-steps/workflow.ts +22 -1
  115. package/src/workflows/engine.ts +3 -2
  116. package/src/workflows/invoke-child.ts +2 -2
@@ -25,7 +25,7 @@ import { randomBytes } from "node:crypto";
25
25
  import { z } from "zod";
26
26
  import { SandboxUnavailableError } from "../sandbox-errors.js";
27
27
  import type { SandboxProvider, SandboxBackgroundProcess } from "../types/sandbox.js";
28
- import { RUNNER_COMMAND, STEP_ENV, stepResultLinePrefix, stepPauseLinePrefix, requestContextPath, stepInputPath, stepResultFilePath, stepLogFilePath } from "./protocol.js";
28
+ import { RUNNER_COMMAND, STEP_ENV, stepResultLinePrefix, stepPauseLinePrefix, requestContextPath, stepInputPath, stepResultFilePath, stepLogFilePath, stepPidFilePath } from "./protocol.js";
29
29
  import { StepPauseRequestSchema } from "./types.js";
30
30
  import type { StepRequest, StepResult } from "./types.js";
31
31
  import type { StepObservability } from "../workflow-steps/observability.js";
@@ -201,6 +201,11 @@ export interface InvokeStepOptions {
201
201
  * is the hook to emit a structured alert so the degradation is visible/paged.
202
202
  * Best-effort: keep it cheap and non-throwing. */
203
203
  onStreamDegraded?: (info: { error: unknown; runnerPid: number; resultToken: string }) => void;
204
+ /** Ceiling on the durable-result recovery poll after a live-stream fault
205
+ * (default 45m — the wedged-runner backstop). The activity passes its step
206
+ * ceiling so a stream fault on a step with hours of legitimate work left
207
+ * recovers instead of timing out; startToClose is the real wall. */
208
+ recoveryDeadlineMs?: number;
204
209
  }
205
210
 
206
211
  /** A step running as a background command (ADR-0028). The activity races its
@@ -331,20 +336,43 @@ async function classifyRunnerOutcome<TOutput>(
331
336
  * pause/resume) AND the launch path after a live-stream transport fault.
332
337
  * `cat`-ing the result file is itself immune to the large-frame compression bug
333
338
  * — it is a single small JSON line, far below any compression threshold. The
334
- * activity's `startToCloseTimeout` is the real upper bound; `DEADLINE` is a
335
- * backstop so a wedged runner can't leak this loop in the worker forever. */
339
+ * activity's `startToCloseTimeout` is the real upper bound; the deadline here
340
+ * is a backstop so a wedged runner can't leak this loop in the worker forever
341
+ * (default 45m; a worker-death recovery reconnecting mid-step passes the full
342
+ * step ceiling instead — hours of legitimate work may remain). */
336
343
  async function pollDurableResult<TOutput>(
337
344
  sandbox: SandboxProvider,
338
345
  runnerPid: number,
339
346
  resultToken: string,
340
- signal?: AbortSignal,
347
+ opts?: { signal?: AbortSignal; deadlineMs?: number },
341
348
  ): Promise<StepResult<TOutput>> {
349
+ const signal = opts?.signal;
342
350
  const resultFile = stepResultFilePath(resultToken);
343
351
  const POLL_MS = 1000;
344
- const DEADLINE = Date.now() + 45 * 60_000;
345
- const readResult = async (): Promise<StepResult<TOutput> | null> => {
346
- const r = await sandbox.commands.run(`cat ${resultFile} 2>/dev/null || true`).catch(() => null);
347
- return r?.stdout ? (parseStepResult<TOutput>(r.stdout, resultToken) ?? null) : null;
352
+ const deadlineMs = opts?.deadlineMs ?? 45 * 60_000;
353
+ const DEADLINE = Date.now() + deadlineMs;
354
+ // Both internal commands carry an explicit budget. Required for Vercel:
355
+ // without `timeoutMs` there is no hard client-side deadline in the provider's
356
+ // `commands.run`, so a wedged VM hangs one `cat` forever and the poll
357
+ // deadline never advances. A timeout throws → the `.catch(() => null)`
358
+ // treats it as a failed read/probe and the loop continues (a hung probe
359
+ // never misclassifies as dead). Harmless on E2B — a 1-line cat is ms-fast.
360
+ const CMD_TIMEOUT_MS = 10_000;
361
+ // A destroyed sandbox (e.g. run cancelled → sandbox killed mid-recovery)
362
+ // rejects EVERY command, which a lone `.catch(() => null)` renders
363
+ // indistinguishable from a live runner — the loop would spin until the
364
+ // deadline (hours) against a VM that no longer exists. Track consecutive
365
+ // iterations where BOTH the result read and the liveness probe rejected and
366
+ // exit with an honest runner-exit once the streak is unambiguous. 20
367
+ // iterations ≈ 30s–7min of continuous rejections (each command may burn its
368
+ // 10s budget) — long enough to ride out a transient provider-API blip,
369
+ // short enough that a dead sandbox doesn't hold the worker slot for hours.
370
+ const UNREACHABLE_LIMIT = 20;
371
+ let unreachableStreak = 0;
372
+ const readResult = async (): Promise<StepResult<TOutput> | "unreachable" | null> => {
373
+ const r = await sandbox.commands.run(`cat ${resultFile} 2>/dev/null || true`, { timeoutMs: CMD_TIMEOUT_MS }).catch(() => "unreachable" as const);
374
+ if (r === "unreachable") return "unreachable";
375
+ return r.stdout ? (parseStepResult<TOutput>(r.stdout, resultToken) ?? null) : null;
348
376
  };
349
377
  for (let attempt = 1; ; attempt++) {
350
378
  // Aborted = the activity already resolved via the pause watcher (a re-pause);
@@ -354,11 +382,26 @@ async function pollDurableResult<TOutput>(
354
382
  return { ok: false, error: { kind: "runner-exit", message: "reconnectStep aborted (run re-paused)", exitCode: 1 } };
355
383
  }
356
384
  const fromFile = await readResult();
357
- if (fromFile) return fromFile;
385
+ if (fromFile !== null && fromFile !== "unreachable") return fromFile;
358
386
 
359
387
  const probe = await sandbox.commands
360
- .run(`test -d /proc/${runnerPid} && echo alive || echo dead`)
388
+ .run(`test -d /proc/${runnerPid} && echo alive || echo dead`, { timeoutMs: CMD_TIMEOUT_MS })
361
389
  .catch(() => null);
390
+ if (fromFile === "unreachable" && probe === null) {
391
+ unreachableStreak++;
392
+ if (unreachableStreak >= UNREACHABLE_LIMIT) {
393
+ return {
394
+ ok: false,
395
+ error: {
396
+ kind: "runner-exit",
397
+ message: `sandbox stopped responding while waiting for runner pid ${runnerPid} to emit a result (${UNREACHABLE_LIMIT} consecutive failed polls)`,
398
+ exitCode: 1,
399
+ },
400
+ };
401
+ }
402
+ } else {
403
+ unreachableStreak = 0;
404
+ }
362
405
  const dead = (probe?.stdout ?? "").includes("dead");
363
406
  process.stderr.write(
364
407
  `[recover] poll ${attempt}: result-file=absent runner=${dead ? "dead" : "alive"} pid=${runnerPid}\n`,
@@ -366,7 +409,7 @@ async function pollDurableResult<TOutput>(
366
409
  if (dead) {
367
410
  // Close the write-then-exit race with one final read, else classify a crash.
368
411
  const finalRead = await readResult();
369
- if (finalRead) return finalRead;
412
+ if (finalRead !== null && finalRead !== "unreachable") return finalRead;
370
413
  return {
371
414
  ok: false,
372
415
  error: { kind: "runner-exit", message: `runner pid ${runnerPid} exited without writing a result file`, exitCode: 1 },
@@ -375,7 +418,11 @@ async function pollDurableResult<TOutput>(
375
418
  if (Date.now() > DEADLINE) {
376
419
  return {
377
420
  ok: false,
378
- error: { kind: "runner-exit", message: `timed out after 45m waiting for runner pid ${runnerPid} to emit a result`, exitCode: 1 },
421
+ error: {
422
+ kind: "runner-exit",
423
+ message: `timed out after ${Math.round(deadlineMs / 60_000)}m waiting for runner pid ${runnerPid} to emit a result`,
424
+ exitCode: 1,
425
+ },
379
426
  };
380
427
  }
381
428
  await new Promise((r) => setTimeout(r, POLL_MS));
@@ -397,8 +444,9 @@ async function recoverLogsAndResult<TOutput>(
397
444
  resultToken: string,
398
445
  liveSplitters: { stdoutConsumedChars: () => number },
399
446
  opts: Pick<InvokeStepOptions, "onStdout" | "onStderr"> | undefined,
447
+ pollOpts?: { deadlineMs?: number },
400
448
  ): Promise<StepResult<TOutput>> {
401
- const result = await pollDurableResult<TOutput>(sandbox, runnerPid, resultToken);
449
+ const result = await pollDurableResult<TOutput>(sandbox, runnerPid, resultToken, pollOpts);
402
450
  // Runner has exited → the tee'd log file is complete. Best-effort: a failed
403
451
  // readback just means the recovered run keeps the live logs it already had.
404
452
  if (sandbox.files.read && opts?.onStdout) {
@@ -416,6 +464,38 @@ async function recoverLogsAndResult<TOutput>(
416
464
  return result;
417
465
  }
418
466
 
467
+ /** Read the foreground launch's pidfile back — the /proc liveness handle for
468
+ * the recovery poll. Returns `null` when recovery is impossible, for either
469
+ * reason: the sandbox cannot run ANY command (genuinely dead VM), or the
470
+ * pidfile never appeared. The pidfile write is the FIRST statement of the
471
+ * runner pipeline, so a still-absent pid after the retries means the runner
472
+ * never started and no result file will ever exist — recovery would poll
473
+ * blind for the full step ceiling and then misclassify; the caller must
474
+ * rethrow the original terminal error instead (fast, honest). The retries
475
+ * cover both a transiently-unreachable sandbox and the launch race where the
476
+ * stream fault fired before the VM executed the pidfile write. */
477
+ async function readRunnerPid(
478
+ sandbox: SandboxProvider,
479
+ resultToken: string,
480
+ ): Promise<number | null> {
481
+ const ATTEMPTS = 3;
482
+ const RETRY_DELAY_MS = 2_000;
483
+ for (let attempt = 1; attempt <= ATTEMPTS; attempt++) {
484
+ try {
485
+ const r = await sandbox.commands.run(
486
+ `cat ${stepPidFilePath(resultToken)} 2>/dev/null || true`,
487
+ { timeoutMs: 10_000 },
488
+ );
489
+ const pid = Number.parseInt(r.stdout.trim(), 10);
490
+ if (Number.isInteger(pid) && pid > 0) return pid;
491
+ } catch {
492
+ // Sandbox unreachable this attempt — fall through to the retry delay.
493
+ }
494
+ if (attempt < ATTEMPTS) await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
495
+ }
496
+ return null;
497
+ }
498
+
419
499
  /** Write the step's input + request-context files and build the runner env.
420
500
  * Shared by the foreground and background launch paths. Returns the
421
501
  * per-invocation `resultToken` + the env map. */
@@ -453,21 +533,53 @@ export async function invokeStep<TOutput = unknown>(
453
533
  // directly: the walk disappears entirely. Egress is edge-enforced with NO
454
534
  // root exemption (see sandbox.ts), so root is confined exactly like the
455
535
  // non-root user. `HOME=/root` so the spawned `claude` finds root's skills.
456
- result = await sandbox.commands.run(RUNNER_COMMAND, {
457
- envs: { ...envs, HOME: "/root", IS_SANDBOX: "1" },
458
- sudo: true,
459
- timeoutMs: 0,
460
- ...(splitters.onStdout ? { onStdout: splitters.onStdout } : {}),
461
- ...(splitters.onStderr ? { onStderr: splitters.onStderr } : {}),
462
- });
536
+ //
537
+ // Same pipeline pattern as `launchStep`: tee the runner's stdout to the
538
+ // durable token-keyed log file so a live-stream fault can backfill the
539
+ // undelivered tail, and write the wrapping shell's pid (`$$`) to a pidfile
540
+ // first — the shell waits on the pipeline, so `/proc/<pid>` liveness is a
541
+ // correct "runner may still write a result" proxy for the recovery poll.
542
+ // `set -o pipefail` is REQUIRED: without it the pipeline's exit code is
543
+ // tee's (0), masking a non-zero runner exit and breaking the runner-exit
544
+ // classification (the provider execs via `sh -c`, where pipefail works).
545
+ result = await sandbox.commands.run(
546
+ `set -o pipefail; echo $$ > ${stepPidFilePath(resultToken)}; ${RUNNER_COMMAND} | tee ${stepLogFilePath(resultToken)}`,
547
+ {
548
+ envs: { ...envs, HOME: "/root", IS_SANDBOX: "1" },
549
+ sudo: true,
550
+ timeoutMs: 0,
551
+ ...(splitters.onStdout ? { onStdout: splitters.onStdout } : {}),
552
+ ...(splitters.onStderr ? { onStderr: splitters.onStderr } : {}),
553
+ },
554
+ );
463
555
  } catch (e) {
464
556
  // Typed sandbox-infrastructure failure: the engine's recovery contract
465
- // (re-provision when retryable, honest terminal classification otherwise)
466
- // keys on this error propagating INTACT — its `[sandbox-unavailable:*]`
467
- // message prefix must reach the workflow as the failure leaf, not ride an
468
- // embedded stderr tail that a later slice(-2000) can drop. Rethrow before
469
- // the CommandExitError downgrade below.
470
- if (e instanceof SandboxUnavailableError) throw e;
557
+ // keys on retryability. Its `[sandbox-unavailable:*]` message prefix must
558
+ // reach the workflow as the failure leaf, not ride an embedded stderr tail
559
+ // that a later slice(-2000) can drop. Handle before the CommandExitError
560
+ // downgrade below.
561
+ if (e instanceof SandboxUnavailableError) {
562
+ // Pre-launch (retryable): nothing user-side ran — propagate INTACT so the
563
+ // workflow layer re-provisions (identity + prefix contract; pinned test).
564
+ if (e.retryable) throw e;
565
+ // Post-launch terminal: the LIVE stream/wait call died (e.g. Vercel's raw
566
+ // "The operation timed out.") while the runner is very likely still alive
567
+ // and will write its durable result file. Recover instead of failing —
568
+ // NEVER re-launch the runner. A null pid means recovery is impossible
569
+ // (sandbox can't run ANY command, or the pidfile never appeared because
570
+ // the runner never actually started): rethrow the original terminal
571
+ // classification fast rather than polling blind for the step ceiling.
572
+ // We do NOT flush the live splitter here — the recovery backfills from
573
+ // the last whole line, re-reading any in-flight partial line whole from
574
+ // the durable log (avoids a split/duplicated line).
575
+ const runnerPid = await readRunnerPid(sandbox, resultToken);
576
+ if (runnerPid === null) throw e;
577
+ opts?.onStreamDegraded?.({ error: e, runnerPid, resultToken });
578
+ return recoverLogsAndResult<TOutput>(
579
+ sandbox, runnerPid, resultToken, splitters, opts,
580
+ opts?.recoveryDeadlineMs !== undefined ? { deadlineMs: opts.recoveryDeadlineMs } : undefined,
581
+ );
582
+ }
471
583
  // Some providers (E2B) throw a CommandExitError on a non-zero exit instead of
472
584
  // returning it. Recover stdout/stderr/exitCode from the error so we can STILL
473
585
  // parse the runner's structured `__AC_STEP_RESULT__` payload — otherwise the
@@ -564,7 +676,14 @@ export async function launchStep<TOutput = unknown>(
564
676
  export async function reconnectStep<TOutput = unknown>(
565
677
  sandbox: SandboxProvider,
566
678
  resume: { runnerPid: number; resultToken: string; stepIndex: number },
567
- opts?: Pick<InvokeStepOptions, "onStdout" | "onStderr"> & { signal?: AbortSignal },
679
+ opts?: Pick<InvokeStepOptions, "onStdout" | "onStderr"> & {
680
+ signal?: AbortSignal;
681
+ /** Ceiling on the durable-result poll (default 45m — the wedged-runner
682
+ * backstop). A worker-death recovery reconnecting MID-step passes the
683
+ * step ceiling instead: hours of legitimate work may remain, and the
684
+ * activity's per-attempt startToClose already bounds it server-side. */
685
+ pollDeadlineMs?: number;
686
+ },
568
687
  ): Promise<RunningStep<TOutput>> {
569
688
  if (!sandbox.commands.connectProcess) {
570
689
  throw new Error("reconnectStep requires a provider with background-command support (commands.connectProcess)");
@@ -572,6 +691,7 @@ export async function reconnectStep<TOutput = unknown>(
572
691
  const { runnerPid, resultToken } = resume;
573
692
  const splitters = makeStreamSplitters(opts, resultToken);
574
693
  const signal = opts?.signal;
694
+ const pollDeadlineMs = opts?.pollDeadlineMs;
575
695
 
576
696
  // Re-attach to the suspended runner for LIVE stdout streaming only. This is
577
697
  // best-effort: a reconnect that throws after the native resume just means no
@@ -600,7 +720,10 @@ export async function reconnectStep<TOutput = unknown>(
600
720
  // runner's durable result file + `/proc` liveness instead — the same
601
721
  // authoritative signal the launch-path recovery uses (`pollDurableResult`).
602
722
  try {
603
- const result = await pollDurableResult<TOutput>(sandbox, runnerPid, resultToken, signal);
723
+ const result = await pollDurableResult<TOutput>(sandbox, runnerPid, resultToken, {
724
+ ...(signal ? { signal } : {}),
725
+ ...(pollDeadlineMs !== undefined ? { deadlineMs: pollDeadlineMs } : {}),
726
+ });
604
727
  splitters.flush();
605
728
  return result;
606
729
  } finally {
@@ -81,6 +81,14 @@ export function stepResultFilePath(token: string): string {
81
81
  return `/tmp/wf/step-result-${token}.json`;
82
82
  }
83
83
 
84
+ /** Sandbox-side pidfile the FOREGROUND launch writes (`echo $$ > …`) before
85
+ * spawning the runner pipeline — the wrapping shell's pid. The recovery path
86
+ * reads it back to probe `/proc/<pid>` liveness when the live stream/wait
87
+ * call dies mid-step (providers with no background-process pid — Vercel). */
88
+ export function stepPidFilePath(token: string): string {
89
+ return `/tmp/wf/step-pid-${token}.pid`;
90
+ }
91
+
84
92
  /** Sandbox-side path where the runner's stdout is tee'd as a durable LOG file,
85
93
  * keyed by the per-invocation token. The live output rides E2B's connect-web
86
94
  * command stream, which THROWS on a compressed large frame (gRPC-web cannot
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Break-glass compliance session wire types (ADR-0051).
3
+ *
4
+ * A team admin has no default access to a scoped (Private/Shared) document.
5
+ * To view one they don't hold a grant on, they open a READ-ONLY,
6
+ * owner-approved, time-boxed compliance session over a scope (one factory, or
7
+ * the whole team). These are the shapes the server's `/api/v1/compliance/*`
8
+ * routes emit — `client.ts` is a thin typed wrapper over them.
9
+ */
10
+
11
+ export type ComplianceScopeKind = "factory" | "team";
12
+ export type ComplianceStatus = "pending" | "active" | "expired" | "revoked";
13
+
14
+ /** One break-glass compliance session. `expiresAt` is null while pending —
15
+ * the TTL clock starts at approval, not request. */
16
+ export interface ComplianceSession {
17
+ id: string;
18
+ teamId: string;
19
+ requestedByUserId: string | null;
20
+ requestedByLabel: string | null;
21
+ approvedByUserId: string | null;
22
+ approvedByLabel: string | null;
23
+ reason: string;
24
+ scopeKind: ComplianceScopeKind;
25
+ /** The factory this session covers (factory scope); null for team scope. */
26
+ scopeRef: string | null;
27
+ status: ComplianceStatus;
28
+ ttlSeconds: number;
29
+ expiresAt: string | null;
30
+ revokedByUserId: string | null;
31
+ revokedByLabel: string | null;
32
+ createdAt: string;
33
+ approvedAt: string | null;
34
+ revokedAt: string | null;
35
+ }
36
+
37
+ /** One per-document access recorded under an active session — the individual
38
+ * audit trail the session's reasoned entry authorizes. */
39
+ export interface ComplianceAccess {
40
+ id: string;
41
+ sessionId: string;
42
+ /** Nulled when the accessed file is later deleted (ON DELETE SET NULL);
43
+ * `filePath` is the durable snapshot that survives. */
44
+ fileId: string | null;
45
+ filePath: string;
46
+ accessedByUserId: string | null;
47
+ accessedByLabel: string | null;
48
+ accessedAt: string;
49
+ }
50
+
51
+ export interface RequestComplianceSessionInput {
52
+ scopeKind: ComplianceScopeKind;
53
+ /** Required for factory scope; must be omitted for team scope. */
54
+ scopeRef?: string | null;
55
+ reason: string;
56
+ /** Session lifetime once approved — 15 min floor, 7 day ceiling. */
57
+ ttlSeconds: number;
58
+ }
59
+
60
+ export interface ListComplianceSessionsOptions {
61
+ status?: ComplianceStatus;
62
+ limit?: number;
63
+ cursor?: string;
64
+ }
65
+
66
+ export interface ComplianceSessionsPage {
67
+ sessions: ComplianceSession[];
68
+ nextCursor: string | null;
69
+ }
70
+
71
+ export interface ListComplianceAccessesOptions {
72
+ limit?: number;
73
+ cursor?: string;
74
+ }
75
+
76
+ export interface ComplianceAccessesPage {
77
+ accesses: ComplianceAccess[];
78
+ nextCursor: string | null;
79
+ }