chati-dev 4.5.28 → 4.5.29

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 (172) hide show
  1. package/bin/chati.js +151 -123
  2. package/framework/agents/build/dev.md +4 -4
  3. package/framework/agents/deploy/devops.md +3 -3
  4. package/framework/agents/discover/brief.md +18 -6
  5. package/framework/agents/discover/brownfield-wu.md +3 -3
  6. package/framework/agents/discover/greenfield-wu.md +4 -4
  7. package/framework/agents/plan/architect.md +2 -2
  8. package/framework/agents/plan/detail.md +4 -4
  9. package/framework/agents/plan/phases.md +3 -3
  10. package/framework/agents/plan/tasks.md +47 -19
  11. package/framework/agents/plan/ux-brand-architect.md +1 -1
  12. package/framework/agents/plan/ux.md +26 -2
  13. package/framework/agents/quality/qa-implementation.md +8 -8
  14. package/framework/agents/quality/qa-planning.md +58 -4
  15. package/framework/agents/quality/qa-visual.md +4 -4
  16. package/framework/config.yaml +2 -2
  17. package/framework/constitution.md +12 -1
  18. package/framework/context/governance.md +2 -1
  19. package/framework/context/root.md +3 -1
  20. package/framework/domains/constitution.yaml +1 -1
  21. package/framework/domains/global.yaml +1 -1
  22. package/framework/domains/workflows/standard-flow.yaml +7 -8
  23. package/framework/hooks/advance-trigger.js +3 -2
  24. package/framework/hooks/brief-validator.js +1 -1
  25. package/framework/hooks/post-dev.js +1 -1
  26. package/framework/hooks/team-quality-gate.js +9 -2
  27. package/framework/i18n/en.yaml +1 -0
  28. package/framework/i18n/es.yaml +1 -0
  29. package/framework/i18n/fr.yaml +1 -0
  30. package/framework/i18n/pt.yaml +1 -0
  31. package/framework/manifest.json +83 -83
  32. package/framework/manifest.sig +1 -1
  33. package/framework/orchestrator/chati-router.js +14 -6
  34. package/framework/orchestrator/chati.md +61 -23
  35. package/framework/rules/human-writing-style.md +7 -0
  36. package/framework/schemas/session.schema.json +25 -2
  37. package/framework/schemas/task.schema.json +26 -0
  38. package/framework/tasks/orchestrator-handoff.md +10 -10
  39. package/framework/tasks/orchestrator-resume.md +3 -3
  40. package/framework/workflows/brownfield-fullstack.yaml +6 -3
  41. package/framework/workflows/brownfield-service.yaml +5 -3
  42. package/framework/workflows/brownfield-ui.yaml +5 -3
  43. package/framework/workflows/greenfield-fullstack.yaml +6 -3
  44. package/framework/workflows/standard-flow.yaml +8 -3
  45. package/node_modules/@chati/core/native/browser-proof-wire.h +29 -0
  46. package/node_modules/@chati/core/native/darwin-browser-listener.c +36 -0
  47. package/node_modules/@chati/core/native/darwin-browser-listener.h +32 -0
  48. package/node_modules/@chati/core/native/darwin-browser-proof.h +86 -0
  49. package/node_modules/@chati/core/native/darwin-code-identity.h +35 -0
  50. package/node_modules/@chati/core/native/darwin-launch-messages.h +37 -0
  51. package/node_modules/@chati/core/native/darwin-launcher.c +312 -0
  52. package/node_modules/@chati/core/native/descriptor-fs.c +407 -0
  53. package/node_modules/@chati/core/native/execution-admission.h +59 -0
  54. package/node_modules/@chati/core/native/execution-gate.c +41 -0
  55. package/node_modules/@chati/core/native/linux-process.c +41 -0
  56. package/node_modules/@chati/core/native/prebuilds/darwin-arm64/darwin-launcher +0 -0
  57. package/node_modules/@chati/core/native/prebuilds/darwin-arm64/descriptor-fs.node +0 -0
  58. package/node_modules/@chati/core/native/prebuilds/darwin-arm64/execution-gate +0 -0
  59. package/node_modules/@chati/core/native/prebuilds/linux-x64/descriptor-fs.node +0 -0
  60. package/node_modules/@chati/core/native/prebuilds/linux-x64/execution-gate +0 -0
  61. package/node_modules/@chati/core/native/process-info-pipe.c +91 -0
  62. package/node_modules/@chati/core/package.json +2 -1
  63. package/node_modules/@chati/core/scripts/build-native.js +36 -0
  64. package/node_modules/@chati/core/src/darwin-browser-proof.js +87 -0
  65. package/node_modules/@chati/core/src/darwin-host-boot.js +58 -0
  66. package/node_modules/@chati/core/src/darwin-process.js +79 -0
  67. package/node_modules/@chati/core/src/darwin-supervisor-channels.js +82 -0
  68. package/node_modules/@chati/core/src/darwin-terminal-receipt.js +111 -0
  69. package/node_modules/@chati/core/src/descriptor-fs.js +222 -0
  70. package/node_modules/@chati/core/src/execution-gate.js +8 -0
  71. package/node_modules/@chati/core/src/host-boot-marker.js +45 -0
  72. package/node_modules/@chati/core/src/index.js +18 -0
  73. package/node_modules/@chati/core/src/linux-host-boot.js +218 -0
  74. package/node_modules/@chati/core/src/linux-process.js +133 -0
  75. package/node_modules/@chati/planning/src/browser-verification-plan.js +71 -0
  76. package/node_modules/@chati/planning/src/index.js +33 -0
  77. package/node_modules/@chati/planning/src/planning-material.js +64 -0
  78. package/node_modules/@chati/planning/src/verification-plan.js +57 -0
  79. package/node_modules/@chati/rail/package.json +1 -0
  80. package/node_modules/@chati/rail/src/execution-restoration.js +92 -0
  81. package/node_modules/@chati/rail/src/index.js +812 -167
  82. package/node_modules/@chati/rail/src/provider-processing.js +118 -0
  83. package/node_modules/@chati/rail/src/terminal-containment.js +23 -0
  84. package/node_modules/@chati/release-lane/src/index.js +40 -43
  85. package/node_modules/@chati/review-council/src/index.js +223 -0
  86. package/node_modules/@chati/tracking-clickup/src/index.js +67 -58
  87. package/package-artifact-manifest.json +1 -1
  88. package/package-artifact-manifest.sig +1 -1
  89. package/package.json +3 -2
  90. package/scripts/verify-real-harness-e2e.js +20 -8
  91. package/src/installer/core.js +147 -107
  92. package/src/installer/package-artifact.js +7 -0
  93. package/src/installer/preflight.js +13 -0
  94. package/src/installer/provider-overlay.js +1 -2
  95. package/src/installer/templates.js +58 -0
  96. package/src/installer-v2/catalog-client.js +31 -1
  97. package/src/installer-v2/clickup-connection-storage.js +92 -0
  98. package/src/installer-v2/clickup-mcp-client.js +155 -0
  99. package/src/installer-v2/clickup-oauth-loopback.js +96 -0
  100. package/src/installer-v2/clickup-oauth.js +233 -0
  101. package/src/installer-v2/index.js +141 -15
  102. package/src/installer-v2/installation-authority.js +98 -4
  103. package/src/installer-v2/provider-executable.js +117 -12
  104. package/src/installer-v2/wizard-installation.js +11 -0
  105. package/src/orchestrator/agent-selector.js +39 -1
  106. package/src/orchestrator/browser-artifact-store.js +131 -0
  107. package/src/orchestrator/cli.js +762 -205
  108. package/src/orchestrator/clickup-mcp-observer.js +226 -0
  109. package/src/orchestrator/clickup-runtime.js +45 -24
  110. package/src/orchestrator/council-gate-runtime.js +903 -0
  111. package/src/orchestrator/evidence-authority-enrollment.js +75 -0
  112. package/src/orchestrator/evidence-authority-request.js +46 -0
  113. package/src/orchestrator/handoff-engine.js +4 -11
  114. package/src/orchestrator/pipeline-manager.js +42 -4
  115. package/src/orchestrator/planning-contract-revision.js +711 -0
  116. package/src/orchestrator/planning-reconciliation.js +302 -0
  117. package/src/orchestrator/planning-runtime.js +476 -29
  118. package/src/orchestrator/rail-adjudication-evidence.js +15 -24
  119. package/src/orchestrator/rail-human-effort-authority.js +46 -0
  120. package/src/orchestrator/rail-human-effort.js +149 -0
  121. package/src/orchestrator/rail-runtime.js +291 -47
  122. package/src/orchestrator/rail-verification.js +278 -0
  123. package/src/orchestrator/review-runtime.js +37 -2
  124. package/src/orchestrator/runtime-installation-v2.js +143 -24
  125. package/src/orchestrator/session-manager.js +1457 -29
  126. package/src/tasks/handoff.js +57 -33
  127. package/src/terminal/adapters/claude-adapter.js +3 -0
  128. package/src/terminal/adapters/codex-adapter.js +13 -0
  129. package/src/terminal/adapters/grok-adapter.js +1 -0
  130. package/src/terminal/agent-attempt-budget.js +35 -0
  131. package/src/terminal/browser-endpoint-proxy.js +174 -0
  132. package/src/terminal/browser-observer-driver.js +94 -0
  133. package/src/terminal/browser-observer-supervisor.js +73 -0
  134. package/src/terminal/browser-observer.js +95 -0
  135. package/src/terminal/browser-process-facts.js +75 -0
  136. package/src/terminal/browser-runtime.js +131 -0
  137. package/src/terminal/darwin-authority-policy.js +213 -0
  138. package/src/terminal/darwin-browser-application.js +116 -0
  139. package/src/terminal/darwin-browser-observer.js +57 -0
  140. package/src/terminal/darwin-execution.js +185 -0
  141. package/src/terminal/executable-snapshot.js +202 -0
  142. package/src/terminal/handoff-parser.js +48 -2
  143. package/src/terminal/isolation.js +14 -13
  144. package/src/terminal/prompt-builder.js +176 -15
  145. package/src/terminal/provider-preflight.js +47 -27
  146. package/src/terminal/rail-execution-worktree.js +22 -6
  147. package/src/terminal/rail-process-observer.js +10 -0
  148. package/src/terminal/rail-prompts.js +21 -3
  149. package/src/terminal/rail-readonly-workspace.js +54 -5
  150. package/src/terminal/rail-worktree-checkpoint.js +315 -0
  151. package/src/terminal/rail-worktree-restoration.js +485 -0
  152. package/src/terminal/run-agent.js +164 -42
  153. package/src/terminal/run-council-gate.js +338 -0
  154. package/src/terminal/run-planning-reconciliation.js +49 -0
  155. package/src/terminal/run-rail-adjudication.js +29 -7
  156. package/src/terminal/run-rail-recovery.js +97 -0
  157. package/src/terminal/run-rail-review.js +36 -15
  158. package/src/terminal/run-rail-rework.js +26 -15
  159. package/src/terminal/run-rail-task.js +22 -9
  160. package/src/terminal/runtime-native-helper.js +85 -0
  161. package/src/terminal/spawner.js +899 -142
  162. package/src/terminal/verification-process-facts.js +49 -0
  163. package/src/upgrade/entrypoints.js +68 -0
  164. package/src/upgrade/generation.js +194 -0
  165. package/src/upgrade/maintenance.js +79 -0
  166. package/src/upgrade/paths.js +81 -0
  167. package/src/upgrade/upgrade.js +267 -0
  168. package/src/utils/execution-role.js +15 -0
  169. package/src/utils/handoff-paths.js +59 -0
  170. package/src/wizard/i18n.js +1 -0
  171. package/src/wizard/index.js +17 -5
  172. package/src/wizard/questions.js +24 -12
@@ -8,21 +8,30 @@
8
8
  */
9
9
 
10
10
  import { spawn, spawnSync } from 'child_process';
11
- import { createHash } from 'node:crypto';
11
+ import { createHash, randomUUID } from 'node:crypto';
12
12
  import { EventEmitter } from 'node:events';
13
13
  import {
14
- closeSync, constants, fstatSync, fsyncSync, lstatSync, mkdirSync, mkdtempSync, openSync,
14
+ closeSync, constants, existsSync, fstatSync, fsyncSync, lstatSync, mkdirSync, mkdtempSync, openSync,
15
15
  readFileSync, readSync, realpathSync, rmSync, writeSync,
16
16
  } from 'node:fs';
17
17
  import { homedir, hostname, tmpdir } from 'node:os';
18
18
  import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
19
- import { canonicalize, sha256 } from '@chati/core';
19
+ import { canonicalize, sha256, directoryPathSync, openAtSync, mkdirAtSync, lstatAtSync, nativeExecutionGatePath,
20
+ createLinuxProcessInfoPipe, readLinuxProcessInfoPipe, captureLinuxNamespaceIdentity, observeLinuxNamespace } from '@chati/core';
21
+ import { captureDarwinHostBootIdentity, createDarwinTerminalLaunchBinding, assertDarwinTerminalLaunch, assertDarwinTerminalCompletion } from '@chati/core';
20
22
  import { validateWriteScopes, buildIsolationEnv } from './isolation.js';
21
23
  import { getProvider } from './cli-registry.js';
22
24
  import { getRateLimiter } from './rate-limiter.js';
23
25
  import { recordModelSelection } from '../orchestrator/session-manager.js';
26
+ import {
27
+ assertDarwinSealedLaunch, disposeDarwinSealedLaunch, prepareDarwinSealedLaunch, prepareDarwinAuthorityLaunch,
28
+ prepareDarwinBrowserObserverLaunch,
29
+ } from './darwin-execution.js';
30
+ import { PROVIDER_CONTROL_PLANE_DIRECTORIES } from './darwin-authority-policy.js';
31
+ import { createDarwinVerificationBinding, projectVerificationExecutableBinding, verificationProcessCompleted } from './verification-process-facts.js';
24
32
  import {
25
33
  digestProviderExecutable, openPinnedProviderExecutable, openPinnedProviderSupportingExecutables,
34
+ sealedProviderRuntimeEnvironment, validateProviderExecutableBinding,
26
35
  } from '../installer-v2/provider-executable.js';
27
36
  import {
28
37
  installationAuthorityStateDirectory, signHostAuthorityPayload, verifyHostAuthorityPayload,
@@ -59,17 +68,18 @@ const OVERLOAD_PATTERNS = [/529/, /overloaded/i, /capacity/i, /too many requests
59
68
 
60
69
  let _counter = 0;
61
70
  const TERMINAL_AUTHORITY_STATE = new WeakMap();
71
+ // Diagnostic observations are not receipts, cleanup proof or execution authority.
72
+ const TERMINAL_DIAGNOSTIC_OBSERVATIONS = new WeakMap();
73
+ const TERMINAL_ATTEMPT_DIAGNOSTICS = new WeakMap();
62
74
  const TERMINAL_AUTHORITY_DOMAIN = 'chati-terminal-receipt-v1';
63
75
  const TERMINAL_SECURITY_TEST_HOOKS = Symbol.for('chati.terminal.security-test-hooks');
64
76
  const READ_ONLY_PROVIDERS = new Set(['claude', 'codex', 'grok']);
65
77
  const BUBBLEWRAP_PATH = '/usr/bin/bwrap';
66
- const PROVIDER_CONTROL_PLANE_DIRECTORIES = Object.freeze([
67
- '.chati', '.chati.dev', '.claude', '.codex', '.grok', '.agents',
68
- ]);
69
78
  const PROCESS_TREE_SETTLE_WAIT = new Int32Array(new SharedArrayBuffer(4));
70
79
  const PRIVATE_PROMPT_MARKER = '<chati-private-prompt-file>';
71
80
  const CONTAINED_PROMPT_PATH = '/run/user/chati-private-prompt';
72
81
  const CONTAINED_PROVIDER_EXECUTABLE_PATH = '/run/user/chati-provider-executable';
82
+ const CONTAINED_EXECUTION_GATE_PATH = '/run/user/chati-execution-gate';
73
83
  const AUTHORITY_SYSTEM_PATH = '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin';
74
84
  const TEAM_CONTEXT_ENV_KEYS = Object.freeze([
75
85
  'CHATI_TEAM_ID', 'CHATI_TEAM_MEMBER', 'CHATI_TEAM_TASK_LIST', 'CHATI_TEAM_MAILBOX',
@@ -80,6 +90,40 @@ function runTerminalSecurityTestHook(name, payload) {
80
90
  if (hooks && typeof hooks[name] === 'function') hooks[name](payload);
81
91
  }
82
92
 
93
+ function terminalAttemptSnapshot(handle, attempt) {
94
+ const observation = TERMINAL_DIAGNOSTIC_OBSERVATIONS.get(handle);
95
+ if (!observation) return null;
96
+ return Object.freeze({
97
+ attempt,
98
+ timeout_ms: Number.isSafeInteger(observation.timeout) && observation.timeout > 0 ? observation.timeout : null,
99
+ elapsed_ms: Number.isFinite(observation.elapsedMs) && observation.elapsedMs >= 0 ? Math.floor(observation.elapsedMs) : null,
100
+ exit_code: Number.isInteger(observation.exitCode) ? observation.exitCode : null,
101
+ timed_out: observation.timedOut === true,
102
+ termination_cause: observation.terminationCause,
103
+ });
104
+ }
105
+
106
+ function projectAttemptDiagnostics(attempts, exceptionStage = null) {
107
+ return Object.freeze({ schema_version: 1,
108
+ attempts: Object.freeze(attempts.map(row => Object.freeze({ ...row }))),
109
+ exception_stage: exceptionStage });
110
+ }
111
+
112
+ function rememberAttemptDiagnostics(target, attempts, exceptionStage = null) {
113
+ if (target !== null && (typeof target === 'object' || typeof target === 'function')) {
114
+ TERMINAL_ATTEMPT_DIAGNOSTICS.set(target, projectAttemptDiagnostics(attempts, exceptionStage));
115
+ }
116
+ return target;
117
+ }
118
+
119
+ /** Closed, content-free observations only. Reading this never signs, waits or kills. */
120
+ export function getTerminalAttemptDiagnostics(handleOrError) {
121
+ const remembered = TERMINAL_ATTEMPT_DIAGNOSTICS.get(handleOrError);
122
+ if (remembered) return projectAttemptDiagnostics(remembered.attempts, remembered.exception_stage);
123
+ const observation = terminalAttemptSnapshot(handleOrError, 0);
124
+ return observation ? projectAttemptDiagnostics([observation]) : null;
125
+ }
126
+
83
127
  function openPrivatePromptFile(prompt) {
84
128
  const directory = mkdtempSync(join(tmpdir(), 'chati-terminal-prompt-'));
85
129
  const path = join(directory, 'prompt');
@@ -189,6 +233,24 @@ function digestPinnedExecutable(descriptor, expected = null) {
189
233
  return Object.freeze({ digest, snapshot: after });
190
234
  }
191
235
 
236
+ // Internal descriptor boundary, exported for deterministic race regressions on
237
+ // disposable images. Runtime callers cannot select a different helper path.
238
+ export function _pinNativeExecutionGateDescriptor(descriptor) {
239
+ const stat = fstatSync(descriptor);
240
+ const magic = Buffer.alloc(4); readSync(descriptor, magic, 0, 4, 0);
241
+ if (!stat.isFile() || stat.nlink !== 1 || (stat.mode & 0o022) !== 0
242
+ || (stat.mode & 0o111) === 0 || ![0, process.geteuid()].includes(stat.uid)
243
+ || magic.toString('hex') !== '7f454c46') {
244
+ return failContainment('execution admission requires a private pinned native helper');
245
+ }
246
+ runTerminalSecurityTestHook('afterExecutionGateMagicValidation', { descriptor });
247
+ const executable = digestPinnedExecutable(descriptor);
248
+ if (!sameExecutableSnapshot(executableSnapshot(stat), executable.snapshot)) {
249
+ return failContainment('execution admission helper changed between format validation and hashing');
250
+ }
251
+ return executable;
252
+ }
253
+
192
254
  function currentBubblewrapTrust(descriptor) {
193
255
  const trustedDirectories = ['/', '/usr', '/usr/bin'].map((path) => ({ path, stat: lstatSync(path) }));
194
256
  const pathStat = lstatSync(BUBBLEWRAP_PATH);
@@ -258,8 +320,8 @@ function closeDirectoryChain(chain) {
258
320
  }
259
321
 
260
322
  function openPinnedDirectoryChain(path) {
261
- if (process.platform !== 'linux') {
262
- return failContainment('authority-bearing RAIL processes require Linux descriptor-pinned project storage');
323
+ if (!['linux', 'darwin'].includes(process.platform)) {
324
+ return failContainment(`authority-bearing RAIL processes require descriptor-pinned project storage; unsupported platform: ${process.platform}`);
263
325
  }
264
326
  const absolute = resolve(path);
265
327
  const descriptors = [];
@@ -270,18 +332,18 @@ function openPinnedDirectoryChain(path) {
270
332
  descriptors.push(descriptor);
271
333
  snapshots.push(directorySnapshot(fstatSync(descriptor)));
272
334
  for (const segment of absolute.slice(1).split(sep).filter(Boolean)) {
273
- descriptor = openSync(
274
- join(`/proc/self/fd/${descriptor}`, segment),
335
+ descriptor = openAtSync(
336
+ descriptor, segment,
275
337
  constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0),
276
338
  );
339
+ descriptors.push(descriptor);
277
340
  const stat = fstatSync(descriptor);
278
341
  if (!stat.isDirectory()) return failContainment('authority project chain must contain only real directories');
279
- descriptors.push(descriptor);
280
342
  snapshots.push(directorySnapshot(stat));
281
343
  }
282
344
  return Object.freeze({
283
345
  path: absolute,
284
- reference: `/proc/self/fd/${descriptors.at(-1)}`,
346
+ reference: process.platform === 'linux' ? `/proc/self/fd/${descriptors.at(-1)}` : directoryPathSync(descriptors.at(-1)),
285
347
  descriptors: Object.freeze(descriptors),
286
348
  snapshots: Object.freeze(snapshots),
287
349
  identity_digest: sha256({ path: absolute, snapshots }),
@@ -312,18 +374,17 @@ function assertPinnedDirectoryChain(chain) {
312
374
  function ensureScopedDirectory(projectChain, relativeDirectory) {
313
375
  const descriptors = [];
314
376
  const snapshots = [];
315
- let reference = projectChain.reference;
377
+ let parentDescriptor = projectChain.descriptors.at(-1);
316
378
  try {
317
379
  for (const segment of relativeDirectory.split(sep).filter(Boolean)) {
318
- const target = join(reference, segment);
319
- try { mkdirSync(target, { mode: 0o700 }); }
380
+ try { mkdirAtSync(parentDescriptor, segment, { mode: 0o700 }); }
320
381
  catch (error) { if (error?.code !== 'EEXIST') throw error; }
321
- const pathStat = lstatSync(target);
382
+ const pathStat = lstatAtSync(parentDescriptor, segment);
322
383
  if (pathStat.isSymbolicLink() || !pathStat.isDirectory()) {
323
384
  return failContainment('Planning write scope ancestors must be real directories');
324
385
  }
325
- const descriptor = openSync(
326
- target,
386
+ const descriptor = openAtSync(
387
+ parentDescriptor, segment,
327
388
  constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0),
328
389
  );
329
390
  const pinned = fstatSync(descriptor);
@@ -333,9 +394,9 @@ function ensureScopedDirectory(projectChain, relativeDirectory) {
333
394
  }
334
395
  descriptors.push(descriptor);
335
396
  snapshots.push(directorySnapshot(pinned));
336
- reference = `/proc/self/fd/${descriptor}`;
397
+ parentDescriptor = descriptor;
337
398
  }
338
- return Object.freeze({ descriptors, snapshots, reference });
399
+ return Object.freeze({ descriptors, snapshots, directoryDescriptor: parentDescriptor });
339
400
  } catch (error) {
340
401
  descriptors.reverse().forEach((descriptor) => { try { closeSync(descriptor); } catch { /* already closed */ } });
341
402
  if (error?.code === 'TERMINAL_CONTAINMENT_UNAVAILABLE') throw error;
@@ -376,9 +437,8 @@ function preparePinnedWriteScopes(projectChain, writeScope = []) {
376
437
  targetSnapshot = parent.snapshots.at(-1);
377
438
  } else {
378
439
  const name = rel.split(sep).at(-1);
379
- const target = join(parent.reference, name);
380
- const pathFlags = constants.O_RDWR | constants.O_CREAT | (constants.O_NOFOLLOW || 0);
381
- targetDescriptor = openSync(target, pathFlags, 0o600);
440
+ const pathFlags = constants.O_RDWR | constants.O_CREAT | (constants.O_NOFOLLOW || 0) | (constants.O_NONBLOCK || 0);
441
+ targetDescriptor = openAtSync(parent.directoryDescriptor, name, pathFlags, 0o600);
382
442
  const stat = fstatSync(targetDescriptor);
383
443
  if (!stat.isFile() || stat.nlink !== 1) {
384
444
  closeSync(targetDescriptor);
@@ -444,31 +504,134 @@ function hiddenInstallationAuthorityArgs() {
444
504
  }
445
505
  }
446
506
 
507
+ function buildDarwinAuthorityContainment({
508
+ args, cwd, readOnly, writeScope, terminalId, provider, providerExecutableBinding,
509
+ promptRecord, maskedReadPaths, councilToolIsolation, authorityContext,
510
+ verificationIsolation = false,
511
+ }) {
512
+ if (providerExecutableBinding === undefined) {
513
+ throw Object.assign(new Error('authority-bearing provider launch requires an executable binding sealed by the installation'), { code: 'PROVIDER_EXECUTABLE_BINDING_REQUIRED' });
514
+ }
515
+ const binding = validateProviderExecutableBinding(providerExecutableBinding, { harnessId: provider });
516
+ const project = realpathSync(cwd);
517
+ let projectChain;
518
+ let stateChain;
519
+ let pinnedWriteScopes;
520
+ let darwinLaunch;
521
+ const maskedChains = [];
522
+ const controlPlaneChains = [];
523
+ try {
524
+ projectChain = openPinnedDirectoryChain(project);
525
+ if (!Array.isArray(maskedReadPaths)) return failContainment('masked read paths must be an array');
526
+ for (const requested of [...new Set(maskedReadPaths)]) {
527
+ const physical = realpathSync(requested);
528
+ if (pathContains(physical, project) || pathContains(project, physical)) {
529
+ return failContainment('masked read path must not overlap the authority working directory');
530
+ }
531
+ maskedChains.push(openPinnedDirectoryChain(physical));
532
+ }
533
+ for (const name of PROVIDER_CONTROL_PLANE_DIRECTORIES) {
534
+ let stat;
535
+ try { stat = lstatAtSync(projectChain.descriptors.at(-1), name); }
536
+ catch (error) { if (error.code === 'ENOENT') continue; throw error; }
537
+ if (stat.isSymbolicLink() || !stat.isDirectory()) return failContainment(`${name} control plane must be a real directory`);
538
+ controlPlaneChains.push(openPinnedDirectoryChain(join(project, name)));
539
+ }
540
+ pinnedWriteScopes = preparePinnedWriteScopes(projectChain, readOnly ? [] : writeScope);
541
+ let stateDirectory;
542
+ if (!verificationIsolation) {
543
+ stateDirectory = join(realpathSync(homedir()), provider === 'claude' ? '.claude' : provider === 'codex' ? '.codex' : '.grok');
544
+ try { mkdirSync(stateDirectory, { mode: 0o700 }); }
545
+ catch (error) { if (error.code !== 'EEXIST') throw error; }
546
+ // Descriptor traversal rejects a provider state symlink, including ancestors.
547
+ stateChain = openPinnedDirectoryChain(stateDirectory);
548
+ }
549
+ darwinLaunch = prepareDarwinAuthorityLaunch({
550
+ binding, harnessId: provider, projectDir: project, args, hasPrompt: Boolean(promptRecord),
551
+ promptDirectory: promptRecord?.directory, authorityContext, stateDirectory,
552
+ writeScopes: pinnedWriteScopes, maskedReadPaths: maskedChains.map(chain => chain.path), councilToolIsolation, verificationIsolation,
553
+ subjectReadOnly: verificationIsolation && readOnly === true,
554
+ });
555
+ const containment = Object.freeze({
556
+ kind: darwinLaunch.kind, id: `darwin:${terminalId}`,
557
+ command: darwinLaunch.command, args: darwinLaunch.args, darwinLaunch,
558
+ pinnedProjectChain: projectChain, pinnedStateChain: stateChain,
559
+ controlPlaneChains: Object.freeze(controlPlaneChains), maskedReadChains: Object.freeze(maskedChains),
560
+ pinnedWriteScopes, projectReference: projectChain.reference, projectIdentityDigest: projectChain.identity_digest,
561
+ providerExecutableBinding: binding,
562
+ digest: sha256({
563
+ kind: darwinLaunch.kind, helper_sha256: darwinLaunch.helperSha256, policy_digest: darwinLaunch.policyDigest,
564
+ provider_executable: binding, native_nonce: darwinLaunch.supervisorChannels.nonce,
565
+ project_identity_digest: projectChain.identity_digest,
566
+ state_identity_digest: stateChain?.identity_digest ?? null,
567
+ masked_identity_digests: maskedChains.map(chain => chain.identity_digest),
568
+ }),
569
+ });
570
+ assertAuthorityContainmentStable(containment);
571
+ return containment;
572
+ } catch (error) {
573
+ closeAuthorityContainment({ pinnedProjectChain: projectChain, pinnedStateChain: stateChain,
574
+ pinnedWriteScopes, controlPlaneChains, maskedReadChains: maskedChains });
575
+ if (darwinLaunch) try { disposeDarwinSealedLaunch(darwinLaunch); } catch (cleanupError) { error.darwinCleanupError = cleanupError; }
576
+ throw error;
577
+ }
578
+ }
579
+
447
580
  function buildAuthorityContainment({
448
581
  args, cwd, readOnly, writeScope, terminalId, provider, providerExecutableBinding,
449
- promptRecord = null, maskedReadPaths = [],
582
+ promptRecord = null, maskedReadPaths = [], councilToolIsolation = false, verificationIsolation = false,
583
+ observerDirectory = null,
584
+ executionAdmission = false,
585
+ authorityContext,
450
586
  }) {
587
+ if (process.platform === 'darwin') {
588
+ if (observerDirectory !== null) return failContainment('Darwin browser verification isolation is not implemented');
589
+ return buildDarwinAuthorityContainment({ args, cwd, readOnly, writeScope, terminalId, provider,
590
+ providerExecutableBinding, promptRecord, maskedReadPaths, councilToolIsolation, authorityContext, verificationIsolation });
591
+ }
451
592
  const trustedBubblewrap = requireBubblewrapContainment();
452
593
  if (providerExecutableBinding === undefined) {
453
594
  closeSync(trustedBubblewrap.descriptor);
454
595
  throw Object.assign(new Error('authority-bearing provider launch requires an executable binding sealed by the installation'), { code: 'PROVIDER_EXECUTABLE_BINDING_REQUIRED' });
455
596
  }
456
597
  let projectChain;
598
+ let observerChain;
457
599
  const maskedChains = [];
458
600
  let trustedProvider;
459
601
  let trustedSupportingProviders = [];
602
+ let admissionHelper;
460
603
  let pinnedWriteScopes;
461
- const stateDirectory = join(homedir(), provider === 'claude' ? '.claude' : provider === 'codex' ? '.codex' : '.grok');
604
+ const stateDirectory = verificationIsolation ? '/run/user/chati-verification-home'
605
+ : join(homedir(), provider === 'claude' ? '.claude' : provider === 'codex' ? '.codex' : '.grok');
462
606
  let stateDirectoryArgs;
463
607
  let stateDirectoryDescriptor;
464
608
  const controlPlaneDescriptors = [];
465
609
  const controlPlaneArgs = [];
466
610
  try {
467
- trustedProvider = openPinnedProviderExecutable(providerExecutableBinding, { harnessId: provider, projectDir: cwd });
611
+ if (executionAdmission) {
612
+ const descriptor = openSync(nativeExecutionGatePath(), constants.O_RDONLY | constants.O_NOFOLLOW);
613
+ admissionHelper = { descriptor };
614
+ admissionHelper.executable = _pinNativeExecutionGateDescriptor(descriptor);
615
+ }
616
+ trustedProvider = openPinnedProviderExecutable(providerExecutableBinding, {
617
+ harnessId: provider, projectDir: cwd, requireNativeImage: verificationIsolation,
618
+ });
468
619
  trustedSupportingProviders = openPinnedProviderSupportingExecutables(
469
620
  providerExecutableBinding, { harnessId: provider, projectDir: cwd },
470
621
  );
471
622
  projectChain = openPinnedDirectoryChain(cwd);
623
+ if (observerDirectory !== null) {
624
+ if (!verificationIsolation) return failContainment('observer tools require verification isolation');
625
+ const physical = realpathSync(observerDirectory);
626
+ const stat = lstatSync(physical);
627
+ if (pathContains(physical, cwd) || pathContains(cwd, physical)
628
+ || !stat.isDirectory() || stat.uid !== process.geteuid() || (stat.mode & 0o777) !== 0o700
629
+ || pathContains(physical, installationAuthorityStateDirectory())
630
+ || pathContains(installationAuthorityStateDirectory(), physical)) {
631
+ return failContainment('observer tools must be a separate private directory without authority keys');
632
+ }
633
+ observerChain = openPinnedDirectoryChain(physical);
634
+ }
472
635
  if (!Array.isArray(maskedReadPaths)) return failContainment('masked read paths must be an array');
473
636
  for (const requested of [...new Set(maskedReadPaths.map((entry) => resolve(String(entry))))]) {
474
637
  const physical = realpathSync(requested);
@@ -479,22 +642,27 @@ function buildAuthorityContainment({
479
642
  }
480
643
  pinnedWriteScopes = preparePinnedWriteScopes(projectChain, readOnly ? [] : writeScope);
481
644
  let stateStat;
482
- try { stateStat = lstatSync(stateDirectory); }
483
- catch (error) {
484
- if (error?.code !== 'ENOENT') throw error;
485
- mkdirSync(stateDirectory, { mode: 0o700 });
486
- stateStat = lstatSync(stateDirectory);
487
- }
488
- if (!stateStat.isDirectory()) {
489
- throw Object.assign(new Error('provider state path must be a real directory'), { code: 'TERMINAL_CONTAINMENT_UNAVAILABLE' });
490
- }
491
- stateDirectoryDescriptor = openSync(stateDirectory, constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0));
492
- const descriptorStat = fstatSync(stateDirectoryDescriptor);
493
- if (descriptorStat.dev !== stateStat.dev || descriptorStat.ino !== stateStat.ino) {
494
- throw Object.assign(new Error('provider state directory changed before containment'), { code: 'TERMINAL_CONTAINMENT_UNAVAILABLE' });
645
+ if (verificationIsolation) {
646
+ // Retain descriptor numbering without exposing any host provider state.
647
+ stateDirectoryDescriptor = openSync('/dev/null', constants.O_RDONLY);
648
+ } else {
649
+ try { stateStat = lstatSync(stateDirectory); }
650
+ catch (error) {
651
+ if (error?.code !== 'ENOENT') throw error;
652
+ mkdirSync(stateDirectory, { mode: 0o700 });
653
+ stateStat = lstatSync(stateDirectory);
654
+ }
655
+ if (!stateStat.isDirectory()) {
656
+ throw Object.assign(new Error('provider state path must be a real directory'), { code: 'TERMINAL_CONTAINMENT_UNAVAILABLE' });
657
+ }
658
+ stateDirectoryDescriptor = openSync(stateDirectory, constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0));
659
+ const descriptorStat = fstatSync(stateDirectoryDescriptor);
660
+ if (descriptorStat.dev !== stateStat.dev || descriptorStat.ino !== stateStat.ino) {
661
+ throw Object.assign(new Error('provider state directory changed before containment'), { code: 'TERMINAL_CONTAINMENT_UNAVAILABLE' });
662
+ }
495
663
  }
496
- const physicalProject = realpathSync(projectChain.reference);
497
- const physicalState = realpathSync(stateDirectory);
664
+ const physicalProject = directoryPathSync(projectChain.descriptors.at(-1));
665
+ const physicalState = verificationIsolation ? stateDirectory : realpathSync(stateDirectory);
498
666
  if (pathContains(physicalProject, physicalState) || pathContains(physicalState, physicalProject)) {
499
667
  throw Object.assign(new Error('provider state and project directories must not physically overlap inside authority containment'), { code: 'TERMINAL_CONTAINMENT_UNAVAILABLE' });
500
668
  }
@@ -515,23 +683,22 @@ function buildAuthorityContainment({
515
683
  throw Object.assign(new Error('masked read path overlaps provider state or executable authority'), { code: 'TERMINAL_CONTAINMENT_UNAVAILABLE' });
516
684
  }
517
685
  }
518
- stateDirectoryArgs = ['--bind-fd', '4', stateDirectory];
686
+ stateDirectoryArgs = verificationIsolation ? ['--dir', stateDirectory] : ['--bind-fd', '4', stateDirectory];
519
687
  // Provider-specific configuration is control-plane material too. A RAIL
520
688
  // execution may write its scoped product files, but cannot rewrite a
521
689
  // harness hook, skill, rule or config and then rely on that change in the
522
690
  // same contained process.
523
691
  for (const name of PROVIDER_CONTROL_PLANE_DIRECTORIES) {
524
- const target = join(projectChain.reference, name);
525
692
  let targetStat;
526
- try { targetStat = lstatSync(target); }
693
+ try { targetStat = lstatAtSync(projectChain.descriptors.at(-1), name); }
527
694
  catch (error) { if (error?.code === 'ENOENT') continue; throw error; }
528
695
  if (targetStat.isSymbolicLink() || !targetStat.isDirectory()) {
529
696
  throw Object.assign(new Error(`${name} control plane must be a real directory`), { code: 'TERMINAL_CONTAINMENT_UNAVAILABLE' });
530
697
  }
531
- const descriptor = openSync(target, constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0));
698
+ const descriptor = openAtSync(projectChain.descriptors.at(-1), name, constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0));
532
699
  const pinned = fstatSync(descriptor);
533
700
  if (pinned.dev !== targetStat.dev || pinned.ino !== targetStat.ino
534
- || !pathContains(physicalProject, realpathSync(target))) {
701
+ || !pathContains(physicalProject, directoryPathSync(descriptor))) {
535
702
  closeSync(descriptor);
536
703
  throw Object.assign(new Error(`${name} control plane changed before containment`), { code: 'TERMINAL_CONTAINMENT_UNAVAILABLE' });
537
704
  }
@@ -540,6 +707,7 @@ function buildAuthorityContainment({
540
707
  controlPlaneArgs.push('--ro-bind-fd', String(descriptorNumber), join(cwd, name));
541
708
  }
542
709
  } catch (error) {
710
+ if (admissionHelper) closeSync(admissionHelper.descriptor);
543
711
  controlPlaneDescriptors.forEach((descriptor) => closeSync(descriptor));
544
712
  if (stateDirectoryDescriptor !== undefined) closeSync(stateDirectoryDescriptor);
545
713
  if (trustedProvider?.descriptor !== undefined) closeSync(trustedProvider.descriptor);
@@ -547,6 +715,7 @@ function buildAuthorityContainment({
547
715
  for (const descriptor of pinnedWriteScopes?.descriptors ?? []) try { closeSync(descriptor); } catch { /* already closed */ }
548
716
  for (const chain of maskedChains) closeDirectoryChain(chain);
549
717
  closeDirectoryChain(projectChain);
718
+ closeDirectoryChain(observerChain);
550
719
  closeSync(trustedBubblewrap.descriptor);
551
720
  if (error?.code === 'TERMINAL_CONTAINMENT_UNAVAILABLE' || error?.code?.startsWith('PROVIDER_EXECUTABLE_')) throw error;
552
721
  throw Object.assign(new Error(`cannot prepare provider state directory: ${error.message}`), { code: 'TERMINAL_CONTAINMENT_UNAVAILABLE' });
@@ -558,6 +727,9 @@ function buildAuthorityContainment({
558
727
  const promptDescriptorNumber = 6 + controlPlaneDescriptors.length + writableScopeDescriptors.length;
559
728
  const providerDescriptorNumber = promptDescriptorNumber + (promptRecord ? 1 : 0);
560
729
  const supportingProviderDescriptorNumber = providerDescriptorNumber + 1;
730
+ const observerDescriptorNumber = supportingProviderDescriptorNumber + trustedSupportingProviders.length;
731
+ const admissionHelperDescriptorNumber = observerDescriptorNumber + (observerChain ? 1 : 0);
732
+ const admissionControlDescriptorNumber = admissionHelperDescriptorNumber + 1;
561
733
  const supportingProviderArgs = trustedSupportingProviders.flatMap((support, index) => [
562
734
  '--ro-bind-fd', String(supportingProviderDescriptorNumber + index),
563
735
  join(dirname(CONTAINED_PROVIDER_EXECUTABLE_PATH), basename(support.binding.executable_path)),
@@ -565,15 +737,40 @@ function buildAuthorityContainment({
565
737
  const maskedPathArgs = maskedChains
566
738
  .filter((chain) => !pathContains('/tmp', chain.path))
567
739
  .flatMap((chain) => ['--tmpfs', chain.path]);
740
+ // Council artifacts are untrusted input. The review CLIs receive every byte
741
+ // in their private prompt and must not need a shell, host files, MCPs or web
742
+ // tools to inspect them. Hide the host home, root account and command trees
743
+ // structurally, while re-binding only the selected provider state needed by
744
+ // the CLI transport itself. This remains defense in depth beside the
745
+ // provider-specific no-tools modes below.
746
+ const councilHomeIsolationArgs = stateDirectory.startsWith('/root/')
747
+ ? ['--tmpfs', '/root', '--dir', stateDirectory]
748
+ : ['--tmpfs', '/home', '--dir', dirname(stateDirectory), '--dir', stateDirectory, '--tmpfs', '/root'];
749
+ const councilToolIsolationArgs = councilToolIsolation
750
+ ? [
751
+ ...councilHomeIsolationArgs,
752
+ '--tmpfs', '/usr/bin', '--tmpfs', '/usr/sbin',
753
+ '--tmpfs', '/usr/local/bin', '--tmpfs', '/usr/local/sbin',
754
+ ]
755
+ : [];
568
756
  const containmentArgs = [
569
757
  '--die-with-parent', '--unshare-pid', '--unshare-ipc', '--unshare-cgroup-try',
570
- '--ro-bind', '/', '/',
758
+ ...(verificationIsolation ? [
759
+ '--unshare-net', '--unshare-uts',
760
+ // Checks run untrusted project code: expose runtime system files only,
761
+ // not a read-only view of the operator's files, credentials or brain.
762
+ ...['/usr', '/bin', '/sbin', '/lib', '/lib64'].filter((path) => existsSync(path))
763
+ .flatMap((path) => ['--ro-bind', path, path]),
764
+ '--dir', '/etc',
765
+ ...(existsSync('/etc/ld.so.cache') ? ['--ro-bind', '/etc/ld.so.cache', '/etc/ld.so.cache'] : []),
766
+ ] : ['--ro-bind', '/', '/']),
571
767
  '--dev', '/dev', '--proc', '/proc',
768
+ ...councilToolIsolationArgs,
572
769
  '--tmpfs', '/run/user',
573
770
  '--tmpfs', '/tmp',
574
- ...readOnlyTmpPathEntries(),
771
+ ...(verificationIsolation ? [] : readOnlyTmpPathEntries()),
575
772
  ...maskedPathArgs,
576
- ...hiddenInstallationAuthorityArgs(),
773
+ ...(verificationIsolation ? [] : hiddenInstallationAuthorityArgs()),
577
774
  ...stateDirectoryArgs,
578
775
  pinnedWriteScopes.fullProject ? '--bind-fd' : '--ro-bind-fd', '5', cwd,
579
776
  ...controlPlaneArgs,
@@ -581,8 +778,12 @@ function buildAuthorityContainment({
581
778
  ...(promptRecord ? ['--ro-bind-fd', String(promptDescriptorNumber), CONTAINED_PROMPT_PATH] : []),
582
779
  '--ro-bind-fd', String(providerDescriptorNumber), CONTAINED_PROVIDER_EXECUTABLE_PATH,
583
780
  ...supportingProviderArgs,
781
+ ...(observerChain ? ['--ro-bind-fd', String(observerDescriptorNumber), '/run/chati-observer-tools'] : []),
782
+ ...(admissionHelper ? ['--ro-bind-fd', String(admissionHelperDescriptorNumber), CONTAINED_EXECUTION_GATE_PATH] : []),
783
+ ...(admissionHelper ? ['--info-fd', String(admissionControlDescriptorNumber + 1)] : []),
584
784
  '--chdir', cwd,
585
- '--', CONTAINED_PROVIDER_EXECUTABLE_PATH, ...args,
785
+ '--', ...(admissionHelper ? [CONTAINED_EXECUTION_GATE_PATH, String(admissionControlDescriptorNumber), '30000'] : []),
786
+ CONTAINED_PROVIDER_EXECUTABLE_PATH, ...args,
586
787
  ];
587
788
  return Object.freeze({
588
789
  kind: 'bubblewrap_pid_namespace_v1',
@@ -596,8 +797,13 @@ function buildAuthorityContainment({
596
797
  ...(promptRecord ? [promptRecord.descriptor] : []),
597
798
  trustedProvider.descriptor,
598
799
  ...trustedSupportingProviders.map((support) => support.descriptor),
800
+ ...(observerChain ? [observerChain.descriptors.at(-1)] : []),
801
+ ...(admissionHelper ? [admissionHelper.descriptor] : []),
599
802
  ]),
803
+ admissionHelper,
804
+ admissionControlDescriptorNumber: admissionHelper ? admissionControlDescriptorNumber : null,
600
805
  pinnedProjectChain: projectChain,
806
+ observerChain,
601
807
  maskedReadChains: Object.freeze(maskedChains),
602
808
  pinnedWriteScopes,
603
809
  projectReference: projectChain.reference,
@@ -612,6 +818,7 @@ function buildAuthorityContainment({
612
818
  digest: sha256({
613
819
  binary: BUBBLEWRAP_PATH,
614
820
  binary_sha256: trustedBubblewrap.binaryDigest,
821
+ ...(admissionHelper ? { admission_helper_sha256: admissionHelper.executable.digest } : {}),
615
822
  provider_executable: trustedProvider.binding,
616
823
  supporting_provider_executables: trustedSupportingProviders.map((support) => support.binding),
617
824
  args: containmentArgs.slice(0, -args.length - 1),
@@ -623,12 +830,267 @@ function closeAuthorityContainment(containment) {
623
830
  const descriptors = new Set([
624
831
  ...(containment?.inheritedDescriptors ?? []),
625
832
  ...(containment?.pinnedProjectChain?.descriptors ?? []),
833
+ ...(containment?.pinnedStateChain?.descriptors ?? []),
626
834
  ...(containment?.pinnedWriteScopes?.descriptors ?? []),
835
+ ...(containment?.observerChain?.descriptors ?? []),
627
836
  ]);
628
837
  for (const descriptor of descriptors) {
629
838
  try { closeSync(descriptor); } catch { /* preserve the primary spawn result */ }
630
839
  }
631
840
  for (const chain of containment?.maskedReadChains ?? []) closeDirectoryChain(chain);
841
+ for (const chain of containment?.controlPlaneChains ?? []) closeDirectoryChain(chain);
842
+ }
843
+
844
+ function assertAuthorityContainmentStable(containment) {
845
+ runTerminalSecurityTestHook('beforeAuthorityExecutableRevalidation', { containment });
846
+ if (containment.darwinLaunch) {
847
+ assertDarwinSealedLaunch(containment.darwinLaunch);
848
+ for (const chain of [containment.pinnedProjectChain, containment.pinnedStateChain,
849
+ ...containment.controlPlaneChains, ...containment.maskedReadChains].filter(Boolean)) assertPinnedDirectoryChain(chain);
850
+ assertPinnedWriteScopes(containment.pinnedWriteScopes);
851
+ for (const entry of containment.pinnedWriteScopes.entries) {
852
+ const stat = lstatSync(entry.target);
853
+ const matches = entry.kind === 'directory'
854
+ ? sameDirectorySnapshot(directorySnapshot(stat), entry.snapshot)
855
+ : sameExecutableSnapshot(executableSnapshot(stat), entry.snapshot);
856
+ if (realpathSync(entry.target) !== entry.target || !matches) return failContainment('Darwin write scope pathname no longer names its pinned target');
857
+ }
858
+ return true;
859
+ }
860
+ currentBubblewrapTrust(containment.inheritedDescriptors[0]);
861
+ digestPinnedExecutable(containment.inheritedDescriptors[0], containment.trustedExecutable);
862
+ if (containment.admissionHelper) digestPinnedExecutable(containment.admissionHelper.descriptor, containment.admissionHelper.executable);
863
+ const executable = digestProviderExecutable(
864
+ containment.providerExecutableDescriptor, containment.trustedProviderExecutable.snapshot,
865
+ );
866
+ if (executable.sha256 !== containment.providerExecutableBinding.executable_sha256) {
867
+ throw Object.assign(new Error('provider executable changed after installation binding validation'), { code: 'PROVIDER_EXECUTABLE_DRIFT' });
868
+ }
869
+ containment.supportingProviderExecutableDescriptors.forEach((descriptor, index) => {
870
+ const supporting = digestProviderExecutable(descriptor, containment.trustedSupportingProviderExecutables[index].snapshot);
871
+ if (supporting.sha256 !== containment.supportingProviderExecutableBindings[index].executable_sha256) {
872
+ throw Object.assign(new Error('provider supporting executable changed after installation binding validation'), { code: 'PROVIDER_EXECUTABLE_DRIFT' });
873
+ }
874
+ });
875
+ assertPinnedDirectoryChain(containment.pinnedProjectChain);
876
+ if (containment.observerChain) assertPinnedDirectoryChain(containment.observerChain);
877
+ for (const chain of containment.maskedReadChains) assertPinnedDirectoryChain(chain);
878
+ assertPinnedWriteScopes(containment.pinnedWriteScopes);
879
+ return true;
880
+ }
881
+
882
+ /**
883
+ * Execute one frozen check in a disposable result workspace. This low-level
884
+ * observer reports process facts only. It cannot issue certificates, assert a
885
+ * verdict or choose the canonical attempt, plan or commit for its caller.
886
+ */
887
+ export async function runIsolatedVerificationCheck(options = {}) {
888
+ return runIsolatedVerificationProcess(options);
889
+ }
890
+
891
+ /** Internal trusted observer entry point; not exposed through check plans or CLI. */
892
+ export async function runIsolatedBrowserSupervisor({ observerDirectory, ...options } = {}) {
893
+ if (typeof observerDirectory !== 'string' || !isAbsolute(observerDirectory)) {
894
+ throw Object.assign(new Error('browser observer requires private read-only tools'), { code: 'VERIFICATION_CHECK_INVALID' });
895
+ }
896
+ return runIsolatedVerificationProcess(options, observerDirectory);
897
+ }
898
+
899
+ /** Separate trusted role, never selectable by an application check plan. */
900
+ export async function runIsolatedDarwinBrowserObserver({ observerDirectory, applicationProject, proxyPort, baseBundleId, applicationSignal, ...options } = {}) {
901
+ if (process.platform !== 'darwin') return failContainment('Darwin browser observer requires an actual Mac');
902
+ if (!(applicationSignal instanceof AbortSignal)) return failContainment('browser observer requires its owned application lifetime signal');
903
+ return runIsolatedVerificationProcess(options, null,
904
+ structuredClone({ observerDirectory, applicationProject, proxyPort, baseBundleId }), applicationSignal);
905
+ }
906
+
907
+ async function runIsolatedVerificationProcess(options = {}, observerDirectory = null, darwinBrowserObserver = null, applicationSignal = null) {
908
+ // Canonical CI/acceptance opts into immutable source; standalone disposable
909
+ // diagnostics retain their existing writable-subject default. Never CLI input.
910
+ const { workingDir, executableBinding, args, stdin, timeoutMs, subjectReadOnly = false } = options;
911
+ const fields = ['workingDir', 'executableBinding', 'args', 'stdin', 'timeoutMs', 'subjectReadOnly'];
912
+ if (Object.keys(options).some((field) => !fields.includes(field))
913
+ || (Object.hasOwn(options, 'subjectReadOnly') && typeof options.subjectReadOnly !== 'boolean')
914
+ || (subjectReadOnly && darwinBrowserObserver !== null)
915
+ || typeof workingDir !== 'string' || !isAbsolute(workingDir)
916
+ || !Array.isArray(args) || args.length > 64
917
+ || args.some((arg) => typeof arg !== 'string' || arg.includes('\0') || Buffer.byteLength(arg) > 8192)
918
+ || typeof stdin !== 'string' || stdin.length === 0 || Buffer.byteLength(stdin) > 1000000
919
+ || !Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 1800000) {
920
+ throw Object.assign(new Error('verification check requires bounded script, arguments, working directory and timeout only'), { code: 'VERIFICATION_CHECK_INVALID' });
921
+ }
922
+ if (process.platform === 'darwin') {
923
+ if (observerDirectory !== null) return failContainment('Darwin browser verification isolation is not implemented');
924
+ return runDarwinVerificationProcess(options, darwinBrowserObserver, applicationSignal);
925
+ }
926
+ let containment;
927
+ let child;
928
+ try {
929
+ containment = buildAuthorityContainment({
930
+ args: [...args], cwd: resolve(workingDir), readOnly: subjectReadOnly, writeScope: subjectReadOnly ? [] : ['.'],
931
+ terminalId: generateTerminalId('verification'), providerExecutableBinding: executableBinding,
932
+ verificationIsolation: true,
933
+ observerDirectory,
934
+ });
935
+ assertAuthorityContainmentStable(containment);
936
+ child = spawn(containment.command, containment.args, {
937
+ cwd: '/', detached: true,
938
+ env: { PATH: AUTHORITY_SYSTEM_PATH, LANG: 'C.UTF-8', LC_ALL: 'C.UTF-8', TZ: 'UTC', TMPDIR: '/tmp',
939
+ ...(observerDirectory ? { XDG_CACHE_HOME: '/tmp/chati-observer-cache', PLAYWRIGHT_BROWSERS_PATH: '/run/chati-observer-tools/chromium' } : {}),
940
+ },
941
+ stdio: ['pipe', 'pipe', 'pipe', ...containment.inheritedDescriptors],
942
+ });
943
+ } finally {
944
+ closeAuthorityContainment(containment);
945
+ }
946
+ return new Promise((resolveResult, reject) => {
947
+ const chunks = { stdout: [], stderr: [] };
948
+ const outputLimit = 2 * 1024 * 1024;
949
+ let outputBytes = 0;
950
+ let timedOut = false;
951
+ let outputLimitExceeded = false;
952
+ let inputError = null;
953
+ let spawnError = null;
954
+ const terminate = () => {
955
+ try { signalProcessTree(child, child.pid, 'SIGKILL'); }
956
+ catch (error) { spawnError ??= error; }
957
+ };
958
+ const timer = setTimeout(() => { timedOut = true; terminate(); }, timeoutMs);
959
+ for (const stream of ['stdout', 'stderr']) {
960
+ child[stream].on('data', (chunk) => {
961
+ const remaining = Math.max(0, outputLimit - outputBytes);
962
+ if (remaining > 0) chunks[stream].push(chunk.subarray(0, remaining));
963
+ outputBytes += chunk.length;
964
+ if (outputBytes > outputLimit) { outputLimitExceeded = true; terminate(); }
965
+ });
966
+ }
967
+ child.once('error', (error) => { spawnError = error; });
968
+ child.stdin.on('error', (error) => { inputError = error.code || error.name; });
969
+ child.once('close', (exitCode, signal) => {
970
+ clearTimeout(timer);
971
+ if (spawnError) { reject(spawnError); return; }
972
+ if (!processTreeStopsAfterClose(child.pid, child.pid)) {
973
+ terminate();
974
+ reject(Object.assign(new Error('verification process group did not terminate'), { code: 'VERIFICATION_PROCESS_STILL_RUNNING' }));
975
+ return;
976
+ }
977
+ const stdout = Buffer.concat(chunks.stdout);
978
+ const stderr = Buffer.concat(chunks.stderr);
979
+ resolveResult(Object.freeze({
980
+ exit_code: exitCode, signal, timed_out: timedOut, output_limit_exceeded: outputLimitExceeded,
981
+ input_error: inputError, process_tree_stopped: true,
982
+ subject_read_only: subjectReadOnly,
983
+ stdout: stdout.toString('utf8'), stderr: stderr.toString('utf8'),
984
+ stdout_base64: stdout.toString('base64'), stderr_base64: stderr.toString('base64'),
985
+ stdout_sha256: createHash('sha256').update(stdout).digest('hex'),
986
+ stderr_sha256: createHash('sha256').update(stderr).digest('hex'),
987
+ captured_output_bytes: stdout.length + stderr.length,
988
+ stdin_sha256: createHash('sha256').update(stdin).digest('hex'),
989
+ executable_binding: containment.providerExecutableBinding,
990
+ containment_kind: containment.kind, containment_digest: containment.digest,
991
+ }));
992
+ });
993
+ child.stdin.end(stdin);
994
+ });
995
+ }
996
+
997
+ async function runDarwinVerificationProcess(options, browserObserver = null, applicationSignal = null) {
998
+ const { executableBinding, args, stdin, timeoutMs, subjectReadOnly = false } = options;
999
+ const workingDir = realpathSync(options.workingDir);
1000
+ const context = { authority_kind: 'verification_check', invocation_id: randomUUID(),
1001
+ launch_nonce: randomUUID(), executable_binding_digest: sha256(executableBinding) };
1002
+ let containment; let child; let timer; let escalation; let drainTimer; let cancelObserver;
1003
+ const assertStable = () => browserObserver ? assertDarwinSealedLaunch(containment.darwinLaunch) : assertAuthorityContainmentStable(containment);
1004
+ try {
1005
+ if (browserObserver) {
1006
+ const launch = prepareDarwinBrowserObserverLaunch({ binding: executableBinding, args: [...args], projectDir: workingDir,
1007
+ authorityContext: context, ...browserObserver });
1008
+ containment = { darwinLaunch: launch, kind: launch.kind, providerExecutableBinding: structuredClone(executableBinding),
1009
+ digest: sha256({ role: 'browser-observer', helper_sha256: launch.helperSha256, policy_digest: launch.policyDigest,
1010
+ executable_binding: executableBinding, native_nonce: launch.supervisorChannels.nonce }) };
1011
+ } else {
1012
+ containment = buildAuthorityContainment({ args: [...args], cwd: workingDir,
1013
+ readOnly: subjectReadOnly, writeScope: subjectReadOnly ? [] : ['.'],
1014
+ terminalId: generateTerminalId('verification'), providerExecutableBinding: executableBinding,
1015
+ verificationIsolation: true, authorityContext: context });
1016
+ }
1017
+ assertStable();
1018
+ const launch = containment.darwinLaunch;
1019
+ child = spawn(launch.command, launch.args, { cwd: '/', detached: true,
1020
+ env: { PATH: '/usr/bin:/bin:/usr/sbin:/sbin', LANG: 'C.UTF-8', LC_ALL: 'C.UTF-8', TZ: 'UTC',
1021
+ HOME: launch.temporaryDirectory, TMPDIR: launch.temporaryDirectory },
1022
+ stdio: ['pipe', 'pipe', 'pipe', 'ignore', launch.projectDescriptor, ...launch.supervisorChannels.inheritedDescriptors] });
1023
+ let ready; let setupError; let inputError = null; let spawnError = null;
1024
+ let timedOut = false; let outputLimitExceeded = false; let outputBytes = 0;
1025
+ const chunks = { stdout: [], stderr: [] };
1026
+ const stop = (signal = 'SIGTERM') => {
1027
+ if (child.exitCode !== null || child.signalCode !== null) return;
1028
+ try { signalProcessTree(child, child.pid, signal); } catch (error) { spawnError ??= error; }
1029
+ if (!escalation) escalation = setTimeout(() => {
1030
+ if (child.exitCode !== null || child.signalCode !== null) return;
1031
+ try { signalProcessTree(child, child.pid, 'SIGKILL'); } catch { /* failed completion is not success */ }
1032
+ }, 2000);
1033
+ };
1034
+ const finished = new Promise((done, reject) => {
1035
+ for (const stream of ['stdout', 'stderr']) child[stream].on('data', bytes => {
1036
+ const available = Math.max(0, 2 * 1024 * 1024 - outputBytes);
1037
+ if (available) chunks[stream].push(bytes.subarray(0, available));
1038
+ outputBytes += bytes.length;
1039
+ if (outputBytes > 2 * 1024 * 1024 && !outputLimitExceeded) { outputLimitExceeded = true; stop(); }
1040
+ });
1041
+ child.once('error', error => { spawnError = error; });
1042
+ child.stdin.on('error', error => { inputError = error.code || error.name; });
1043
+ // A detached writer may keep stdout open after the helper exits. Bound
1044
+ // this drain and reject, never accept truncated output or wait forever.
1045
+ child.once('exit', () => {
1046
+ clearTimeout(timer); clearTimeout(escalation);
1047
+ drainTimer = setTimeout(() => {
1048
+ child.stdout.destroy(); child.stderr.destroy(); child.stdin.destroy();
1049
+ reject(Object.assign(new Error('verification output did not close after native helper exit'), { code: 'VERIFICATION_OUTPUT_NOT_CLOSED' }));
1050
+ }, 2000);
1051
+ });
1052
+ child.once('close', (exitCode, signal) => {
1053
+ try {
1054
+ if (setupError || spawnError) throw setupError || spawnError;
1055
+ const completion = launch.supervisorChannels.readCompletion(exitCode);
1056
+ assertStable();
1057
+ const stdout = Buffer.concat(chunks.stdout); const stderr = Buffer.concat(chunks.stderr);
1058
+ const facts = { exit_code: exitCode, signal, timed_out: timedOut, output_limit_exceeded: outputLimitExceeded,
1059
+ input_error: inputError, process_tree_stopped: false, process_group_stopped: true,
1060
+ subject_read_only: subjectReadOnly,
1061
+ stdout: stdout.toString('utf8'), stderr: stderr.toString('utf8'),
1062
+ stdout_base64: stdout.toString('base64'), stderr_base64: stderr.toString('base64'),
1063
+ stdout_sha256: createHash('sha256').update(stdout).digest('hex'), stderr_sha256: createHash('sha256').update(stderr).digest('hex'),
1064
+ captured_output_bytes: stdout.length + stderr.length, stdin_sha256: createHash('sha256').update(stdin).digest('hex'),
1065
+ executable_binding: projectVerificationExecutableBinding(containment.providerExecutableBinding, executableBinding),
1066
+ containment_kind: containment.kind, containment_digest: containment.digest,
1067
+ darwin_verification_binding: createDarwinVerificationBinding({ context, helperSha256: launch.helperSha256,
1068
+ policyDigest: launch.policyDigest, ready, completion, workingDir, executableBinding, args, stdin }) };
1069
+ verificationProcessCompleted(facts, { ...options, workingDir });
1070
+ done(Object.freeze(facts));
1071
+ } catch (error) { reject(error); }
1072
+ });
1073
+ });
1074
+ timer = setTimeout(() => { timedOut = true; stop(); }, timeoutMs);
1075
+ if (applicationSignal) {
1076
+ cancelObserver = () => { spawnError ??= Object.assign(new Error('browser application exited during observation'), { code: 'BROWSER_APPLICATION_EXITED' }); stop(); };
1077
+ applicationSignal.addEventListener('abort', cancelObserver, { once: true });
1078
+ }
1079
+ try {
1080
+ if (applicationSignal?.aborted) throw Object.assign(new Error('browser application already exited'), { code: 'BROWSER_APPLICATION_EXITED' });
1081
+ ready = launch.supervisorChannels.readReady(child.pid, launch.verifiedCdhash);
1082
+ assertStable();
1083
+ launch.supervisorChannels.authorize();
1084
+ child.stdin.end(stdin);
1085
+ } catch (error) { setupError = error; stop('SIGKILL'); }
1086
+ return await finished;
1087
+ } finally {
1088
+ if (cancelObserver) applicationSignal.removeEventListener('abort', cancelObserver);
1089
+ clearTimeout(timer); clearTimeout(escalation); clearTimeout(drainTimer);
1090
+ // No recursive cleanup of a potentially detached writer's mutable TMP.
1091
+ try { if (containment?.darwinLaunch) disposeDarwinSealedLaunch(containment.darwinLaunch); }
1092
+ finally { closeAuthorityContainment(containment); }
1093
+ }
632
1094
  }
633
1095
 
634
1096
  function signalProcessTree(child, processGroupId, signal) {
@@ -655,7 +1117,55 @@ function processTreeStopsAfterClose(childPid, processGroupId) {
655
1117
  return false;
656
1118
  }
657
1119
 
658
- function terminalAuthorityReceipt(material) {
1120
+ async function stopDarwinAuthority(runtime) {
1121
+ const child = runtime.child;
1122
+ const wasRunning = !runtime.finalized;
1123
+ if (wasRunning) runtime.killRequested = true;
1124
+ const signalHelper = (signal) => {
1125
+ // The native helper owns a SEPARATE admitted provider process group. On
1126
+ // Darwin its own zombie-only group may report EPERM. Never interpret a
1127
+ // lookup of that group (or a reused PID after close) as provider lifetime.
1128
+ if (child.exitCode === null && child.signalCode === null) {
1129
+ try { child.kill(signal); } catch { /* only private completion can prove cleanup */ }
1130
+ }
1131
+ };
1132
+ const waitForClose = async () => {
1133
+ let timer;
1134
+ try {
1135
+ return await Promise.race([
1136
+ runtime.exitPromise.then(() => true),
1137
+ new Promise(resolve => { timer = setTimeout(() => resolve(false), 5000); }),
1138
+ ]);
1139
+ } finally { clearTimeout(timer); }
1140
+ };
1141
+ if (wasRunning) {
1142
+ signalHelper('SIGTERM');
1143
+ if (!(await waitForClose())) {
1144
+ signalHelper('SIGKILL');
1145
+ if (!(await waitForClose())) {
1146
+ throw Object.assign(new Error('native Darwin helper did not close with a private completion'),
1147
+ { code: 'TERMINAL_PROCESS_TREE_UNRESOLVED' });
1148
+ }
1149
+ }
1150
+ }
1151
+ // SIGKILL/ESRCH/helper exit alone is never positive completion evidence.
1152
+ // readCompletion in finalize validates the original READY/GO binding and
1153
+ // scoped group emptiness; detached descendants remain explicitly unknown.
1154
+ if (runtime.timeoutCleanupFailure) throw runtime.timeoutCleanupFailure;
1155
+ if (!runtime.darwinCompletion) {
1156
+ throw Object.assign(new Error('Darwin cleanup requires its private supervisor observation'),
1157
+ { code: 'DARWIN_COMPLETION_OBSERVATION_REQUIRED' });
1158
+ }
1159
+ return { killed: wasRunning, exitCode: runtime.exitCode };
1160
+ }
1161
+
1162
+ function terminalAuthorityReceipt(material, { launchReceipt } = {}) {
1163
+ if (material.purpose === 'terminal_launch' && material.process_containment_kind === 'darwin_scoped_process_group_v1') {
1164
+ assertDarwinTerminalLaunch(material);
1165
+ }
1166
+ if (launchReceipt?.process_containment_kind === 'darwin_scoped_process_group_v1') {
1167
+ assertDarwinTerminalCompletion(material, launchReceipt);
1168
+ }
659
1169
  const signed = signHostAuthorityPayload(canonicalize(material), {
660
1170
  domain: TERMINAL_AUTHORITY_DOMAIN,
661
1171
  });
@@ -727,10 +1237,18 @@ export function getTerminalCompletionAuthorityReceipt(handle) {
727
1237
  if (runtime.outputOverflow) {
728
1238
  throw Object.assign(new Error('terminal output exceeded the authority capture limit'), { code: 'TERMINAL_OUTPUT_OVERFLOW' });
729
1239
  }
730
- if (!processTreeStopsAfterClose(runtime.child.pid, runtime.processGroupId)) {
1240
+ if (state.launchReceipt.darwin_launch_binding && !runtime.darwinCompletion) {
1241
+ throw Object.assign(new Error('Darwin completion requires its private supervisor observation'), { code: 'DARWIN_COMPLETION_OBSERVATION_REQUIRED' });
1242
+ }
1243
+ if (!state.launchReceipt.darwin_launch_binding
1244
+ && !processTreeStopsAfterClose(runtime.child.pid, runtime.processGroupId)) {
731
1245
  try { signalProcessTree(runtime.child, runtime.processGroupId, 'SIGKILL'); } catch { /* fail closed below */ }
732
1246
  throw Object.assign(new Error('terminal process group remained active after the provider CLI exited'), { code: 'TERMINAL_PROCESS_GROUP_STILL_RUNNING' });
733
1247
  }
1248
+ if (state.launchReceipt.process_namespace_identity
1249
+ && observeLinuxNamespace(state.launchReceipt.process_namespace_identity, { timeoutMs: 500 }).status !== 'exited') {
1250
+ throw Object.assign(new Error('the exact provider namespace has not finished exiting'), { code: 'TERMINAL_NAMESPACE_STILL_RUNNING' });
1251
+ }
734
1252
  if (!state.completionReceipt) {
735
1253
  const stdout = runtime.stdout.join('');
736
1254
  let railResult = null;
@@ -762,6 +1280,13 @@ export function getTerminalCompletionAuthorityReceipt(handle) {
762
1280
  schema_version: 1,
763
1281
  purpose: 'terminal_completion',
764
1282
  launch_receipt_digest: sha256(state.launchReceipt),
1283
+ ...(state.launchReceipt.process_namespace_identity ? {
1284
+ process_namespace_identity_digest: sha256(state.launchReceipt.process_namespace_identity),
1285
+ } : {}),
1286
+ ...(runtime.darwinCompletion ? {
1287
+ darwin_launch_binding_digest: sha256(state.launchReceipt.darwin_launch_binding),
1288
+ darwin_completion_observation: runtime.darwinCompletion,
1289
+ } : {}),
765
1290
  terminal_id: identity.terminal_id,
766
1291
  invocation_id: state.context.invocation_id,
767
1292
  launch_nonce: state.context.launch_nonce,
@@ -790,12 +1315,15 @@ export function getTerminalCompletionAuthorityReceipt(handle) {
790
1315
  } : {}),
791
1316
  ...(railResult || {}),
792
1317
  completed_at: runtime.completedAt,
793
- });
1318
+ process_time_kind: 'provider-invocation-wall-time',
1319
+ process_elapsed_ms: runtime.processElapsedMs,
1320
+ }, { launchReceipt: state.launchReceipt });
794
1321
  }
795
1322
  return state.completionReceipt;
796
1323
  }
797
1324
 
798
- /** Wait for an authority terminal and propagate timeout cleanup proof failures. */
1325
+ /** Wait for an authority terminal and propagate cleanup/private completion failures.
1326
+ * Darwin completion proves the scoped process group, not all detached descendants. */
799
1327
  export async function waitForTerminal(handle) {
800
1328
  const state = TERMINAL_AUTHORITY_STATE.get(handle);
801
1329
  if (!state) {
@@ -818,6 +1346,10 @@ export async function waitForTerminal(handle) {
818
1346
  if (!cleanup.ok) throw cleanup.error;
819
1347
  }
820
1348
  if (runtime.timeoutCleanupFailure) throw runtime.timeoutCleanupFailure;
1349
+ // Planning's retry wrapper must not accept output or retry an exited helper
1350
+ // without a complete, bound private observation. The other Darwin consumers
1351
+ // use this same wait boundary; none may interpret an exit event as proof.
1352
+ if (state.launchReceipt.darwin_launch_binding) getTerminalCompletionAuthorityReceipt(handle);
821
1353
  return handle;
822
1354
  }
823
1355
 
@@ -869,11 +1401,18 @@ const BLOCKED_ENV_EXACT = ['CLAUDECODE'];
869
1401
  * User-configured vars like CLAUDE_API_KEY are preserved.
870
1402
  *
871
1403
  * @param {Record<string, string>} env - Source environment (typically process.env)
1404
+ * @param {object} [options] - Explicit child provider and secret policy
872
1405
  * @returns {Record<string, string>} Cleaned environment (new object)
873
1406
  */
874
- export function cleanParentEnv(env) {
1407
+ export function cleanParentEnv(env, { provider, allowProviderSecrets = true } = {}) {
875
1408
  const cleaned = {};
876
1409
  for (const [key, value] of Object.entries(env)) {
1410
+ // setup-token is an authentication credential, not parent session state.
1411
+ // Unknown/other providers and secret-free launches must still strip it.
1412
+ if (key === 'CLAUDE_CODE_OAUTH_TOKEN' && provider === 'claude' && allowProviderSecrets) {
1413
+ cleaned[key] = value;
1414
+ continue;
1415
+ }
877
1416
  if (BLOCKED_ENV_EXACT.includes(key)) continue;
878
1417
  if (BLOCKED_ENV_PREFIXES.some(prefix => key.startsWith(prefix))) continue;
879
1418
  cleaned[key] = value;
@@ -888,10 +1427,10 @@ const AUTHORITY_PROVIDER_SECRET_PREFIXES = Object.freeze({
888
1427
  });
889
1428
  const AUTHORITY_SECRET_NAME = /(API[_-]?KEY|ACCESS[_-]?KEY|PRIVATE[_-]?KEY|PASSWORD|SECRET|TOKEN|CREDENTIAL|AUTH[_-]?SOCK)/i;
890
1429
 
891
- export function cleanAuthorityEnv(env, provider) {
892
- const allowedPrefixes = AUTHORITY_PROVIDER_SECRET_PREFIXES[provider] || [];
1430
+ export function cleanAuthorityEnv(env, provider, { allowProviderSecrets = true } = {}) {
1431
+ const allowedPrefixes = allowProviderSecrets ? (AUTHORITY_PROVIDER_SECRET_PREFIXES[provider] || []) : [];
893
1432
  const cleaned = {};
894
- for (const [key, value] of Object.entries(cleanParentEnv(env))) {
1433
+ for (const [key, value] of Object.entries(cleanParentEnv(env, { provider, allowProviderSecrets }))) {
895
1434
  if (AUTHORITY_SECRET_NAME.test(key) && !allowedPrefixes.some((prefix) => key.startsWith(prefix))) continue;
896
1435
  cleaned[key] = value;
897
1436
  }
@@ -920,8 +1459,10 @@ export function cleanAuthorityEnv(env, provider) {
920
1459
  * @property {number} [timeout] - Max execution time in ms
921
1460
  * @property {function} [beforeProcessSpawn] - Read-only preparation after containment is pinned
922
1461
  * @property {function} [beforeAuthoritySpawn] - Final synchronous authority reservation before spawn
1462
+ * @property {function} [afterAuthoritySpawn] - Commit dispatch with (receipt, handle) before positive-GO
923
1463
  * @property {string[]} [maskedReadPaths] - Canonical host paths hidden from an authority provider
924
1464
  * @property {string} [auditProjectDir] - Canonical CHATI project used only for the host-side model audit
1465
+ * @property {boolean} [councilToolIsolation] - Disable local tools for an untrusted council subject
925
1466
  */
926
1467
 
927
1468
  /**
@@ -976,6 +1517,7 @@ export function buildSpawnCommand(config) {
976
1517
 
977
1518
  const env = {
978
1519
  ...isolationEnv,
1520
+ ...(config.providerExecutableBinding !== undefined ? sealedProviderRuntimeEnvironment(config.provider) : {}),
979
1521
  CHATI_TERMINAL_ID: terminalId,
980
1522
  CHATI_AGENT: config.agent,
981
1523
  CHATI_TASK_ID: config.taskId,
@@ -1050,47 +1592,93 @@ export function spawnTerminal(config) {
1050
1592
  throw Object.assign(new Error(`authorityContext.${field} is required`), { code: 'INVALID_TERMINAL_AUTHORITY_CONTEXT' });
1051
1593
  }
1052
1594
  }
1595
+ // Only callers with an explicit Darwin lifetime/cleanup contract are enabled.
1596
+ if (process.platform === 'darwin'
1597
+ && !['rail_execution', 'rail_review', 'rail_adjudication', 'planning_execution', 'council_gate'].includes(authorityContext.authority_kind)) {
1598
+ throw Object.assign(new Error(`Darwin lifetime and cleanup are not integrated for ${authorityContext.authority_kind}`), {
1599
+ code: 'DARWIN_AUTHORITY_CONSUMER_UNAVAILABLE',
1600
+ });
1601
+ }
1053
1602
  }
1054
1603
 
1055
1604
  const readOnly = Array.isArray(config.writeScope) && config.writeScope.length === 0;
1056
1605
  if (config.beforeProcessSpawn !== undefined && typeof config.beforeProcessSpawn !== 'function') {
1057
1606
  throw Object.assign(new Error('beforeProcessSpawn must be a function'), { code: 'INVALID_TERMINAL_PRESPAWN_HOOK' });
1058
1607
  }
1608
+
1609
+ // Snapshot the eligibility before caller hooks. The sealed Claude transport
1610
+ // needs its own auth even when council subjects have no tools/host secrets.
1611
+ // This is not a prefix exception and does not change general secret filtering.
1612
+ const councilClaudeTransportAuth = actualProvider === 'claude' && readOnly
1613
+ && config.councilToolIsolation === true && authorityContext?.authority_kind === 'council_gate';
1059
1614
  if (config.beforeAuthoritySpawn !== undefined
1060
1615
  && (authorityContext === undefined || typeof config.beforeAuthoritySpawn !== 'function')) {
1061
1616
  throw Object.assign(new Error('beforeAuthoritySpawn requires an authority context and callback'), { code: 'INVALID_TERMINAL_PRESPAWN_HOOK' });
1062
1617
  }
1618
+ if (config.afterAuthoritySpawn !== undefined
1619
+ && (authorityContext?.authority_kind !== 'rail_execution' || typeof config.afterAuthoritySpawn !== 'function')) {
1620
+ throw Object.assign(new Error('afterAuthoritySpawn requires a RAIL execution authority and synchronous durable-dispatch callback'), { code: 'INVALID_TERMINAL_ADMISSION_HOOK' });
1621
+ }
1063
1622
 
1064
1623
  const detachedProcessGroup = authorityContext !== undefined && process.platform !== 'win32';
1065
1624
  let promptRecord;
1066
1625
  let containment;
1067
1626
  let sealedProvider;
1627
+ let darwinLaunch;
1628
+ let darwinCompletionError;
1629
+ let darwinReady;
1630
+ let darwinRuntimeOwned = false;
1631
+ let darwinDisposed = false;
1632
+ const disposeDarwin = (validate = false) => {
1633
+ if (!darwinLaunch || darwinDisposed) return;
1634
+ darwinDisposed = true;
1635
+ try { if (validate) assertDarwinSealedLaunch(darwinLaunch); }
1636
+ catch (error) { darwinCompletionError = error; }
1637
+ try { disposeDarwinSealedLaunch(darwinLaunch); }
1638
+ catch (error) { darwinCompletionError ??= error; }
1639
+ };
1068
1640
  let executionWorktreeBaseline;
1641
+ let executionAdmissionReceipt;
1642
+ let darwinHostBootIdentity;
1643
+ let executionNamespaceIdentity;
1644
+ let processInfoPipe;
1069
1645
  let spawnArgs = args;
1070
1646
  let child;
1647
+ let processStartedTick;
1071
1648
  try {
1072
1649
  if (promptFile !== null) {
1073
1650
  promptRecord = openPrivatePromptFile(promptFile);
1074
- spawnArgs = materializePrivatePromptArgs(args, authorityContext === undefined
1075
- ? '/proc/self/fd/3'
1651
+ spawnArgs = materializePrivatePromptArgs(args, authorityContext === undefined || process.platform === 'darwin'
1652
+ ? (process.platform === 'darwin' ? '/dev/fd/3' : '/proc/self/fd/3')
1076
1653
  : CONTAINED_PROMPT_PATH);
1077
1654
  }
1078
1655
  containment = authorityContext === undefined ? null : buildAuthorityContainment({
1079
1656
  args: spawnArgs, cwd: resolve(cwd), readOnly, terminalId, provider: actualProvider,
1080
1657
  writeScope: config.writeScope, providerExecutableBinding: config.providerExecutableBinding, promptRecord,
1081
1658
  maskedReadPaths: config.maskedReadPaths,
1659
+ councilToolIsolation: config.councilToolIsolation === true,
1660
+ executionAdmission: config.afterAuthoritySpawn !== undefined,
1661
+ authorityContext,
1082
1662
  });
1663
+ darwinLaunch = containment?.darwinLaunch;
1083
1664
  if (!containment && config.providerExecutableBinding !== undefined) {
1084
- if (process.platform !== 'linux') {
1665
+ if (process.platform === 'darwin') {
1666
+ darwinLaunch = prepareDarwinSealedLaunch({
1667
+ binding: config.providerExecutableBinding, harnessId: actualProvider,
1668
+ projectDir: cwd, args: spawnArgs, hasPrompt: Boolean(promptRecord),
1669
+ promptDirectory: promptRecord?.directory,
1670
+ });
1671
+ } else if (process.platform === 'linux') {
1672
+ sealedProvider = openPinnedProviderExecutable(config.providerExecutableBinding, {
1673
+ harnessId: actualProvider,
1674
+ projectDir: cwd,
1675
+ });
1676
+ } else {
1085
1677
  throw Object.assign(
1086
- new Error('sealed Planning provider execution requires Linux descriptor execution'),
1678
+ new Error('sealed Planning provider execution is unavailable on this platform'),
1087
1679
  { code: 'PROVIDER_EXECUTABLE_UNSUPPORTED' },
1088
1680
  );
1089
1681
  }
1090
- sealedProvider = openPinnedProviderExecutable(config.providerExecutableBinding, {
1091
- harnessId: actualProvider,
1092
- projectDir: cwd,
1093
- });
1094
1682
  }
1095
1683
  const auditProjectDir = config.auditProjectDir ? resolve(config.auditProjectDir) : null;
1096
1684
  if (containment && auditProjectDir && auditProjectDir !== resolve(cwd)
@@ -1121,28 +1709,9 @@ export function spawnTerminal(config) {
1121
1709
  }
1122
1710
 
1123
1711
  if (config.beforeProcessSpawn) config.beforeProcessSpawn();
1712
+ if (darwinLaunch) assertDarwinSealedLaunch(darwinLaunch);
1124
1713
  if (containment) {
1125
- runTerminalSecurityTestHook('beforeAuthorityExecutableRevalidation', { containment });
1126
- currentBubblewrapTrust(containment.inheritedDescriptors[0]);
1127
- digestPinnedExecutable(containment.inheritedDescriptors[0], containment.trustedExecutable);
1128
- const providerExecutable = digestProviderExecutable(
1129
- containment.providerExecutableDescriptor,
1130
- containment.trustedProviderExecutable.snapshot,
1131
- );
1132
- if (providerExecutable.sha256 !== containment.providerExecutableBinding.executable_sha256) {
1133
- throw Object.assign(new Error('provider executable changed after installation binding validation'), { code: 'PROVIDER_EXECUTABLE_DRIFT' });
1134
- }
1135
- containment.supportingProviderExecutableDescriptors.forEach((descriptor, index) => {
1136
- const supportingExecutable = digestProviderExecutable(
1137
- descriptor, containment.trustedSupportingProviderExecutables[index].snapshot,
1138
- );
1139
- if (supportingExecutable.sha256 !== containment.supportingProviderExecutableBindings[index].executable_sha256) {
1140
- throw Object.assign(new Error('provider supporting executable changed after installation binding validation'), { code: 'PROVIDER_EXECUTABLE_DRIFT' });
1141
- }
1142
- });
1143
- assertPinnedDirectoryChain(containment.pinnedProjectChain);
1144
- for (const chain of containment.maskedReadChains) assertPinnedDirectoryChain(chain);
1145
- assertPinnedWriteScopes(containment.pinnedWriteScopes);
1714
+ assertAuthorityContainmentStable(containment);
1146
1715
  } else if (sealedProvider) {
1147
1716
  const providerExecutable = digestProviderExecutable(
1148
1717
  sealedProvider.descriptor,
@@ -1155,26 +1724,65 @@ export function spawnTerminal(config) {
1155
1724
  if (executionWorktreeBaseline) {
1156
1725
  executionWorktreeBaseline = assertRailExecutionWorktreeBaselineUnchanged(executionWorktreeBaseline);
1157
1726
  }
1727
+ if (darwinLaunch?.supervisorChannels) darwinHostBootIdentity = captureDarwinHostBootIdentity();
1728
+ if (containment?.admissionHelper || (darwinLaunch?.supervisorChannels && config.afterAuthoritySpawn)) {
1729
+ if (containment.admissionHelper) processInfoPipe = [...createLinuxProcessInfoPipe()];
1730
+ if (!executionWorktreeBaseline) {
1731
+ throw Object.assign(new Error('execution admission requires an observed canonical repository baseline'), { code: 'EXECUTION_ADMISSION_BASE_REQUIRED' });
1732
+ }
1733
+ executionAdmissionReceipt = terminalAuthorityReceipt({
1734
+ schema_version: 1, purpose: 'terminal_execution_admission', authority_kind: 'rail_execution',
1735
+ execution_admission_protocol: 'native-positive-go-v1',
1736
+ execution_admission_helper_sha256: containment.admissionHelper?.executable.digest ?? darwinLaunch.helperSha256,
1737
+ invocation_id: authorityContext.invocation_id, launch_nonce: authorityContext.launch_nonce,
1738
+ terminal_id: terminalId, task_id: config.taskId,
1739
+ supervisor_process_id: `process://chati/${process.pid}`, supervisor_hostname: hostname(),
1740
+ ...(darwinHostBootIdentity ? { darwin_host_boot_identity: darwinHostBootIdentity } : {}),
1741
+ working_dir: resolve(cwd), project_identity_digest: containment.projectIdentityDigest,
1742
+ prompt_digest: sha256(config.prompt ?? ''), provider_id: authorityContext.provider_id,
1743
+ harness_id: actualProvider, model_id: actualModel, reasoning_configuration: actualReasoningConfiguration,
1744
+ provider_executable_path: containment.providerExecutableBinding.executable_path,
1745
+ provider_executable_sha256: containment.providerExecutableBinding.executable_sha256,
1746
+ execution_base_commit_ref: executionWorktreeBaseline.base_commit_ref,
1747
+ execution_launch_head_ref: executionWorktreeBaseline.launch_head_ref,
1748
+ execution_repository_identity_digest: executionWorktreeBaseline.repository_identity_digest,
1749
+ execution_preexisting_commits_digest: executionWorktreeBaseline.preexisting_commits_digest,
1750
+ execution_worktree_clean: true,
1751
+ });
1752
+ }
1158
1753
  if (config.beforeAuthoritySpawn) {
1159
1754
  config.beforeAuthoritySpawn(Object.freeze({
1160
1755
  working_dir: resolve(cwd),
1161
1756
  project_identity_digest: containment.projectIdentityDigest,
1757
+ ...(executionAdmissionReceipt ? { admission_receipt: executionAdmissionReceipt } : {}),
1162
1758
  }));
1759
+ runTerminalSecurityTestHook('afterAuthorityLaunchReservation', {});
1163
1760
  }
1164
1761
  if (executionWorktreeBaseline) {
1165
1762
  executionWorktreeBaseline = assertRailExecutionWorktreeBaselineUnchanged(executionWorktreeBaseline);
1166
1763
  }
1167
1764
  const sealedProviderDescriptorNumber = 3 + (promptRecord ? 1 : 0);
1168
1765
  const spawnCommand = containment?.command
1766
+ ?? darwinLaunch?.command
1169
1767
  ?? (sealedProvider ? `/proc/self/fd/${sealedProviderDescriptorNumber}` : command);
1170
- child = spawn(spawnCommand, containment?.args ?? spawnArgs, {
1171
- cwd: containment ? '/' : cwd,
1768
+ processStartedTick = process.hrtime.bigint();
1769
+ child = spawn(spawnCommand, containment?.args ?? darwinLaunch?.args ?? spawnArgs, {
1770
+ cwd: containment || darwinLaunch ? '/' : cwd,
1172
1771
  env: {
1173
- ...(containment ? cleanAuthorityEnv(process.env, actualProvider) : cleanParentEnv(process.env)), ...env,
1174
- ...(containment ? { TMPDIR: '/tmp', TMP: '/tmp', TEMP: '/tmp' } : {}),
1772
+ ...(containment ? cleanAuthorityEnv(process.env, actualProvider, {
1773
+ allowProviderSecrets: config.councilToolIsolation !== true,
1774
+ }) : cleanParentEnv(process.env, {
1775
+ provider: actualProvider, allowProviderSecrets: config.councilToolIsolation !== true,
1776
+ })), ...env,
1777
+ ...(containment && councilClaudeTransportAuth && typeof process.env.CLAUDE_CODE_OAUTH_TOKEN === 'string'
1778
+ ? { CLAUDE_CODE_OAUTH_TOKEN: process.env.CLAUDE_CODE_OAUTH_TOKEN } : {}),
1779
+ ...(containment ? Object.fromEntries(['TMPDIR', 'TMP', 'TEMP'].map(key => [key, darwinLaunch?.temporaryDirectory ?? '/tmp'])) : {}),
1175
1780
  },
1176
- stdio: containment
1177
- ? ['pipe', 'pipe', 'pipe', ...containment.inheritedDescriptors]
1781
+ stdio: darwinLaunch
1782
+ ? ['pipe', 'pipe', 'pipe', promptRecord ? promptRecord.descriptor : 'ignore', darwinLaunch.projectDescriptor,
1783
+ ...(darwinLaunch.supervisorChannels?.inheritedDescriptors ?? [])]
1784
+ : containment
1785
+ ? ['pipe', 'pipe', 'pipe', ...containment.inheritedDescriptors, ...(containment.admissionHelper ? ['pipe', processInfoPipe[1]] : [])]
1178
1786
  : [
1179
1787
  'pipe', 'pipe', 'pipe',
1180
1788
  ...(promptRecord ? [promptRecord.descriptor] : []),
@@ -1182,23 +1790,49 @@ export function spawnTerminal(config) {
1182
1790
  ],
1183
1791
  detached: detachedProcessGroup,
1184
1792
  });
1793
+ // Post-spawn identity reads may throw synchronously. Register ownership
1794
+ // cleanup first so the error path cannot strand a private file prompt.
1185
1795
  if (promptRecord) {
1186
1796
  child.once('exit', () => cleanupPrivatePromptFile(promptRecord));
1187
1797
  child.once('error', () => cleanupPrivatePromptFile(promptRecord));
1188
1798
  }
1799
+ if (darwinLaunch) {
1800
+ // Until the trusted runtime owns completion, failed setup still owns its
1801
+ // resources. Authority launches must keep the completion pipe alive past
1802
+ // exit and read it only after stdio has drained on close.
1803
+ if (darwinLaunch.supervisorChannels) {
1804
+ child.once('close', () => { if (!darwinRuntimeOwned) disposeDarwin(true); });
1805
+ } else child.once('exit', () => disposeDarwin(true));
1806
+ child.once('error', () => { if (!darwinRuntimeOwned) disposeDarwin(); });
1807
+ }
1808
+ if (processInfoPipe) {
1809
+ closeSync(processInfoPipe[1]); processInfoPipe[1] = null;
1810
+ const info = JSON.parse(readLinuxProcessInfoPipe(processInfoPipe[0]));
1811
+ runTerminalSecurityTestHook('beforeLinuxNamespaceIdentityCapture', { info, child });
1812
+ executionNamespaceIdentity = captureLinuxNamespaceIdentity(info, child.pid);
1813
+ }
1814
+ if (darwinLaunch?.supervisorChannels) {
1815
+ darwinReady = darwinLaunch.supervisorChannels.readReady(child.pid, darwinLaunch.verifiedCdhash);
1816
+ assertAuthorityContainmentStable(containment);
1817
+ }
1189
1818
  if (containment) {
1190
1819
  assertPinnedDirectoryChain(containment.pinnedProjectChain);
1191
1820
  }
1192
1821
  } catch (error) {
1822
+ if (darwinLaunch?.supervisorChannels) {
1823
+ try { darwinLaunch.supervisorChannels.dispose(); } catch (cleanupError) { error.darwinChannelCleanupError = cleanupError; }
1824
+ }
1193
1825
  if (child?.pid) {
1194
1826
  try { signalProcessTree(child, detachedProcessGroup ? child.pid : null, 'SIGKILL'); } catch { /* reservation remains fail-closed */ }
1195
1827
  }
1196
1828
  throw error;
1197
1829
  } finally {
1830
+ for (const fd of processInfoPipe ?? []) if (fd !== null) closeSync(fd);
1198
1831
  if (containment) closeAuthorityContainment(containment);
1199
1832
  if (sealedProvider?.descriptor !== undefined) try { closeSync(sealedProvider.descriptor); } catch { /* child inherited its own descriptor */ }
1200
1833
  if (promptRecord) try { closeSync(promptRecord.descriptor); } catch { /* containment already closed it */ }
1201
1834
  if (promptRecord && !child) cleanupPrivatePromptFile(promptRecord);
1835
+ if (darwinLaunch && !child) disposeDarwin();
1202
1836
  }
1203
1837
  const processGroupId = detachedProcessGroup ? child.pid : null;
1204
1838
 
@@ -1222,13 +1856,17 @@ export function spawnTerminal(config) {
1222
1856
  stderrBytes: 0,
1223
1857
  outputOverflow: false,
1224
1858
  completedAt: null,
1859
+ processElapsedMs: null,
1225
1860
  killRequested: false,
1226
1861
  finalized: false,
1227
1862
  exitPromise: new Promise((resolveExit) => { resolveAuthorityExit = resolveExit; }),
1228
1863
  cleanupFailureSignal: new Promise((resolveFailure) => { signalAuthorityCleanupFailure = resolveFailure; }),
1864
+ signalCleanupFailure: signalAuthorityCleanupFailure,
1229
1865
  timeoutCleanupPromise: null,
1230
1866
  timeoutCleanupSettled: false,
1231
1867
  timeoutCleanupFailure: null,
1868
+ darwinCompletion: null,
1869
+ darwinStopPromise: null,
1232
1870
  };
1233
1871
  const processFacade = runtime ? new EventEmitter() : child;
1234
1872
  if (runtime) {
@@ -1238,7 +1876,9 @@ export function spawnTerminal(config) {
1238
1876
  });
1239
1877
  Object.defineProperty(processFacade, 'kill', {
1240
1878
  enumerable: true,
1241
- value: (signal) => signalProcessTree(child, processGroupId, signal),
1879
+ value: (signal) => darwinLaunch?.supervisorChannels
1880
+ ? (child.exitCode === null && child.signalCode === null ? child.kill(signal) : false)
1881
+ : signalProcessTree(child, processGroupId, signal),
1242
1882
  });
1243
1883
  }
1244
1884
 
@@ -1255,6 +1895,21 @@ export function spawnTerminal(config) {
1255
1895
  startedAt: new Date().toISOString(),
1256
1896
  timeout,
1257
1897
  };
1898
+ const diagnosticObservation = {
1899
+ timeout, elapsedMs: null, exitCode: null, timedOut: false, terminationCause: 'unknown',
1900
+ };
1901
+ TERMINAL_DIAGNOSTIC_OBSERVATIONS.set(handle, diagnosticObservation);
1902
+ const observeTermination = (code, signal, error = false) => {
1903
+ if (diagnosticObservation.elapsedMs !== null) return;
1904
+ diagnosticObservation.elapsedMs = Number(process.hrtime.bigint() - processStartedTick) / 1e6;
1905
+ diagnosticObservation.exitCode = Number.isInteger(code) ? code : null;
1906
+ if (diagnosticObservation.terminationCause === 'unknown') {
1907
+ diagnosticObservation.terminationCause = error ? 'spawn_error'
1908
+ : signal ? 'signal' : Number.isInteger(code) ? 'exit' : 'unknown';
1909
+ }
1910
+ };
1911
+ child.once(runtime ? 'close' : 'exit', (code, signal) => observeTermination(code, signal));
1912
+ child.once('error', () => observeTermination(null, null, true));
1258
1913
 
1259
1914
  if (runtime) {
1260
1915
  Object.defineProperties(handle, {
@@ -1277,7 +1932,8 @@ export function spawnTerminal(config) {
1277
1932
  provider_executable_path: containment.providerExecutableBinding.executable_path,
1278
1933
  provider_executable_sha256: containment.providerExecutableBinding.executable_sha256,
1279
1934
  });
1280
- const launchReceipt = terminalAuthorityReceipt({
1935
+ let launchReceipt;
1936
+ try { launchReceipt = terminalAuthorityReceipt({
1281
1937
  schema_version: 1,
1282
1938
  purpose: 'terminal_launch',
1283
1939
  authority_kind: context.authority_kind,
@@ -1290,6 +1946,17 @@ export function spawnTerminal(config) {
1290
1946
  process_containment_kind: containment.kind,
1291
1947
  process_containment_id: containment.id,
1292
1948
  process_containment_digest: containment.digest,
1949
+ ...(executionNamespaceIdentity ? { process_namespace_identity: executionNamespaceIdentity } : {}),
1950
+ ...(darwinReady ? {
1951
+ darwin_launch_binding: createDarwinTerminalLaunchBinding({ helperSha256: darwinLaunch.helperSha256,
1952
+ policyDigest: darwinLaunch.policyDigest, ready: darwinReady, writeScopes: containment.pinnedWriteScopes,
1953
+ hostBootIdentity: darwinHostBootIdentity }),
1954
+ } : {}),
1955
+ ...(executionAdmissionReceipt ? {
1956
+ execution_admission_protocol: 'native-positive-go-v1',
1957
+ execution_admission_helper_sha256: containment.admissionHelper?.executable.digest ?? darwinLaunch.helperSha256,
1958
+ execution_admission_receipt_digest: sha256(executionAdmissionReceipt),
1959
+ } : {}),
1293
1960
  project_identity_digest: containment.projectIdentityDigest,
1294
1961
  agent: config.agent,
1295
1962
  task_id: config.taskId,
@@ -1314,7 +1981,13 @@ export function spawnTerminal(config) {
1314
1981
  execution_worktree_clean: true,
1315
1982
  } : {}),
1316
1983
  started_at: handle.startedAt,
1317
- });
1984
+ }); } catch (error) {
1985
+ if (darwinLaunch?.supervisorChannels) {
1986
+ try { darwinLaunch.supervisorChannels.dispose(); } catch (cleanupError) { error.darwinChannelCleanupError = cleanupError; }
1987
+ try { signalProcessTree(child, processGroupId, 'SIGKILL'); } catch { /* no positive GO has been sent */ }
1988
+ }
1989
+ throw error;
1990
+ }
1318
1991
  TERMINAL_AUTHORITY_STATE.set(handle, {
1319
1992
  context: Object.freeze({ ...context }),
1320
1993
  identity,
@@ -1338,6 +2011,7 @@ export function spawnTerminal(config) {
1338
2011
  if (runtime[key] + bytes > MAX_AUTHORITY_OUTPUT_BYTES) {
1339
2012
  if (!runtime.outputOverflow) {
1340
2013
  runtime.outputOverflow = true;
2014
+ if (diagnosticObservation.terminationCause === 'unknown') diagnosticObservation.terminationCause = 'output_limit';
1341
2015
  runtime.stderr.push(`terminal output exceeded ${MAX_AUTHORITY_OUTPUT_BYTES} byte authority limit`);
1342
2016
  runtime.killRequested = true;
1343
2017
  try { signalProcessTree(child, processGroupId, 'SIGKILL'); } catch { /* process already exited */ }
@@ -1375,6 +2049,18 @@ export function spawnTerminal(config) {
1375
2049
  runtime.status = runtime.killRequested ? 'killed' : 'exited';
1376
2050
  runtime.exitCode = Number.isInteger(code) ? code : -1;
1377
2051
  runtime.completedAt = new Date().toISOString();
2052
+ runtime.processElapsedMs = Number(process.hrtime.bigint() - processStartedTick) / 1e6;
2053
+ if (darwinLaunch?.supervisorChannels) {
2054
+ try {
2055
+ if (error) throw error;
2056
+ runtime.darwinCompletion = darwinLaunch.supervisorChannels.readCompletion(code);
2057
+ } catch (completionError) { darwinCompletionError = completionError; }
2058
+ disposeDarwin(true);
2059
+ if (darwinCompletionError) {
2060
+ runtime.timeoutCleanupFailure = darwinCompletionError;
2061
+ signalAuthorityCleanupFailure(darwinCompletionError);
2062
+ }
2063
+ }
1378
2064
  if (error) runtime.stderr.push(`spawn error: ${error.message}`);
1379
2065
  if (error && processFacade.listenerCount('error') > 0) processFacade.emit('error', error);
1380
2066
  processFacade.emit('exit', runtime.exitCode);
@@ -1385,10 +2071,12 @@ export function spawnTerminal(config) {
1385
2071
  // never omit output that arrived after the OS process exited.
1386
2072
  child.once('close', (code) => finalize(code));
1387
2073
  child.once('error', (error) => finalize(-1, error));
2074
+ darwinRuntimeOwned = Boolean(darwinLaunch?.supervisorChannels);
1388
2075
  } else {
1389
2076
  child.on('exit', (code) => {
1390
2077
  handle.status = 'exited';
1391
- handle.exitCode = code;
2078
+ handle.exitCode = darwinCompletionError ? -1 : code;
2079
+ if (darwinCompletionError) handle.stderr.push(`Darwin sealed execution cleanup: ${darwinCompletionError.message}`);
1392
2080
  });
1393
2081
 
1394
2082
  child.on('error', (err) => {
@@ -1401,6 +2089,8 @@ export function spawnTerminal(config) {
1401
2089
  // Enforce timeout — kill process if it exceeds max execution time
1402
2090
  const timeoutTimer = setTimeout(() => {
1403
2091
  if (handle.status === 'running') {
2092
+ diagnosticObservation.timedOut = true;
2093
+ if (diagnosticObservation.terminationCause === 'unknown') diagnosticObservation.terminationCause = 'runtime_timeout';
1404
2094
  (runtime?.stderr ?? handle.stderr).push(`timeout: process exceeded ${timeout}ms limit`);
1405
2095
  if (runtime) {
1406
2096
  runtime.timeoutCleanupPromise = killTerminal(handle).then(
@@ -1426,6 +2116,32 @@ export function spawnTerminal(config) {
1426
2116
  // Clear timer when process exits normally
1427
2117
  child.on('close', () => clearTimeout(timeoutTimer));
1428
2118
 
2119
+ if (config.afterAuthoritySpawn || darwinLaunch?.supervisorChannels) {
2120
+ const channel = darwinLaunch?.supervisorChannels ? null : child.stdio[containment.admissionControlDescriptorNumber];
2121
+ // No provider code has executed. A failed commit closes the private channel
2122
+ // and the native helper rejects EOF; it never interprets closure as GO.
2123
+ channel?.on('error', () => {});
2124
+ try {
2125
+ // Hand ownership to the caller before invoking its journal write, so a
2126
+ // synchronous failure still allows it to await normal cleanup proof.
2127
+ const committed = config.afterAuthoritySpawn?.(getTerminalLaunchAuthorityReceipt(handle), handle);
2128
+ if (committed && typeof committed.then === 'function') {
2129
+ Promise.resolve(committed).catch(() => {});
2130
+ throw Object.assign(new Error('execution admission cannot await an uncommitted asynchronous dispatch'), { code: 'ASYNC_TERMINAL_ADMISSION_FORBIDDEN' });
2131
+ }
2132
+ runTerminalSecurityTestHook('beforeExecutionAdmissionGo', { handle });
2133
+ if (darwinLaunch?.supervisorChannels) darwinLaunch.supervisorChannels.authorize();
2134
+ else channel.end('G');
2135
+ } catch (error) {
2136
+ if (darwinLaunch?.supervisorChannels) {
2137
+ try { darwinLaunch.supervisorChannels.dispose(); } catch (cleanupError) { error.darwinChannelCleanupError = cleanupError; }
2138
+ } else channel.destroy();
2139
+ runtime.killRequested = true;
2140
+ try { signalProcessTree(child, processGroupId, 'SIGKILL'); } catch { /* gate still rejects EOF */ }
2141
+ throw error;
2142
+ }
2143
+ }
2144
+
1429
2145
  return handle;
1430
2146
  }
1431
2147
 
@@ -1572,9 +2288,23 @@ export function killTerminal(handle) {
1572
2288
  return Promise.resolve({ killed: false, exitCode: handle?.exitCode ?? null });
1573
2289
  }
1574
2290
 
2291
+ const diagnosticObservation = TERMINAL_DIAGNOSTIC_OBSERVATIONS.get(handle);
2292
+ if (diagnosticObservation?.elapsedMs === null && diagnosticObservation.terminationCause === 'unknown') {
2293
+ diagnosticObservation.terminationCause = 'stop_requested';
2294
+ }
2295
+
1575
2296
  const authorityState = TERMINAL_AUTHORITY_STATE.get(handle);
1576
2297
  const rawProcess = authorityState?.runtime?.child ?? handle.process;
1577
2298
  const processGroupId = authorityState?.runtime?.processGroupId ?? null;
2299
+ if (authorityState?.launchReceipt.darwin_launch_binding) {
2300
+ const { runtime } = authorityState;
2301
+ runtime.darwinStopPromise ??= stopDarwinAuthority(runtime).catch(error => {
2302
+ runtime.timeoutCleanupFailure = error;
2303
+ runtime.signalCleanupFailure(error);
2304
+ throw error;
2305
+ });
2306
+ return runtime.darwinStopPromise;
2307
+ }
1578
2308
  if (authorityState) {
1579
2309
  authorityState.runtime.killRequested = true;
1580
2310
  return (async () => {
@@ -1702,7 +2432,7 @@ export function isTransientFailure(exitCode, stderr) {
1702
2432
  * Non-transient failures are returned immediately without retry.
1703
2433
  *
1704
2434
  * @param {SpawnConfig} config
1705
- * @param {{ maxRetries?: number, baseDelay?: number, shouldRetry?: function }} [retryOptions={}]
2435
+ * @param {{ maxRetries?: number, baseDelay?: number, shouldRetry?: function, beforeAttempt?: function }} [retryOptions={}]
1706
2436
  * @returns {Promise<TerminalHandle>}
1707
2437
  */
1708
2438
  export async function spawnTerminalWithRetry(config, retryOptions = {}) {
@@ -1713,57 +2443,84 @@ export async function spawnTerminalWithRetry(config, retryOptions = {}) {
1713
2443
  const enableModelFallback = retryOptions.enableModelFallback === true;
1714
2444
 
1715
2445
  let lastHandle = null;
1716
-
1717
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
1718
- const handle = spawnTerminal(config);
1719
- lastHandle = handle;
1720
-
1721
- // Wait for process to exit
2446
+ let currentHandle = null;
2447
+ let exceptionStage = null;
2448
+ const attempts = [];
2449
+ const observeCurrent = () => {
2450
+ if (!currentHandle) return;
2451
+ const observation = terminalAttemptSnapshot(currentHandle, attempts.length);
2452
+ if (observation) attempts.push(observation);
2453
+ currentHandle = null;
2454
+ };
2455
+ const spawnObserved = async (attemptConfig, beforeAttempt) => {
2456
+ exceptionStage = 'before_attempt';
2457
+ beforeAttempt?.();
2458
+ exceptionStage = 'spawn';
2459
+ currentHandle = spawnTerminal(attemptConfig);
2460
+ const handle = currentHandle;
2461
+ exceptionStage = 'wait';
1722
2462
  await waitForTerminal(handle);
2463
+ observeCurrent();
2464
+ exceptionStage = null;
2465
+ return handle;
2466
+ };
1723
2467
 
1724
- // Success — return immediately
1725
- if (handle.exitCode === 0) {
1726
- handle.retryCount = attempt;
1727
- return handle;
1728
- }
1729
-
1730
- // Check if failure is transient and retries remain
1731
- const combinedOutput = [...(handle.stdout || []), ...(handle.stderr || [])];
1732
- if (attempt < maxRetries && shouldRetry(handle.exitCode, combinedOutput, handle)) {
1733
- const delay = baseDelay * Math.pow(2, attempt);
1734
- await new Promise(resolve => setTimeout(resolve, delay));
1735
- continue;
1736
- }
1737
-
1738
- // Non-transient or out of retries — return as-is
1739
- break;
1740
- }
2468
+ try {
2469
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
2470
+ const handle = await spawnObserved(config, () => retryOptions.beforeAttempt?.({ attempt }));
2471
+ lastHandle = handle;
1741
2472
 
1742
- // Model fallback: if all retries exhausted with overload errors, try with downgraded model
1743
- if (enableModelFallback && lastHandle && lastHandle.exitCode !== 0) {
1744
- const stderrStr = Array.isArray(lastHandle.stderr) ? lastHandle.stderr.join('') : (lastHandle.stderr || '');
1745
- const isOverload = OVERLOAD_PATTERNS.some(p => p.test(stderrStr));
1746
- const fallbackModel = MODEL_DOWNGRADE_MAP[config.model];
2473
+ handle.retryCount = attempt;
1747
2474
 
1748
- if (isOverload && fallbackModel) {
1749
- const fallbackConfig = { ...config, model: fallbackModel };
1750
- const fallbackHandle = spawnTerminal(fallbackConfig);
2475
+ // Success return immediately
2476
+ if (handle.exitCode === 0) {
2477
+ handle.retryCount = attempt;
2478
+ return rememberAttemptDiagnostics(handle, attempts);
2479
+ }
1751
2480
 
1752
- await waitForTerminal(fallbackHandle);
2481
+ // Check if failure is transient and retries remain
2482
+ const combinedOutput = [...(handle.stdout || []), ...(handle.stderr || [])];
2483
+ exceptionStage = 'retry_decision';
2484
+ const retry = attempt < maxRetries && shouldRetry(handle.exitCode, combinedOutput, handle);
2485
+ exceptionStage = null;
2486
+ if (retry) {
2487
+ const delay = baseDelay * Math.pow(2, attempt);
2488
+ await new Promise(resolve => setTimeout(resolve, delay));
2489
+ continue;
2490
+ }
1753
2491
 
1754
- // Record fallback metadata
1755
- fallbackHandle.modelFallback = {
1756
- original: config.model,
1757
- actual: fallbackModel,
1758
- reason: 'overload_exhausted_retries',
1759
- timestamp: new Date().toISOString(),
1760
- };
2492
+ // Non-transient or out of retries — return as-is
2493
+ break;
2494
+ }
1761
2495
 
1762
- fallbackHandle.retryCount = maxRetries;
1763
- return fallbackHandle;
2496
+ // Model fallback: if all retries exhausted with overload errors, try with downgraded model
2497
+ if (enableModelFallback && lastHandle && lastHandle.exitCode !== 0) {
2498
+ const stderrStr = Array.isArray(lastHandle.stderr) ? lastHandle.stderr.join('') : (lastHandle.stderr || '');
2499
+ const isOverload = OVERLOAD_PATTERNS.some(p => p.test(stderrStr));
2500
+ const fallbackModel = MODEL_DOWNGRADE_MAP[config.model];
2501
+
2502
+ if (isOverload && fallbackModel) {
2503
+ const fallbackConfig = { ...config, model: fallbackModel };
2504
+ const fallbackHandle = await spawnObserved(fallbackConfig);
2505
+
2506
+ // Record fallback metadata
2507
+ fallbackHandle.modelFallback = {
2508
+ original: config.model,
2509
+ actual: fallbackModel,
2510
+ reason: 'overload_exhausted_retries',
2511
+ timestamp: new Date().toISOString(),
2512
+ };
2513
+
2514
+ fallbackHandle.retryCount = maxRetries;
2515
+ return rememberAttemptDiagnostics(fallbackHandle, attempts);
2516
+ }
1764
2517
  }
1765
- }
1766
2518
 
1767
- if (lastHandle) lastHandle.retryCount = lastHandle.retryCount ?? maxRetries;
1768
- return lastHandle;
2519
+ if (lastHandle) lastHandle.retryCount = lastHandle.retryCount ?? maxRetries;
2520
+ return rememberAttemptDiagnostics(lastHandle, attempts);
2521
+ } catch (error) {
2522
+ observeCurrent();
2523
+ rememberAttemptDiagnostics(error, attempts, exceptionStage);
2524
+ throw error;
2525
+ }
1769
2526
  }