@logicseek/hello-cc 1.0.5 → 1.1.0-rc.3

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 (146) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/docs/codex-account.zh-CN.md +38 -0
  3. package/docs/codex-protocol.md +59 -0
  4. package/docs/commands.md +9 -0
  5. package/docs/commands.zh-CN.md +7 -0
  6. package/docs/macos-process-identity.md +2 -0
  7. package/docs/native.md +179 -14
  8. package/docs/native.zh-CN.md +136 -6
  9. package/docs/web-handoff.zh-CN.md +118 -8
  10. package/lib/cli/app.mjs +47 -4
  11. package/lib/cli/commands/coordination.mjs +14 -4
  12. package/lib/cli/commands/dsh.mjs +1 -1
  13. package/lib/cli/commands/gc.mjs +22 -15
  14. package/lib/cli/commands/hook.mjs +19 -3
  15. package/lib/cli/commands/install.mjs +80 -10
  16. package/lib/cli/commands/misc.mjs +36 -9
  17. package/lib/cli/commands/native.mjs +4 -38
  18. package/lib/cli/commands/peer.mjs +3 -0
  19. package/lib/cli/commands/run.mjs +13 -6
  20. package/lib/cli/commands/tmux.mjs +1 -2
  21. package/lib/cli-runtime.mjs +19 -2
  22. package/lib/core/coordination/results.mjs +27 -10
  23. package/lib/core/sessions/hook-root-identity.mjs +47 -0
  24. package/lib/core/sessions/launch.mjs +7 -1
  25. package/lib/db/connection.mjs +140 -42
  26. package/lib/db/schema.mjs +59 -0
  27. package/lib/diagnostics/codex-protocol.mjs +105 -0
  28. package/lib/guidance.mjs +21 -36
  29. package/lib/integrations/codex-account.mjs +143 -0
  30. package/lib/integrations/codex-interactions.mjs +13 -11
  31. package/lib/integrations/dsh-collaboration.mjs +6 -12
  32. package/lib/integrations/dsh-cordis.mjs +17 -13
  33. package/lib/integrations/dsh.mjs +16 -11
  34. package/lib/integrations/mcp-elicitation.mjs +116 -0
  35. package/lib/integrations/mcp-url-elicitation.mjs +25 -0
  36. package/lib/integrations/native/claude.mjs +24 -6
  37. package/lib/integrations/native/codex.mjs +38 -7
  38. package/lib/integrations/native/dsh-acp.mjs +67 -24
  39. package/lib/integrations/native/index.mjs +4 -1
  40. package/lib/integrations/native/interactions.mjs +2 -1
  41. package/lib/integrations/native/jsonrpc.mjs +37 -5
  42. package/lib/integrations/native/telemetry.mjs +30 -0
  43. package/lib/process/identity.mjs +4 -0
  44. package/lib/process/pinned-cwd.mjs +302 -0
  45. package/lib/process/pinned-guidance.mjs +138 -0
  46. package/lib/process/pinned-project-state.mjs +105 -0
  47. package/lib/process/selected-cwd-identity.mjs +95 -0
  48. package/lib/runtime/buffer-evidence.mjs +2 -1
  49. package/lib/runtime/client.mjs +9 -0
  50. package/lib/runtime/native/client.mjs +4 -1
  51. package/lib/runtime/native/history.mjs +140 -0
  52. package/lib/runtime/native/launcher.mjs +106 -0
  53. package/lib/runtime/native/service.mjs +64 -8
  54. package/lib/runtime/native/store.mjs +83 -58
  55. package/lib/runtime/paths.mjs +17 -2
  56. package/lib/runtime/private-file.mjs +76 -0
  57. package/lib/runtime/private-state.mjs +546 -0
  58. package/lib/runtime/project-context.mjs +2 -1
  59. package/lib/runtime/project-path.mjs +92 -20
  60. package/lib/runtime/project-trust.mjs +79 -0
  61. package/lib/runtime/projects.mjs +9 -3
  62. package/lib/runtime/state-migration.mjs +697 -0
  63. package/lib/runtime/state.mjs +69 -27
  64. package/lib/shared/automatic-result-key.mjs +6 -0
  65. package/lib/shared/json-file.mjs +55 -6
  66. package/lib/shared/redact.mjs +2 -1
  67. package/lib/ui/help.mjs +6 -4
  68. package/lib/web/agent-defaults.mjs +110 -0
  69. package/lib/web/auto-attach.mjs +3 -2
  70. package/lib/web/browser/contracts.d.ts +10 -0
  71. package/lib/web/browser/core.mjs +1997 -0
  72. package/lib/web/browser/project-data.mjs +158 -0
  73. package/lib/web/browser/session-sync.mjs +82 -0
  74. package/lib/web/browser-assets.mjs +19 -0
  75. package/lib/web/buffer-gc-runtime.mjs +2 -2
  76. package/lib/web/codex-app-server.mjs +111 -37
  77. package/lib/web/codex-sessions.mjs +61 -12
  78. package/lib/web/context-files.mjs +82 -0
  79. package/lib/web/cookie-auth.mjs +51 -3
  80. package/lib/web/external-sessions.mjs +76 -9
  81. package/lib/web/http-routes.mjs +238 -25
  82. package/lib/web/http.mjs +50 -4
  83. package/lib/web/liveness-reaper.mjs +2 -2
  84. package/lib/web/native-sessions.mjs +323 -14
  85. package/lib/web/project-contexts.mjs +236 -44
  86. package/lib/web/project-file-write.mjs +249 -0
  87. package/lib/web/project-files.mjs +268 -0
  88. package/lib/web/pty-sessions.mjs +11 -3
  89. package/lib/web/runtime-main.mjs +132 -80
  90. package/lib/web/session-serialize.mjs +87 -8
  91. package/lib/web/session-state-sync.mjs +98 -0
  92. package/lib/web/shutdown-diagnostics.mjs +38 -0
  93. package/lib/web/shutdown-response.mjs +21 -0
  94. package/lib/web/shutdown-state.mjs +91 -0
  95. package/lib/web/startup.mjs +103 -35
  96. package/lib/web/task-results.mjs +19 -19
  97. package/lib/web/tls.mjs +84 -33
  98. package/lib/web/tmux-sessions.mjs +73 -9
  99. package/lib/web/tmux-stream.mjs +36 -8
  100. package/lib/web/ui-agent-defaults.mjs +149 -0
  101. package/lib/web/ui-agent-start.mjs +330 -0
  102. package/lib/web/ui-codex-account.mjs +81 -0
  103. package/lib/web/ui-codex.mjs +105 -85
  104. package/lib/web/ui-command-palette.mjs +21 -5
  105. package/lib/web/ui-file-writes.mjs +220 -0
  106. package/lib/web/ui-files.mjs +231 -0
  107. package/lib/web/ui-handoff.mjs +10 -2
  108. package/lib/web/ui-history.mjs +175 -105
  109. package/lib/web/ui-i18n.mjs +280 -8
  110. package/lib/web/ui-interactions.mjs +107 -6
  111. package/lib/web/ui-native-timeline.mjs +326 -0
  112. package/lib/web/ui-native.mjs +93 -43
  113. package/lib/web/ui-review.mjs +5 -3
  114. package/lib/web/ui-safe-markdown.mjs +79 -0
  115. package/lib/web/ui-session-tools.mjs +224 -0
  116. package/lib/web/ui-template.mjs +29 -1968
  117. package/lib/web/ui-terminal-find.mjs +5 -3
  118. package/lib/web/ui-workbench.mjs +5 -3
  119. package/lib/web/ui-workspace.mjs +3 -0
  120. package/package.json +8 -2
  121. package/scripts/benchmark-session-sync.mjs +53 -0
  122. package/scripts/codex-protocol.mjs +88 -0
  123. package/scripts/dsh-approval-layers-acceptance.mjs +172 -0
  124. package/scripts/dsh-package-provenance.mjs +118 -0
  125. package/scripts/mcp-form-live-fixture.mjs +81 -0
  126. package/scripts/native-bounded-live-acceptance.mjs +193 -0
  127. package/scripts/native-fixture-root.mjs +49 -0
  128. package/scripts/native-installed-acceptance.mjs +611 -0
  129. package/scripts/native-stability-checks.mjs +40 -0
  130. package/scripts/regression-cookie-expiry.mjs +70 -0
  131. package/scripts/regression-http-transport.mjs +9 -0
  132. package/scripts/regression-terminal-marker.mjs +95 -0
  133. package/scripts/regression.mjs +481 -219
  134. package/scripts/shim-reentry-probe.mjs +105 -0
  135. package/scripts/web-acceptance-diagnostics.mjs +90 -0
  136. package/scripts/web-codex-account-acceptance.mjs +178 -0
  137. package/scripts/web-file-preview-checks.mjs +302 -0
  138. package/scripts/web-mcp-form-acceptance.mjs +134 -0
  139. package/scripts/web-mcp-form-live-acceptance.mjs +368 -0
  140. package/scripts/web-native-handoff-checks.mjs +128 -0
  141. package/scripts/web-native-interaction-acceptance.mjs +219 -18
  142. package/scripts/web-native-interaction-diagnostics.mjs +80 -0
  143. package/scripts/web-native-model-acceptance.mjs +22 -7
  144. package/scripts/web-session-tools-checks.mjs +112 -0
  145. package/scripts/web-workbench-acceptance.mjs +435 -0
  146. package/scripts/web-workbench-installed-acceptance.mjs +122 -0
@@ -17,6 +17,11 @@ import WebSocket from 'ws';
17
17
  import { inspectProcessIdentity, compareProcessIdentity } from '../lib/process/identity.mjs';
18
18
  import { redactSecrets } from '../lib/shared/redact.mjs';
19
19
  import { applyBufferPlan, planBufferFiles } from '../lib/runtime/buffer-gc.mjs';
20
+ import { readOwnedReentryOutputAtDeadline, readReentryTrace } from './shim-reentry-probe.mjs';
21
+ import { createCookieExpiryWindow } from './regression-cookie-expiry.mjs';
22
+ import { waitForTerminalMarker, terminalMarkerFailureDiagnostic } from './regression-terminal-marker.mjs';
23
+ import { fixtureFetch as fetch } from './regression-http-transport.mjs';
24
+ import { parseShutdownDiagnostics } from '../lib/web/shutdown-diagnostics.mjs';
20
25
 
21
26
  const repoRoot = path.resolve(import.meta.dirname, '..');
22
27
  const hccBin = path.join(repoRoot, 'bin', 'hcc.mjs');
@@ -29,6 +34,10 @@ const tmuxSession = `hcc-reg-${process.pid}`;
29
34
  const tmuxSocketName = `hcc-reg-${testId}`.replace(/[^A-Za-z0-9_-]/g, '-');
30
35
  const secondProjectRoot = path.join(root, 'second-project');
31
36
  const TERMINAL_WEBSOCKET_TIMEOUT_MS = 15_000;
37
+ const COOKIE_WEBSOCKET_SNAPSHOT_TIMEOUT_MS = 5_000;
38
+ // now() uses whole seconds: TTL=1 may leave only 1ms before expiry. Reserve
39
+ // the existing snapshot budget plus clock quantization/setup margin first.
40
+ const COOKIE_EXPIRY_FIXTURE_TTL_SEC = Math.ceil(COOKIE_WEBSOCKET_SNAPSHOT_TIMEOUT_MS / 1000) + 2;
32
41
  const realHome = process.env.HOME || os.homedir();
33
42
  const realRegistryFile = path.join(realHome, '.hello-cc', 'projects.json');
34
43
  let realTmuxBin = '';
@@ -41,10 +50,13 @@ let currentStage = 'startup';
41
50
  let lastStopDiagnostic = null;
42
51
  const diagnosticSecrets = new Set();
43
52
  const fixtureFailures = new WeakSet();
53
+ const fixtureCommandFailures = new WeakMap();
54
+ const fixtureHttpFailures = new WeakMap();
44
55
  const managedTmuxSessions = new Set();
45
56
 
46
57
  const env = {
47
58
  ...process.env,
59
+ NODE_NO_WARNINGS: '1',
48
60
  HOME: home,
49
61
  PATH: `${fakeBin}${path.delimiter}${process.env.PATH || ''}`,
50
62
  SHELL: '/bin/bash'
@@ -53,6 +65,7 @@ for (const key of Object.keys(env)) {
53
65
  if (key.startsWith('HCC_')) delete env[key];
54
66
  }
55
67
  if (process.env.HCC_REGRESSION_DEBUG === '1') env.HCC_DEBUG = '1';
68
+ env.HCC_SHUTDOWN_DIAGNOSTICS = '1';
56
69
  delete env.TMUX;
57
70
  delete env.TMUX_PANE;
58
71
 
@@ -61,8 +74,8 @@ function log(message) {
61
74
  process.stdout.write(`${message}\n`);
62
75
  }
63
76
 
64
- function fail(message) {
65
- const error = fixtureFailureError(message);
77
+ function fail(message, commandResult = null, diagnosticOperation = null) {
78
+ const error = fixtureFailureError(message, commandResult, diagnosticOperation);
66
79
  if (process.env.GITHUB_ACTIONS === 'true') {
67
80
  const escaped = fixtureDiagnosticText({ stage: currentStage, failure: fixtureFailureDiagnostic(error) }, diagnosticSecrets)
68
81
  .replace(/%/g, '%25')
@@ -73,25 +86,91 @@ function fail(message) {
73
86
  throw error;
74
87
  }
75
88
 
76
- export function fixtureFailureError(message) {
89
+ export function fixtureFailureError(message, commandResult = null, diagnosticOperation = null) {
77
90
  const error = new Error(String(message));
78
91
  Object.defineProperty(error, 'fixtureStack', {
79
92
  value: Object.freeze(new Error('fixture failure').stack.split('\n').slice(1, 7))
80
93
  });
81
94
  fixtureFailures.add(error);
95
+ const command = fixtureCommandResult(commandResult, diagnosticOperation);
96
+ if (command) fixtureCommandFailures.set(error, command);
82
97
  return error;
83
98
  }
84
99
 
85
100
  export function fixtureFailureDiagnostic(error) {
86
101
  const names = ['Error', 'TypeError', 'RangeError', 'SyntaxError', 'AssertionError', 'AbortError'];
102
+ const terminalMarker = terminalMarkerFailureDiagnostic(error);
87
103
  return {
88
104
  name: names.includes(error?.name) ? error.name : 'Error',
89
105
  stack: fixtureFailures.has(error)
90
106
  ? error.fixtureStack
91
- : new Error('regression failure handler').stack.split('\n').slice(1, 7)
107
+ : new Error('regression failure handler').stack.split('\n').slice(1, 7),
108
+ ...(fixtureCommandFailures.has(error) ? { command: fixtureCommandFailures.get(error) } : {}),
109
+ ...(fixtureHttpFailures.has(error) ? { http: fixtureHttpFailures.get(error) } : {}),
110
+ ...(terminalMarker ? { terminalMarker } : {})
92
111
  };
93
112
  }
94
113
 
114
+ function fixtureHttpRoute(route) {
115
+ if (typeof route !== 'string') return null;
116
+ const pathname = route.split(/[?#]/, 1)[0];
117
+ if (['/', '/api/projects', '/api/runtime', '/api/runtime/gc-buffers', '/api/detected',
118
+ '/api/sessions', '/api/sessions/attach', '/api/resumable',
119
+ '/assets/web/browser/core.mjs', '/assets/web/ui-agent-start.mjs'].includes(pathname)) return pathname;
120
+ const action = pathname.match(/^\/api\/peers\/[^/?#]+\/actions\/(status|state|inbox|heartbeat|task-next|task-takeover|lock-acquire)$/);
121
+ if (action) return `/api/peers/:peer/actions/${action[1]}`;
122
+ const session = pathname.match(/^\/api\/sessions\/[^/?#]+\/(input|stop)$/);
123
+ if (session) return `/api/sessions/:peer/${session[1]}`;
124
+ if (/^\/api\/detected\/[^/?#]+\/msg$/.test(pathname)) return '/api/detected/:peer/msg';
125
+ return null;
126
+ }
127
+
128
+ function fixtureHttpCallFrames() {
129
+ // Capture our call site before invoking fetch. Never parse the thrown error's
130
+ // stack, whose text can contain URLs, request data or third-party messages.
131
+ const capture = {};
132
+ Error.captureStackTrace(capture, withFixtureHttpDiagnostic);
133
+ const source = pathToFileURL(path.join(repoRoot, 'scripts', 'regression.mjs')).href;
134
+ const location = new RegExp(`(?:^|[ (])${source.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}:(\\d+):(\\d+)\\)?$`);
135
+ return Object.freeze(String(capture.stack).split('\n').flatMap((frame) => {
136
+ const match = frame.match(location);
137
+ if (!match) return [];
138
+ return [Object.freeze({ file: 'scripts/regression.mjs', line: Number(match[1]), column: Number(match[2]) })];
139
+ }).slice(0, 6));
140
+ }
141
+
142
+ function fixtureHttpNetworkCode(error) {
143
+ const allowed = ['ECONNRESET', 'ECONNREFUSED', 'EHOSTUNREACH', 'ENETUNREACH', 'ENOTFOUND',
144
+ 'EAI_AGAIN', 'ETIMEDOUT', 'UND_ERR_CONNECT_TIMEOUT', 'UND_ERR_HEADERS_TIMEOUT',
145
+ 'UND_ERR_BODY_TIMEOUT', 'UND_ERR_SOCKET'];
146
+ // Reading data descriptors avoids evaluating arbitrary error/cause getters.
147
+ try {
148
+ const direct = Object.getOwnPropertyDescriptor(error, 'code')?.value;
149
+ const cause = Object.getOwnPropertyDescriptor(error, 'cause')?.value;
150
+ const nested = cause && Object.getOwnPropertyDescriptor(cause, 'code')?.value;
151
+ return allowed.includes(direct) ? direct : allowed.includes(nested) ? nested : null;
152
+ } catch { return null; }
153
+ }
154
+
155
+ export async function withFixtureHttpDiagnostic(route, options, request) {
156
+ const method = options?.method ?? 'GET';
157
+ const context = {
158
+ method: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'].includes(method) ? method : null,
159
+ route: fixtureHttpRoute(route),
160
+ callFrames: fixtureHttpCallFrames()
161
+ };
162
+ const started = performance.now();
163
+ try {
164
+ return await request();
165
+ } catch (error) {
166
+ if (error && (typeof error === 'object' || typeof error === 'function') && !fixtureHttpFailures.has(error)) {
167
+ fixtureHttpFailures.set(error, Object.freeze({ ...context,
168
+ code: fixtureHttpNetworkCode(error), elapsedMs: Math.max(0, Math.round(performance.now() - started)) }));
169
+ }
170
+ throw error;
171
+ }
172
+ }
173
+
95
174
  export function fixtureDiagnosticText(value, secrets = []) {
96
175
  function scrub(text) {
97
176
  for (const secret of secrets) {
@@ -129,7 +208,9 @@ export function observeFixtureOwner(owner, { inspect = inspectProcessIdentity, p
129
208
  }
130
209
  details.birthHash = crypto.createHash('sha256').update(observed.identity.startToken).digest('hex');
131
210
  details.commandHash = observed.identity.commandHash;
132
- if (compareProcessIdentity(owner, observed.identity) === 'dead') return { ...details, state: 'exited', reason: 'pid_reused' };
211
+ const comparison = compareProcessIdentity(owner, observed.identity);
212
+ if (comparison === 'dead') return { ...details, state: 'exited', reason: 'pid_reused' };
213
+ if (comparison !== 'live') return { ...details, state: 'unknown', reason: 'identity_incompatible' };
133
214
  if (owner.commandHash !== observed.identity.commandHash) return { ...details, state: 'unknown', reason: 'command_changed' };
134
215
  return { ...details, state: 'same-owner', reason: 'identity_match' };
135
216
  }
@@ -158,62 +239,88 @@ export async function waitForFixtureOwnerExit(owner, timeoutMs, {
158
239
  }
159
240
  }
160
241
 
161
- function fixtureErrorPayload(output) {
162
- const lines = String(output || '').split(/\r?\n/);
163
- let start = null, standalone = false, quoted = false, escaped = false;
164
- const stack = [];
165
- for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
166
- const line = lines[lineIndex];
167
- for (let offset = 0; offset < line.length; offset += 1) {
168
- const char = line[offset];
169
- if (start === null) {
170
- if (char !== '{' && char !== '[') continue;
171
- start = lineIndex;
172
- standalone = line.slice(0, offset).trim() === '';
173
- }
174
- if (quoted) {
175
- if (escaped) escaped = false;
176
- else if (char === '\\') escaped = true;
177
- else if (char === '"') quoted = false;
178
- continue;
179
- }
180
- if (char === '"') quoted = true;
181
- else if (char === '{' || char === '[') stack.push(char);
182
- else if (char === '}' || char === ']') {
183
- if (stack.pop() !== (char === '}' ? '{' : '[')) return null;
184
- if (stack.length) continue;
185
- // Consume arrays and prefixed JSON blocks as a unit, so their nested
186
- // objects can never masquerade as independent CLI error documents.
187
- if (standalone && line.slice(offset + 1).trim() === '') {
188
- try {
189
- const parsed = JSON.parse(lines.slice(start, lineIndex + 1).join('\n'));
190
- if (parsed && !Array.isArray(parsed) && parsed.error &&
191
- typeof parsed.error === 'object' && !Array.isArray(parsed.error)) return parsed;
192
- } catch {}
193
- }
194
- start = null;
195
- }
196
- }
197
- }
198
- return null;
199
- }
200
-
201
- export function fixtureDownResult(result) {
202
- // Public --json failures share stderr with Node warnings. Accept only whole
203
- // independent JSON objects; ordinary output never enters the diagnostic.
204
- const payload = fixtureErrorPayload(result.stdout) || fixtureErrorPayload(result.stderr);
242
+ function fixtureCliFailurePayload(output) {
243
+ if (typeof output !== 'string' || Buffer.byteLength(output) > 64 * 1024) return null;
244
+ const text = output.trim();
245
+ const parse = value => {
246
+ try {
247
+ const payload = JSON.parse(value);
248
+ return payload?.ok === false && !Array.isArray(payload) && payload.error && typeof payload.error === 'object' &&
249
+ !Array.isArray(payload.error) ? payload : null;
250
+ } catch { return null; }
251
+ };
252
+ const direct = parse(text);
253
+ if (direct) return direct;
254
+ // Node warnings can precede or follow the CLI's complete JSON document.
255
+ // Strip only bounded, recognized edge lines; never scan arbitrary logs for
256
+ // inner objects, which could turn a nested error into the CLI failure.
257
+ const lines = text.split(/\r?\n/);
258
+ let first = 0, last = lines.length, warningLines = 0, warningBytes = 0;
259
+ const warning = line =>
260
+ /^\(node:\d+\) (?:\[[A-Z][A-Z0-9_]*\] )?(?:[A-Za-z]+Warning|Warning):/.test(line) ||
261
+ /^\(Use (?:`node --trace-(?:warnings|deprecation) \.\.\.`|node --trace-(?:warnings|deprecation)(?: \.\.\.)?) to show where the warning was created\)$/.test(line);
262
+ const strip = line => {
263
+ const trimmed = line.trim();
264
+ if (trimmed && !warning(trimmed)) return false;
265
+ if (trimmed) warningLines += 1;
266
+ warningBytes += Buffer.byteLength(line) + 1;
267
+ return true;
268
+ };
269
+ while (first < last && strip(lines[first])) first += 1;
270
+ while (first < last && strip(lines[last - 1])) last -= 1;
271
+ if (warningLines > 8 || warningBytes > 4096) return null;
272
+ return parse(lines.slice(first, last).join('\n'));
273
+ }
274
+
275
+ function fixtureCliResultParts(result) {
276
+ // --json failures are written with console.error; stdout is retained as a
277
+ // compatibility fallback for fixtures. Raw output never enters diagnostics.
278
+ const stderrFailure = fixtureCliFailurePayload(result.stderr);
279
+ const stdoutFailure = fixtureCliFailurePayload(result.stdout);
280
+ const payload = stderrFailure || stdoutFailure;
205
281
  const extra = payload?.error?.extra || payload?.error || {};
206
282
  const code = payload?.error?.code || result.error?.code;
207
- const numeric = ['pid', 'elapsedMs', 'timeoutMs'];
208
- return {
283
+ const diagnostic = {
209
284
  status: Number.isSafeInteger(result.status) ? result.status : null,
210
- signal: /^SIG[A-Z0-9]{1,12}$/.test(result.signal || '') ? result.signal : null,
285
+ signal: typeof result.signal === 'string' && Object.hasOwn(os.constants.signals, result.signal) ? result.signal : null,
211
286
  code: typeof code === 'string' && /^[A-Z][A-Z0-9_]{0,79}$/.test(code) ? code : null,
212
- extra: Object.fromEntries(numeric.filter(key => typeof extra[key] === 'number' &&
213
- Number.isFinite(extra[key]) && extra[key] >= 0).map(key => [key, extra[key]]))
287
+ source: stderrFailure ? 'stderr' : stdoutFailure ? 'stdout' : result.error?.code ? 'spawn' : null,
288
+ stdoutBytes: typeof result.stdout === 'string' ? Buffer.byteLength(result.stdout) : 0,
289
+ stderrBytes: typeof result.stderr === 'string' ? Buffer.byteLength(result.stderr) : 0
290
+ };
291
+ return { diagnostic, extra };
292
+ }
293
+
294
+ export function fixtureDownResult(result) {
295
+ const { diagnostic, extra } = fixtureCliResultParts(result);
296
+ const numeric = ['pid', 'elapsedMs', 'timeoutMs', 'stopElapsedMs', 'confirmationMs'];
297
+ return {
298
+ ...diagnostic,
299
+ extra: {
300
+ ...Object.fromEntries(numeric.filter(key => typeof extra[key] === 'number' &&
301
+ Number.isFinite(extra[key]) && extra[key] >= 0).map(key => [key, extra[key]])),
302
+ ...(['live', 'dead', 'unknown'].includes(extra.state) ? { state: extra.state } : {}),
303
+ ...(['evidence_unavailable', 'exit_unconfirmed', 'pointer_cleanup_failed', 'pointer_changed'].includes(extra.stopPhase)
304
+ ? { stopPhase: extra.stopPhase } : {})
305
+ }
214
306
  };
215
307
  }
216
308
 
309
+ function fixtureCommandResult(result, operation) {
310
+ // Only this explicit fixture operation opts in. Never copy command arguments,
311
+ // error messages, output, URLs or peer identifiers into public annotations.
312
+ if (operation !== 'broadcast' || !result || typeof result !== 'object') return null;
313
+ const { diagnostic, extra } = fixtureCliResultParts(result);
314
+ const context = Object.fromEntries(['elapsedMs', 'timeoutMs'].filter(key =>
315
+ Number.isSafeInteger(extra[key]) && extra[key] >= 0).map(key => [key, extra[key]]));
316
+ if (Number.isSafeInteger(extra.status) && extra.status >= 100 && extra.status <= 599) context.status = extra.status;
317
+ if (extra.method === 'GET' || extra.method === 'POST') context.method = extra.method;
318
+ const route = typeof extra.path === 'string' ? extra.path.split(/[?#]/, 1)[0] : '';
319
+ if (route === '/api/sessions') context.path = route;
320
+ else if (/^\/api\/sessions\/[^/?#]+\/input$/.test(route)) context.path = '/api/sessions/:peer/input';
321
+ return Object.freeze({ operation, ...diagnostic, extra: Object.freeze(context) });
322
+ }
323
+
217
324
  export function fixtureStopEnvironment(base) {
218
325
  const local = { ...base, HCC_RUNTIME_LOCAL_ONLY: '1' };
219
326
  delete local.HCC_RUNTIME_URL;
@@ -239,6 +346,13 @@ function rememberRuntimePid(pid) {
239
346
  function collectRuntimeDiagnostic(extra = {}) {
240
347
  const diagnostic = { stage: currentStage, pid: runtimePid,
241
348
  owner: observeFixtureOwner(runtimeOwner), pointerExists: fs.existsSync(path.join(root, '.hello-cc', 'runtime.json')), ...extra };
349
+ let logFd;
350
+ try {
351
+ logFd = fs.openSync(path.join(root, '.hello-cc', 'web.log'), 'r');
352
+ const size = fs.fstatSync(logFd).size, bytes = Buffer.alloc(Math.min(size, 65536));
353
+ fs.readSync(logFd, bytes, 0, bytes.length, Math.max(0, size - bytes.length));
354
+ diagnostic.shutdownPhases = parseShutdownDiagnostics(bytes.toString('utf8'), runtimePid);
355
+ } catch {} finally { if (logFd !== undefined) fs.closeSync(logFd); }
242
356
  try {
243
357
  const db = new DatabaseSync(path.join(root, '.hello-cc', 'mesh.db'), { readOnly: true, timeout: 200 });
244
358
  try { diagnostic.events = db.prepare('SELECT id, type FROM events ORDER BY id DESC LIMIT 12').all(); }
@@ -260,6 +374,10 @@ function canonicalPath(value) {
260
374
  catch { return path.resolve(value); }
261
375
  }
262
376
 
377
+ export function isRegressionEntry(entry = process.argv[1], filename = import.meta.filename) {
378
+ return Boolean(entry) && canonicalPath(entry) === canonicalPath(filename);
379
+ }
380
+
263
381
  function samePath(a, b) {
264
382
  return canonicalPath(a) === canonicalPath(b);
265
383
  }
@@ -296,7 +414,7 @@ function run(command, args, options = {}) {
296
414
  });
297
415
  if (result.status !== 0) {
298
416
  const output = [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
299
- fail(`${commandText(command, args)} failed${output ? `\n${output}` : ''}`);
417
+ fail(`${commandText(command, args)} failed${output ? `\n${output}` : ''}`, result, options.diagnosticOperation);
300
418
  }
301
419
  return result.stdout || '';
302
420
  }
@@ -1601,11 +1719,13 @@ function currentRuntimeUrl(route, params = {}) {
1601
1719
  return runtimeUrl(currentRuntime(), route, params);
1602
1720
  }
1603
1721
 
1604
- function runtimeFetch(route, options = {}, params = {}) {
1605
- const runtime = currentRuntime();
1606
- const headers = { ...(options.headers || {}), 'X-HCC-API-Version': '2' };
1607
- if (runtime.token) headers.Authorization = `Bearer ${runtime.token}`;
1608
- return fetch(runtimeUrl(runtime, route, params), { ...options, headers });
1722
+ export function runtimeFetch(route, options = {}, params = {}) {
1723
+ return withFixtureHttpDiagnostic(route, options, () => {
1724
+ const runtime = currentRuntime();
1725
+ const headers = { ...(options.headers || {}), 'X-HCC-API-Version': '2' };
1726
+ if (runtime.token) headers.Authorization = `Bearer ${runtime.token}`;
1727
+ return fetch(runtimeUrl(runtime, route, params), { ...options, headers });
1728
+ });
1609
1729
  }
1610
1730
 
1611
1731
  function directTlsRequest(runtime, route, options = {}) {
@@ -1613,6 +1733,7 @@ function directTlsRequest(runtime, route, options = {}) {
1613
1733
  const body = options.body === undefined || options.body === null ? null : String(options.body);
1614
1734
  return new Promise((resolve, reject) => {
1615
1735
  const req = https.request({
1736
+ agent: false,
1616
1737
  method: options.method || 'GET',
1617
1738
  hostname: url.hostname,
1618
1739
  port: url.port || 443,
@@ -1700,17 +1821,37 @@ async function waitForFile(file, expected, label = file) {
1700
1821
  ensureFile(file, expected);
1701
1822
  }
1702
1823
 
1703
- async function waitForFileContent(file, expected, label = file) {
1704
- const deadline = Date.now() + 10000;
1824
+ async function waitForFileContent(file, expected, label = file, { diagnostics = null, ownedReentryOutput = false } = {}) {
1825
+ const startedAt = Date.now();
1826
+ const deadline = startedAt + 10000;
1705
1827
  let actual = '(missing)';
1828
+ let lastReadAt = null;
1706
1829
  while (Date.now() < deadline) {
1707
1830
  if (fs.existsSync(file)) {
1831
+ lastReadAt = Date.now();
1708
1832
  actual = fs.readFileSync(file, 'utf8').trim();
1709
1833
  if (actual === expected) break;
1710
1834
  }
1711
1835
  await sleep(100);
1712
1836
  }
1837
+ let finalObservation = null;
1838
+ if (actual !== expected && ownedReentryOutput) {
1839
+ finalObservation = readOwnedReentryOutputAtDeadline(file, expected, deadline, Date.now());
1840
+ if (finalObservation.matched) actual = expected;
1841
+ }
1713
1842
  if (actual !== expected) {
1843
+ if (diagnostics) {
1844
+ const detail = JSON.stringify({ ...diagnostics(), wait: {
1845
+ started_at_ms: startedAt, deadline_ms: deadline, last_read_at_ms: lastReadAt,
1846
+ final_observation: finalObservation
1847
+ } });
1848
+ if (process.env.GITHUB_ACTIONS === 'true') {
1849
+ const escaped = detail.replace(/%/g, '%25').replace(/\r/g, '%0D').replace(/\n/g, '%0A');
1850
+ process.stderr.write(`::error title=Shim regression diagnostic::${escaped}\n`);
1851
+ } else {
1852
+ process.stderr.write(`Shim regression diagnostic: ${detail}\n`);
1853
+ }
1854
+ }
1714
1855
  fail(`timed out waiting for ${label}\nexpected: ${expected}\nactual: ${actual}`);
1715
1856
  }
1716
1857
  ensureFile(file, expected);
@@ -1751,9 +1892,9 @@ function assertHtmlCsp(response, html, label) {
1751
1892
  ? "default-src 'self'; " +
1752
1893
  `script-src 'self' 'nonce-${nonce}'; ` +
1753
1894
  "style-src 'self' 'unsafe-inline'; " +
1754
- "img-src 'self' data:; " +
1895
+ "img-src 'self' data: blob:; " +
1755
1896
  "connect-src 'self' ws: wss:; " +
1756
- "object-src 'none'; base-uri 'none'; frame-ancestors 'none'"
1897
+ "object-src 'none'; base-uri 'none'; frame-ancestors 'none'; frame-src 'self' blob:"
1757
1898
  : '';
1758
1899
  if (!nonce || policy !== expected) {
1759
1900
  fail(`${label} missing complete nonce CSP: ${policy || '(missing)'}`);
@@ -1775,34 +1916,10 @@ async function waitForProcessExit(pid, label, timeoutMs = 5000) {
1775
1916
  }
1776
1917
 
1777
1918
  async function expectWebSocketMarker(peer, marker) {
1778
- await new Promise((resolve, reject) => {
1779
- let sawSnapshot = false;
1780
- let sawMarker = false;
1781
- const ws = trackTerminalControl(new WebSocket(runtimeWsUrl(peer), runtimeWsOptions()));
1782
- const timer = setTimeout(() => {
1783
- try { ws.terminate(); } catch {}
1784
- reject(new Error(`${peer} websocket timeout`));
1785
- }, TERMINAL_WEBSOCKET_TIMEOUT_MS);
1786
- ws.on('message', (raw) => {
1787
- const msg = JSON.parse(String(raw));
1788
- if (msg.type === 'snapshot') {
1789
- sawSnapshot = true;
1790
- claimTerminalControl(ws).then(() => ws.send(JSON.stringify({
1791
- type: 'input', data: `echo ${marker}\r`, action_token: ws.hccActionToken,
1792
- epoch: ws.hccControl.epoch
1793
- }))).catch(reject);
1794
- }
1795
- if (['snapshot', 'data', 'replace'].includes(msg.type) && String(msg.data || '').includes(marker)) {
1796
- sawMarker = true;
1797
- }
1798
- if (sawSnapshot && sawMarker) {
1799
- clearTimeout(timer);
1800
- releaseTerminalControl(ws);
1801
- ws.close();
1802
- resolve();
1803
- }
1804
- });
1805
- ws.on('error', (err) => { clearTimeout(timer); reject(err); });
1919
+ const ws = trackTerminalControl(new WebSocket(runtimeWsUrl(peer), runtimeWsOptions()));
1920
+ await waitForTerminalMarker(ws, marker, {
1921
+ claim: signal => claimTerminalControl(ws, { signal }),
1922
+ release: () => releaseTerminalControl(ws), timeoutMs: TERMINAL_WEBSOCKET_TIMEOUT_MS
1806
1923
  });
1807
1924
  }
1808
1925
 
@@ -1812,31 +1929,44 @@ function trackTerminalControl(ws) {
1812
1929
  ws.on('message', (raw) => {
1813
1930
  let message;
1814
1931
  try { message = JSON.parse(String(raw)); } catch { return; }
1932
+ if (!message || typeof message !== 'object') return;
1815
1933
  if (message.action_token) ws.hccActionToken = message.action_token;
1816
1934
  if (message.control) ws.hccControl = message.control;
1817
1935
  });
1818
1936
  return ws;
1819
1937
  }
1820
1938
 
1821
- async function claimTerminalControl(ws) {
1939
+ export async function claimTerminalControl(ws, { signal } = {}) {
1940
+ const aborted = () => Object.assign(new Error('terminal control claim aborted'), { code: 'TERMINAL_CONTROL_ABORTED' });
1941
+ if (signal?.aborted) throw aborted();
1822
1942
  if (ws.hccControl?.can_control) return ws.hccControl;
1823
1943
  return new Promise((resolve, reject) => {
1824
1944
  const timer = setTimeout(() => finish(new Error('terminal control claim timeout')), 5000);
1945
+ let settled = false;
1825
1946
  const finish = (error = null) => {
1947
+ if (settled) return;
1948
+ settled = true;
1826
1949
  clearTimeout(timer);
1827
1950
  ws.off('message', onMessage);
1951
+ signal?.removeEventListener('abort', onAbort);
1828
1952
  if (error) reject(error);
1829
1953
  else resolve(ws.hccControl);
1830
1954
  };
1831
1955
  const onMessage = (raw) => {
1956
+ if (settled) return;
1832
1957
  let message;
1833
1958
  try { message = JSON.parse(String(raw)); } catch { return; }
1959
+ if (!message || typeof message !== 'object') return;
1834
1960
  if (message.type === 'error') finish(new Error(`terminal control claim failed: ${JSON.stringify(message.error)}`));
1835
1961
  else if (message.type === 'control' && message.control?.can_control) finish();
1836
1962
  };
1963
+ const onAbort = () => finish(aborted());
1837
1964
  ws.on('message', onMessage);
1838
- ws.send(JSON.stringify({ type: 'control', action: 'claim', action_token: ws.hccActionToken,
1839
- epoch: ws.hccControl?.epoch, force: true }));
1965
+ signal?.addEventListener('abort', onAbort, { once: true });
1966
+ try {
1967
+ ws.send(JSON.stringify({ type: 'control', action: 'claim', action_token: ws.hccActionToken,
1968
+ epoch: ws.hccControl?.epoch, force: true }));
1969
+ } catch (error) { finish(error); }
1840
1970
  });
1841
1971
  }
1842
1972
 
@@ -1949,24 +2079,60 @@ async function fetchTerminalSnapshot(peer, params = {}) {
1949
2079
  });
1950
2080
  }
1951
2081
 
1952
- async function issueBrowserSessionCookie() {
2082
+ function browserSessionCookie(setCookie, endpoint, { expired = false } = {}) {
2083
+ const url = new URL(endpoint);
2084
+ const protocol = url.protocol.slice(0, -1);
2085
+ const effectivePort = url.port || (protocol === 'https' ? '443' : '80');
2086
+ const expectedName = `hcc_sid_v2_${protocol}_${effectivePort}`;
2087
+ const match = String(setCookie).match(/^([^=;\s]+)=([^;]*)/);
2088
+ if (!match || match[1] !== expectedName || (expired ? match[2] !== '' : !match[2])) {
2089
+ fail(`browser session cookie did not match endpoint scope ${expectedName} (expired=${expired})`);
2090
+ }
2091
+ return { name: match[1], sid: match[2], cookie: `${match[1]}=${match[2]}` };
2092
+ }
2093
+
2094
+ async function selectCookieProject(baseUrl, cookie, projectRoot = root, {
2095
+ signal, origin = new URL(baseUrl).origin, headers = {}
2096
+ } = {}) {
2097
+ const url = new URL('/api/projects/select', baseUrl);
2098
+ url.searchParams.set('root', projectRoot);
2099
+ const response = await fetch(url, {
2100
+ method: 'POST',
2101
+ headers: {
2102
+ ...headers,
2103
+ Cookie: cookie,
2104
+ Origin: origin,
2105
+ 'X-HCC-API-Version': '2'
2106
+ },
2107
+ signal
2108
+ });
2109
+ const selected = await response.json();
2110
+ if (!response.ok || !selected.project_identity) {
2111
+ fail(`cookie project selection failed: status=${response.status} body=${JSON.stringify(selected)}`);
2112
+ }
2113
+ return selected.project_identity;
2114
+ }
2115
+
2116
+ async function issueBrowserSessionCookie({ signal, selectProject = true } = {}) {
1953
2117
  const runtime = currentRuntime();
1954
2118
  const baseUrl = runtime.base_url || `http://127.0.0.1:${port}`;
1955
2119
  const response = await fetch(new URL('/login', baseUrl), {
1956
2120
  method: 'POST',
1957
2121
  headers: { 'Content-Type': 'application/json' },
1958
2122
  body: JSON.stringify({ token: runtime.token || '' }),
2123
+ signal,
1959
2124
  redirect: 'manual'
1960
2125
  });
1961
2126
  const setCookie = response.headers.get('set-cookie') || '';
1962
- const sid = setCookie.match(/hcc_sid=([^;]+)/)?.[1] || '';
1963
- if (response.status !== 302 || !sid) {
1964
- fail(`browser session login failed: status=${response.status} cookie=${setCookie}`);
2127
+ if (response.status !== 302) {
2128
+ fail(`browser session login failed: status=${response.status}`);
1965
2129
  }
1966
- return { baseUrl, origin: new URL(baseUrl).origin, sid };
2130
+ const auth = browserSessionCookie(setCookie, baseUrl);
2131
+ const projectIdentity = selectProject ? await selectCookieProject(baseUrl, auth.cookie, root, { signal }) : null;
2132
+ return { baseUrl, origin: new URL(baseUrl).origin, ...auth, projectIdentity };
1967
2133
  }
1968
2134
 
1969
- async function cookieRuntimeFetch(route, auth, options = {}, params = {}) {
2135
+ export async function cookieRuntimeFetch(route, auth, options = {}, params = {}) {
1970
2136
  const url = new URL(route, auth.baseUrl);
1971
2137
  for (const [key, value] of Object.entries(params)) {
1972
2138
  if (value !== undefined && value !== null) url.searchParams.set(key, String(value));
@@ -1974,9 +2140,10 @@ async function cookieRuntimeFetch(route, auth, options = {}, params = {}) {
1974
2140
  return fetch(url, {
1975
2141
  ...options,
1976
2142
  headers: {
1977
- Cookie: `hcc_sid=${auth.sid}`,
2143
+ Cookie: auth.cookie,
1978
2144
  Origin: auth.origin,
1979
2145
  'X-HCC-API-Version': '2',
2146
+ ...(auth.projectIdentity ? { 'X-HCC-Root-Identity': auth.projectIdentity } : {}),
1980
2147
  ...(options.headers || {})
1981
2148
  }
1982
2149
  });
@@ -1990,8 +2157,9 @@ async function cookieRuntimeFetchWithoutOrigin(route, auth, options = {}, params
1990
2157
  return fetch(url, {
1991
2158
  ...options,
1992
2159
  headers: {
1993
- Cookie: `hcc_sid=${auth.sid}`,
2160
+ Cookie: auth.cookie,
1994
2161
  'X-HCC-API-Version': '2',
2162
+ ...(auth.projectIdentity ? { 'X-HCC-Root-Identity': auth.projectIdentity } : {}),
1995
2163
  ...(options.headers || {})
1996
2164
  }
1997
2165
  });
@@ -2017,9 +2185,10 @@ async function expectSocketMarkerAfter(ws, marker, action) {
2017
2185
  });
2018
2186
  }
2019
2187
 
2020
- async function openCookieTerminalWebSocket(peer, sid, params = {}) {
2188
+ async function openCookieTerminalWebSocket(peer, cookie, params = {}, selectedProjectIdentity = null) {
2021
2189
  const runtime = currentRuntime();
2022
2190
  const baseUrl = runtime.base_url || `http://127.0.0.1:${port}`;
2191
+ const projectIdentity = selectedProjectIdentity || await selectCookieProject(baseUrl, cookie, params.root || root);
2023
2192
  const url = new URL(`/ws/terminal/${encodeURIComponent(peer)}`, baseUrl);
2024
2193
  url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
2025
2194
  url.searchParams.set('api_version', '2');
@@ -2030,8 +2199,9 @@ async function openCookieTerminalWebSocket(peer, sid, params = {}) {
2030
2199
  let settled = false;
2031
2200
  const ws = trackTerminalControl(new WebSocket(url, {
2032
2201
  headers: {
2033
- Cookie: `hcc_sid=${sid}`,
2034
- Origin: new URL(baseUrl).origin
2202
+ Cookie: cookie,
2203
+ Origin: new URL(baseUrl).origin,
2204
+ 'X-HCC-Root-Identity': projectIdentity
2035
2205
  }
2036
2206
  }));
2037
2207
  const timer = setTimeout(() => {
@@ -2039,7 +2209,7 @@ async function openCookieTerminalWebSocket(peer, sid, params = {}) {
2039
2209
  settled = true;
2040
2210
  try { ws.terminate(); } catch {}
2041
2211
  reject(new Error(`${peer} cookie websocket snapshot timeout`));
2042
- }, 5000);
2212
+ }, COOKIE_WEBSOCKET_SNAPSHOT_TIMEOUT_MS);
2043
2213
  const rejectBeforeSnapshot = (err) => {
2044
2214
  if (settled) return;
2045
2215
  settled = true;
@@ -2071,12 +2241,12 @@ async function assertLogoutClosesCookieWebSocket(peer, params = {}) {
2071
2241
  redirect: 'manual'
2072
2242
  });
2073
2243
  const setCookie = login.headers.get('set-cookie') || '';
2074
- const sid = setCookie.match(/hcc_sid=([^;]+)/)?.[1] || '';
2075
- if (login.status !== 302 || !sid) {
2076
- fail(`cookie websocket login failed: status=${login.status} cookie=${setCookie}`);
2244
+ if (login.status !== 302) {
2245
+ fail(`cookie websocket login failed: status=${login.status}`);
2077
2246
  }
2247
+ const auth = browserSessionCookie(setCookie, baseUrl);
2078
2248
 
2079
- const ws = await openCookieTerminalWebSocket(peer, sid, params);
2249
+ const ws = await openCookieTerminalWebSocket(peer, auth.cookie, params);
2080
2250
  try {
2081
2251
  const closed = new Promise((resolve, reject) => {
2082
2252
  const timer = setTimeout(() => reject(new Error(`${peer} cookie websocket logout close timeout`)), 5000);
@@ -2093,7 +2263,7 @@ async function assertLogoutClosesCookieWebSocket(peer, params = {}) {
2093
2263
  fetch(new URL('/logout', baseUrl), {
2094
2264
  method: 'POST',
2095
2265
  headers: {
2096
- Cookie: `hcc_sid=${sid}`,
2266
+ Cookie: auth.cookie,
2097
2267
  Origin: origin
2098
2268
  }
2099
2269
  }),
@@ -2102,11 +2272,12 @@ async function assertLogoutClosesCookieWebSocket(peer, params = {}) {
2102
2272
  if (logout.status !== 204 || !(logout.headers.get('set-cookie') || '').includes('Max-Age=0')) {
2103
2273
  fail(`cookie websocket logout did not expire its session: status=${logout.status}`);
2104
2274
  }
2275
+ browserSessionCookie(logout.headers.get('set-cookie') || '', baseUrl, { expired: true });
2105
2276
  if (closeResult.code !== 4001 || ws.readyState !== WebSocket.CLOSED) {
2106
2277
  fail(`cookie websocket logout did not close the established socket with 4001:\n${JSON.stringify({ closeResult, readyState: ws.readyState }, null, 2)}`);
2107
2278
  }
2108
2279
  const revoked = await fetch(new URL('/api/runtime', baseUrl), {
2109
- headers: { Cookie: `hcc_sid=${sid}`, 'X-HCC-API-Version': '2' }
2280
+ headers: { Cookie: auth.cookie, 'X-HCC-API-Version': '2' }
2110
2281
  });
2111
2282
  if (revoked.status !== 401) {
2112
2283
  fail(`cookie websocket logout left the old cookie authorized: ${revoked.status}`);
@@ -2120,7 +2291,7 @@ async function assertLogoutClosesCookieWebSocket(peer, params = {}) {
2120
2291
 
2121
2292
  async function assertEvictionClosesCookieWebSocket(peer, params = {}) {
2122
2293
  const auth = await issueBrowserSessionCookie();
2123
- const ws = await openCookieTerminalWebSocket(peer, auth.sid, params);
2294
+ const ws = await openCookieTerminalWebSocket(peer, auth.cookie, params, auth.projectIdentity);
2124
2295
  try {
2125
2296
  const closed = new Promise((resolve, reject) => {
2126
2297
  const timer = setTimeout(() => reject(new Error(`${peer} cookie websocket eviction close timeout`)), 10000);
@@ -2134,14 +2305,14 @@ async function assertEvictionClosesCookieWebSocket(peer, params = {}) {
2134
2305
  });
2135
2306
  });
2136
2307
  for (let offset = 0; offset < 256; offset += 32) {
2137
- await Promise.all(Array.from({ length: 32 }, () => issueBrowserSessionCookie()));
2308
+ await Promise.all(Array.from({ length: 32 }, () => issueBrowserSessionCookie({ selectProject: false })));
2138
2309
  }
2139
2310
  const closeResult = await closed;
2140
2311
  if (closeResult.code !== 4001 || !closeResult.reason.includes('session limit reached')) {
2141
2312
  fail(`cookie websocket eviction did not close with 4001:\n${JSON.stringify(closeResult, null, 2)}`);
2142
2313
  }
2143
2314
  const revoked = await fetch(new URL('/api/runtime', auth.baseUrl), {
2144
- headers: { Cookie: `hcc_sid=${auth.sid}`, 'X-HCC-API-Version': '2' }
2315
+ headers: { Cookie: auth.cookie, 'X-HCC-API-Version': '2' }
2145
2316
  });
2146
2317
  if (revoked.status !== 401) fail(`evicted cookie remained authorized: ${revoked.status}`);
2147
2318
  } finally {
@@ -2621,7 +2792,7 @@ async function assertShimIgnoresGlobalRuntime(generateShim) {
2621
2792
  started_at: Math.floor(Date.now() / 1000),
2622
2793
  global_runtime: true
2623
2794
  };
2624
- fs.writeFileSync(path.join(fakeHome, '.hello-cc', 'runtime.json'), JSON.stringify(globalRuntime, null, 2));
2795
+ fs.writeFileSync(path.join(fakeHome, '.hello-cc', 'runtime.json'), JSON.stringify(globalRuntime, null, 2), { mode: 0o600 });
2625
2796
 
2626
2797
  const hccWrapper = path.join(testDir, 'hcc-wrapper');
2627
2798
  fs.writeFileSync(hccWrapper, `#!/usr/bin/env bash\nexec ${sh(process.execPath)} ${sh(hccBin)} "$@"\n`, { mode: 0o755 });
@@ -2731,7 +2902,7 @@ async function cookieSessionExpiryWorkflow() {
2731
2902
  env: {
2732
2903
  ...env,
2733
2904
  HCC_REGRESSION_TEST: '1',
2734
- HCC_REGRESSION_WEB_SESSION_TTL_SEC: '1'
2905
+ HCC_REGRESSION_WEB_SESSION_TTL_SEC: String(COOKIE_EXPIRY_FIXTURE_TTL_SEC)
2735
2906
  }
2736
2907
  });
2737
2908
  try {
@@ -2757,40 +2928,56 @@ async function cookieSessionExpiryWorkflow() {
2757
2928
  }
2758
2929
 
2759
2930
  const auth = await issueBrowserSessionCookie();
2760
- ws = await openCookieTerminalWebSocket(expirySessionId, auth.sid, { root });
2931
+ ws = await openCookieTerminalWebSocket(expirySessionId, auth.cookie, { root }, auth.projectIdentity);
2761
2932
  if (!ws.hccActionToken) fail('short-TTL cookie terminal snapshot omitted its action token');
2762
- const closed = new Promise((resolve, reject) => {
2763
- const timer = setTimeout(() => reject(new Error('expired cookie websocket close timeout')), 5000);
2933
+ const expiryWindow = createCookieExpiryWindow(COOKIE_EXPIRY_FIXTURE_TTL_SEC * 1000);
2934
+ // Listen before waiting so spontaneous expiry is retained. Start the close
2935
+ // deadline only after expiry, and defer rejection to avoid an unhandled
2936
+ // error while this fixture is still waiting for the cookie to expire.
2937
+ const closed = new Promise(resolve => {
2764
2938
  ws.once('close', (code, reason) => {
2765
- clearTimeout(timer);
2766
2939
  resolve({ code, reason: String(reason || '') });
2767
2940
  });
2768
- ws.once('error', (err) => {
2769
- clearTimeout(timer);
2770
- reject(err);
2771
- });
2941
+ ws.once('error', error => resolve({ error }));
2772
2942
  });
2773
2943
 
2774
- await sleep(1200);
2944
+ // A probe-only cookie is issued after the target cookie. Observing its
2945
+ // expiry must not revoke the target socket before its input guard runs.
2946
+ const probeAuth = await expiryWindow.prepare(signal => issueBrowserSessionCookie({ signal }));
2947
+ // Cookie expiry uses the server's wall clock, which can advance more slowly
2948
+ // than this fixture's timer. Observe real expiry within the original total
2949
+ // TTL + close budget before sending input on this same socket.
2950
+ await expiryWindow.waitForHttpExpiry(signal =>
2951
+ cookieRuntimeFetch('/api/runtime', probeAuth, { signal }));
2952
+ if (ws.readyState !== WebSocket.OPEN) fail('target cookie websocket closed before the expired-input check');
2775
2953
  ws.send(JSON.stringify({
2776
2954
  type: 'input',
2777
2955
  data: `echo ${marker}\r`,
2778
2956
  action_token: ws.hccActionToken,
2779
2957
  epoch: ws.hccControl.epoch
2780
2958
  }));
2781
- const closeResult = await closed;
2782
- if (closeResult.code !== 4001 || !closeResult.reason.includes('session expired')) {
2783
- fail(`expired cookie websocket did not close with 4001/session expired:\n${JSON.stringify(closeResult, null, 2)}`);
2784
- }
2785
-
2959
+ let closeError = null;
2960
+ const closeResult = await expiryWindow.waitForClose(closed).catch(error => {
2961
+ closeError = error; return null;
2962
+ });
2963
+ // Inspect the HTTP and input boundaries even if the close observation
2964
+ // times out; a missing close event must not hide accepted terminal input.
2786
2965
  const expiredHttp = await cookieRuntimeFetch('/api/runtime', auth);
2966
+ const snapshot = await fetchTerminalSnapshot(expirySessionId, { root });
2967
+ const inputRejected = !snapshot.includes(marker);
2787
2968
  if (expiredHttp.status !== 401) {
2788
2969
  fail(`expired browser cookie remained authorized over HTTP: ${expiredHttp.status}`);
2789
2970
  }
2790
- const snapshot = await fetchTerminalSnapshot(expirySessionId, { root });
2791
- if (snapshot.includes(marker)) {
2971
+ if (!inputRejected) {
2792
2972
  fail(`expired browser cookie executed terminal input after expiry:\n${snapshot}`);
2793
2973
  }
2974
+ if (closeError) {
2975
+ fail(`expired cookie websocket close failed: code=${closeError.code || 'CLOSE_FAILED'} httpStatus=${expiredHttp.status} inputRejected=${inputRejected}`);
2976
+ }
2977
+ if (closeResult.error) throw closeResult.error;
2978
+ if (closeResult.code !== 4001 || !closeResult.reason.includes('session expired')) {
2979
+ fail(`expired cookie websocket did not close with 4001/session expired:\n${JSON.stringify(closeResult, null, 2)}`);
2980
+ }
2794
2981
  } finally {
2795
2982
  if (ws && ws.readyState !== WebSocket.CLOSED) {
2796
2983
  try { ws.terminate(); } catch {}
@@ -2829,8 +3016,8 @@ async function webSecretRedactionWorkflow() {
2829
3016
  body: JSON.stringify({ token: secret }),
2830
3017
  redirect: 'manual'
2831
3018
  });
2832
- const sid = (login.headers.get('set-cookie') || '').match(/hcc_sid=([^;]+)/)?.[1] || '';
2833
- if (login.status !== 302 || !sid) fail(`redaction login failed with status ${login.status}`);
3019
+ if (login.status !== 302) fail(`redaction login failed with status ${login.status}`);
3020
+ const auth = browserSessionCookie(login.headers.get('set-cookie') || '', runtime.base_url);
2834
3021
 
2835
3022
  const create = await runtimeFetch('/api/sessions', {
2836
3023
  method: 'POST',
@@ -2844,7 +3031,7 @@ async function webSecretRedactionWorkflow() {
2844
3031
  })
2845
3032
  }, { root });
2846
3033
  if (!create.ok) fail(`redaction PTY create failed with status ${create.status}`);
2847
- terminalWs = await openCookieTerminalWebSocket(sessionId, sid, { root });
3034
+ terminalWs = await openCookieTerminalWebSocket(sessionId, auth.cookie, { root });
2848
3035
  terminalWs.send(JSON.stringify({ type: 'resize', cols: 90, rows: 28,
2849
3036
  action_token: terminalWs.hccActionToken, epoch: terminalWs.hccControl.epoch }));
2850
3037
 
@@ -2861,7 +3048,7 @@ async function webSecretRedactionWorkflow() {
2861
3048
 
2862
3049
  const invalidCookie = await fetch(new URL('/api/runtime', runtime.base_url), {
2863
3050
  headers: {
2864
- Cookie: `hcc_sid=${secret}`,
3051
+ Cookie: `${auth.name}=${secret}`,
2865
3052
  'X-HCC-API-Version': '2'
2866
3053
  }
2867
3054
  });
@@ -2940,12 +3127,15 @@ async function assertWebWrapperParentSurvives() {
2940
3127
  }
2941
3128
  }
2942
3129
 
2943
- async function stopRuntime() {
3130
+ async function stopRuntime({ peerStops = [] } = {}) {
2944
3131
  if (!runtimePid) return;
2945
3132
  const owner = runtimeOwner;
2946
3133
  const before = observeFixtureOwner(owner);
3134
+ const downStarted = performance.now();
2947
3135
  const down = fixtureDownResult(stopFixtureCli());
2948
- lastStopDiagnostic = { before, down, stack: new Error('stopRuntime').stack.split('\n').slice(1, 7), signals: [] };
3136
+ const downDurationMs = Math.round(performance.now() - downStarted);
3137
+ const afterDown = observeFixtureOwner(owner);
3138
+ lastStopDiagnostic = { before, down, downDurationMs, afterDown, peerStops, stack: new Error('stopRuntime').stack.split('\n').slice(1, 7), signals: [] };
2949
3139
  try {
2950
3140
  await waitForFixtureOwnerExit(owner, 5000);
2951
3141
  } catch (err) {
@@ -3174,14 +3364,14 @@ async function setupRegression() {
3174
3364
  { headers: { Accept: 'text/html' } }
3175
3365
  );
3176
3366
  const directTlsSetCookie = String(directTlsExchange.headers['set-cookie']?.[0] || '');
3177
- const directTlsSid = directTlsSetCookie.match(/hcc_sid=([^;]+)/)?.[1] || '';
3178
- if (directTlsExchange.status !== 302 || !directTlsSid || !directTlsSetCookie.includes('Secure')) {
3179
- fail(`direct TLS login did not issue a Secure cookie: status=${directTlsExchange.status} cookie=${directTlsSetCookie}`);
3367
+ if (directTlsExchange.status !== 302 || !directTlsSetCookie.includes('Secure')) {
3368
+ fail(`direct TLS login did not issue a Secure cookie: status=${directTlsExchange.status}`);
3180
3369
  }
3370
+ const directTlsAuth = browserSessionCookie(directTlsSetCookie, directTlsRuntime.base_url);
3181
3371
  const directTlsLogout = await directTlsRequest(directTlsRuntime, '/logout', {
3182
3372
  method: 'POST',
3183
3373
  headers: {
3184
- Cookie: `hcc_sid=${directTlsSid}`,
3374
+ Cookie: directTlsAuth.cookie,
3185
3375
  Origin: new URL(directTlsRuntime.base_url).origin
3186
3376
  }
3187
3377
  });
@@ -3189,8 +3379,9 @@ async function setupRegression() {
3189
3379
  if (directTlsLogout.status !== 204 ||
3190
3380
  !directTlsLogoutCookie.includes('Max-Age=0') ||
3191
3381
  !directTlsLogoutCookie.includes('Secure')) {
3192
- fail(`direct TLS logout did not expire a Secure cookie: status=${directTlsLogout.status} cookie=${directTlsLogoutCookie}`);
3382
+ fail(`direct TLS logout did not expire a Secure cookie: status=${directTlsLogout.status}`);
3193
3383
  }
3384
+ browserSessionCookie(directTlsLogoutCookie, directTlsRuntime.base_url, { expired: true });
3194
3385
  await stopRuntime();
3195
3386
 
3196
3387
  await assertWebWrapperParentSurvives();
@@ -3298,26 +3489,28 @@ async function setupRegression() {
3298
3489
  });
3299
3490
  if (exchange.status !== 302) fail(`browser-nav ?token did not exchange to a cookie redirect: ${exchange.status}`);
3300
3491
  const setCookie = exchange.headers.get('set-cookie') || '';
3301
- if (!setCookie.includes('hcc_sid=') || !setCookie.includes('HttpOnly') || !setCookie.includes('SameSite=Lax')) {
3302
- fail(`exchange did not set an HttpOnly SameSite cookie: ${setCookie}`);
3303
- }
3304
- if (setCookie.includes('Secure')) fail(`plaintext LAN exchange issued a Secure cookie: ${setCookie}`);
3305
- const sidMatch = setCookie.match(/hcc_sid=([^;]+)/);
3306
- if (!sidMatch) fail(`exchange did not return a session id: ${setCookie}`);
3307
- const sid = sidMatch[1];
3308
- // (b) API with the session cookie → 200
3309
- const withCookie = await fetch(`${baseUrl}/api/runtime`, { headers: { Cookie: `hcc_sid=${sid}`, 'X-HCC-API-Version': '2' } });
3492
+ if (!setCookie.includes('HttpOnly') || !setCookie.includes('SameSite=Lax')) {
3493
+ fail('exchange did not set an HttpOnly SameSite cookie');
3494
+ }
3495
+ if (setCookie.includes('Secure')) fail('plaintext LAN exchange issued a Secure cookie');
3496
+ const browserAuth = browserSessionCookie(setCookie, baseUrl);
3497
+ // (b) API with the session cookie and explicitly selected project → 200
3498
+ const cookieProjectIdentity = await selectCookieProject(baseUrl, browserAuth.cookie);
3499
+ const withCookie = await fetch(`${baseUrl}/api/runtime`, {
3500
+ headers: { Cookie: browserAuth.cookie, 'X-HCC-API-Version': '2', 'X-HCC-Root-Identity': cookieProjectIdentity }
3501
+ });
3310
3502
  if (!withCookie.ok) fail(`API with session cookie required auth: ${withCookie.status}`);
3311
3503
  // (c) API with a bogus session cookie → 401
3312
- const bogusCookie = await fetch(`${baseUrl}/api/runtime`, { headers: { Cookie: 'hcc_sid=bogus', 'X-HCC-API-Version': '2' } });
3504
+ const bogusCookie = await fetch(`${baseUrl}/api/runtime`, { headers: { Cookie: `${browserAuth.name}=bogus`, 'X-HCC-API-Version': '2' } });
3313
3505
  if (bogusCookie.status !== 401) fail(`API with bogus session cookie did not 401: ${bogusCookie.status}`);
3314
3506
  // (d) cookie-authenticated writes require an exact same-origin Origin header.
3315
3507
  const crossOriginCookieWrite = await fetch(`${baseUrl}/api/csrf-probe`, {
3316
3508
  method: 'POST',
3317
3509
  headers: {
3318
- Cookie: `hcc_sid=${sid}`,
3510
+ Cookie: browserAuth.cookie,
3319
3511
  Origin: `http://127.0.0.1:${port + 1}`,
3320
3512
  'X-HCC-API-Version': '2',
3513
+ 'X-HCC-Root-Identity': cookieProjectIdentity,
3321
3514
  'Content-Type': 'application/json'
3322
3515
  },
3323
3516
  body: '{}'
@@ -3332,9 +3525,10 @@ async function setupRegression() {
3332
3525
  const sameOriginCookieWrite = await fetch(`${baseUrl}/api/csrf-probe`, {
3333
3526
  method: 'POST',
3334
3527
  headers: {
3335
- Cookie: `hcc_sid=${sid}`,
3528
+ Cookie: browserAuth.cookie,
3336
3529
  Origin: new URL(baseUrl).origin,
3337
3530
  'X-HCC-API-Version': '2',
3531
+ 'X-HCC-Root-Identity': cookieProjectIdentity,
3338
3532
  'Content-Type': 'application/json'
3339
3533
  },
3340
3534
  body: '{}'
@@ -3359,23 +3553,25 @@ async function setupRegression() {
3359
3553
  body: JSON.stringify({ token: cookieToken }),
3360
3554
  redirect: 'manual'
3361
3555
  });
3362
- if (loginPost.status !== 302 || !(loginPost.headers.get('set-cookie') || '').includes('hcc_sid=')) {
3556
+ if (loginPost.status !== 302) {
3363
3557
  fail(`POST /login did not issue a session cookie: ${loginPost.status}`);
3364
3558
  }
3559
+ browserSessionCookie(loginPost.headers.get('set-cookie') || '', baseUrl);
3365
3560
 
3366
3561
  // (h) logout revokes the opaque session and expires the browser cookie.
3367
3562
  const logout = await fetch(`${baseUrl}/logout`, {
3368
3563
  method: 'POST',
3369
3564
  headers: {
3370
- Cookie: `hcc_sid=${sid}`,
3565
+ Cookie: browserAuth.cookie,
3371
3566
  Origin: new URL(baseUrl).origin
3372
3567
  }
3373
3568
  });
3374
3569
  const logoutCookie = logout.headers.get('set-cookie') || '';
3375
- if (logout.status !== 204 || !logoutCookie.includes('hcc_sid=') || !logoutCookie.includes('Max-Age=0')) {
3376
- fail(`logout did not revoke and expire the session cookie: status=${logout.status} cookie=${logoutCookie}`);
3570
+ if (logout.status !== 204 || !logoutCookie.includes('Max-Age=0')) {
3571
+ fail(`logout did not revoke and expire the session cookie: status=${logout.status}`);
3377
3572
  }
3378
- const revokedCookie = await fetch(`${baseUrl}/api/runtime`, { headers: { Cookie: `hcc_sid=${sid}`, 'X-HCC-API-Version': '2' } });
3573
+ browserSessionCookie(logoutCookie, baseUrl, { expired: true });
3574
+ const revokedCookie = await fetch(`${baseUrl}/api/runtime`, { headers: { Cookie: browserAuth.cookie, 'X-HCC-API-Version': '2' } });
3379
3575
  if (revokedCookie.status !== 401) fail(`logout left the old session cookie authorized: ${revokedCookie.status}`);
3380
3576
 
3381
3577
  // (i) trusted loopback reverse-proxy headers mark issued and expired cookies
@@ -3404,14 +3600,24 @@ async function setupRegression() {
3404
3600
  redirect: 'manual'
3405
3601
  });
3406
3602
  const proxySetCookie = proxyExchange.headers.get('set-cookie') || '';
3407
- const proxySid = proxySetCookie.match(/hcc_sid=([^;]+)/)?.[1] || '';
3408
- if (proxyExchange.status !== 302 || !proxySid || !proxySetCookie.includes('Secure')) {
3409
- fail(`trusted proxy login did not issue a Secure cookie: status=${proxyExchange.status} cookie=${proxySetCookie}`);
3603
+ if (proxyExchange.status !== 302 || !proxySetCookie.includes('Secure')) {
3604
+ fail(`trusted proxy login did not issue a Secure cookie: status=${proxyExchange.status}`);
3410
3605
  }
3606
+ const proxyAuth = browserSessionCookie(proxySetCookie, proxyOrigin);
3607
+ const proxyProjectIdentity = await selectCookieProject(baseUrl, proxyAuth.cookie, root, {
3608
+ origin: proxyOrigin, headers: proxyHeaders
3609
+ });
3610
+ const withProxyCookie = await fetch(`${baseUrl}/api/runtime`, {
3611
+ headers: {
3612
+ ...proxyHeaders, Cookie: proxyAuth.cookie, 'X-HCC-API-Version': '2',
3613
+ 'X-HCC-Root-Identity': proxyProjectIdentity
3614
+ }
3615
+ });
3616
+ if (!withProxyCookie.ok) fail(`trusted proxy API rejected its session cookie: ${withProxyCookie.status}`);
3411
3617
  const proxyLogout = await fetch(`${baseUrl}/logout`, {
3412
3618
  method: 'POST',
3413
3619
  headers: {
3414
- Cookie: `hcc_sid=${proxySid}`,
3620
+ Cookie: proxyAuth.cookie,
3415
3621
  Origin: proxyOrigin,
3416
3622
  'X-Forwarded-Host': 'public.example.test:9443',
3417
3623
  'X-Forwarded-Proto': 'https'
@@ -3419,9 +3625,12 @@ async function setupRegression() {
3419
3625
  });
3420
3626
  const proxyLogoutCookie = proxyLogout.headers.get('set-cookie') || '';
3421
3627
  if (proxyLogout.status !== 204 || !proxyLogoutCookie.includes('Max-Age=0') || !proxyLogoutCookie.includes('Secure')) {
3422
- fail(`trusted proxy logout did not expire a Secure cookie: status=${proxyLogout.status} cookie=${proxyLogoutCookie}`);
3628
+ fail(`trusted proxy logout did not expire a Secure cookie: status=${proxyLogout.status}`);
3423
3629
  }
3424
- const revokedProxyCookie = await fetch(`${baseUrl}/api/runtime`, { headers: { Cookie: `hcc_sid=${proxySid}`, 'X-HCC-API-Version': '2' } });
3630
+ browserSessionCookie(proxyLogoutCookie, proxyOrigin, { expired: true });
3631
+ const revokedProxyCookie = await fetch(`${baseUrl}/api/runtime`, {
3632
+ headers: { ...proxyHeaders, Cookie: proxyAuth.cookie, 'X-HCC-API-Version': '2' }
3633
+ });
3425
3634
  if (revokedProxyCookie.status !== 401) fail(`trusted proxy logout left the old cookie authorized: ${revokedProxyCookie.status}`);
3426
3635
 
3427
3636
  // A forwarded authority other than the pinned public origin is untrusted.
@@ -3436,7 +3645,7 @@ async function setupRegression() {
3436
3645
  });
3437
3646
  const defaultPortProxySetCookie = defaultPortProxyExchange.headers.get('set-cookie') || '';
3438
3647
  if (defaultPortProxyExchange.status !== 403 || defaultPortProxySetCookie) {
3439
- fail(`unpinned proxy authority received a session: status=${defaultPortProxyExchange.status} cookie=${defaultPortProxySetCookie}`);
3648
+ fail(`unpinned proxy authority received a session: status=${defaultPortProxyExchange.status} setCookie=${Boolean(defaultPortProxySetCookie)}`);
3440
3649
  }
3441
3650
  const unpinnedProxyLogin = await fetch(`${baseUrl}/login`, {
3442
3651
  method: 'POST',
@@ -3461,7 +3670,7 @@ async function setupRegression() {
3461
3670
  });
3462
3671
  const spoofedLanCookie = spoofedLanExchange.headers.get('set-cookie') || '';
3463
3672
  if (spoofedLanExchange.status !== 403 || spoofedLanCookie) {
3464
- fail(`non-loopback forwarded spoof received a session: status=${spoofedLanExchange.status} cookie=${spoofedLanCookie}`);
3673
+ fail(`non-loopback forwarded spoof received a session: status=${spoofedLanExchange.status} setCookie=${Boolean(spoofedLanCookie)}`);
3465
3674
  }
3466
3675
  } else {
3467
3676
  log('non-loopback forwarded spoof: no non-loopback IPv4 interface; focused boundary coverage retained');
@@ -4606,16 +4815,24 @@ async function multiProjectWebWorkflow() {
4606
4815
  fail(`detected API did not return liveness metadata:\n${JSON.stringify(detectedJson, null, 2)}`);
4607
4816
  }
4608
4817
 
4609
- const htmlResponse = await fetch(currentRuntimeUrl('/'));
4610
- const html = await htmlResponse.text();
4611
- const startForm = html.match(/<form\b[^>]*\bid="startForm"[^>]*>[\s\S]*?<\/form>/)?.[0];
4612
- if (!startForm) fail('web form missing startForm');
4613
- // Approval details may display the working directory. The simplified launch
4614
- // form must still derive it from the selected project instead of an input.
4615
- if (startForm.includes('Working directory') || /<input\b[^>]*(?:id|name)="(?:cwd|workdir)"/.test(startForm)) {
4616
- fail('web launch form still exposes Working directory');
4617
- }
4618
- for (const forbidden of ['Alias optional', 'Role tag', 'Command<input', 'commandbar', 'lineInput', 'Send text to active terminal']) {
4818
+ const htmlResponse = await withFixtureHttpDiagnostic('/', {}, () => fetch(currentRuntimeUrl('/')));
4819
+ const pageHtml = await htmlResponse.text();
4820
+ if (!pageHtml.includes('type="module" src="/assets/web/browser/core.mjs"')) fail('web page missing ESM bootstrap');
4821
+ const moduleSources = [];
4822
+ for (const asset of ['browser/core.mjs', 'ui-agent-start.mjs']) {
4823
+ const response = await runtimeFetch('/assets/web/' + asset);
4824
+ if (!response.ok || !response.headers.get('content-type')?.includes('javascript')) fail('web ESM asset did not load: ' + asset);
4825
+ moduleSources.push(await response.text());
4826
+ }
4827
+ const html = pageHtml + '\n' + moduleSources.join('\n');
4828
+ const startFormHtml = html.match(/<form\b[^>]*\bid="startForm"[^>]*>[\s\S]*?<\/form>/)?.[0];
4829
+ if (!startFormHtml) fail('web form missing simplified session form');
4830
+ // A current-directory label in an approval or session details is valid;
4831
+ // obsolete creation inputs must be checked inside the creation form.
4832
+ for (const forbidden of ['Alias optional', 'Role tag', 'Command<input']) {
4833
+ if (startFormHtml.includes(forbidden)) fail(`web form still exposes ${forbidden}`);
4834
+ }
4835
+ for (const forbidden of ['commandbar', 'lineInput', 'Send text to active terminal']) {
4619
4836
  if (html.includes(forbidden)) fail(`web form still exposes ${forbidden}`);
4620
4837
  }
4621
4838
  for (const expected of [
@@ -4623,6 +4840,7 @@ async function multiProjectWebWorkflow() {
4623
4840
  'id="projectPath"',
4624
4841
  'id="addProjectBtn"',
4625
4842
  'id="startForm"',
4843
+ 'id="agentCwd"',
4626
4844
  'id="kind"',
4627
4845
  'id="sessionKindFilter"',
4628
4846
  'id="sessions"'
@@ -4666,7 +4884,7 @@ async function multiProjectWebWorkflow() {
4666
4884
  !html.includes("fetch('/logout', { method: 'POST', headers })")) {
4667
4885
  fail('web UI missing logout control or session revocation request');
4668
4886
  }
4669
- if (!html.includes('id="startMode"') || !html.includes('id="resumeArg"') || !html.includes('syncStartModeOptions') || !html.includes("mode === 'resume'")) {
4887
+ if (!html.includes('id="startMode"') || !html.includes('id="resumeArg"') || !html.includes('syncStartModeOptions') || !html.includes("mode.value === 'resume'")) {
4670
4888
  fail('web form missing provider resume controls');
4671
4889
  }
4672
4890
  for (const expected of [
@@ -4921,7 +5139,7 @@ async function multiProjectWebWorkflow() {
4921
5139
  fail(`same-origin cookie administrator create failed or leaked its action token:\n${JSON.stringify(cookieCreated, null, 2)}`);
4922
5140
  }
4923
5141
 
4924
- const cookieAdminWs = await openCookieTerminalWebSocket(cookieAdminId, cookieAdmin.sid, { root });
5142
+ const cookieAdminWs = await openCookieTerminalWebSocket(cookieAdminId, cookieAdmin.cookie, { root }, cookieAdmin.projectIdentity);
4925
5143
  try {
4926
5144
  if (!cookieAdminWs.hccActionToken) fail('cookie administrator terminal snapshot omitted its action token');
4927
5145
  const inputRoute = `/api/sessions/${encodeURIComponent(cookieAdminId)}/input`;
@@ -5733,7 +5951,7 @@ async function bufferGcArbitrationWorkflow() {
5733
5951
 
5734
5952
  const liveId = `gc-live-external-${testId}`;
5735
5953
  const liveFiles = ['out', 'in', 'resize', 'meta'].map((suffix) => path.join(rootBufs, `${liveId}.${suffix}`));
5736
- for (const file of liveFiles.slice(0, 3)) fs.writeFileSync(file, file.endsWith('.out') ? 'live\n' : '');
5954
+ for (const file of liveFiles.slice(0, 3)) fs.writeFileSync(file, file.endsWith('.out') ? 'live\n' : '', { mode: 0o600 });
5737
5955
  const identity = inspectProcessIdentity(process.pid).identity;
5738
5956
  fs.writeFileSync(liveFiles[3], JSON.stringify({
5739
5957
  id: liveId,
@@ -5747,7 +5965,7 @@ async function bufferGcArbitrationWorkflow() {
5747
5965
  wrapper_identity: identity,
5748
5966
  cols: 120,
5749
5967
  rows: 40
5750
- }));
5968
+ }), { mode: 0o600 });
5751
5969
  await waitFor(async () => {
5752
5970
  const data = await (await runtimeFetch('/api/sessions', {}, { root })).json();
5753
5971
  return (data.sessions || []).some((session) => session.id === liveId);
@@ -5756,8 +5974,8 @@ async function bufferGcArbitrationWorkflow() {
5756
5974
 
5757
5975
  const legacyId = `gc-legacy-${testId}`;
5758
5976
  const legacyFiles = ['out', 'in', 'resize', 'meta'].map((suffix) => path.join(rootBufs, `${legacyId}.${suffix}`));
5759
- for (const file of legacyFiles.slice(0, 3)) fs.writeFileSync(file, '');
5760
- fs.writeFileSync(legacyFiles[3], JSON.stringify({ id: legacyId, pid: process.pid, wrapper_pid: process.pid }));
5977
+ for (const file of legacyFiles.slice(0, 3)) fs.writeFileSync(file, '', { mode: 0o600 });
5978
+ fs.writeFileSync(legacyFiles[3], JSON.stringify({ id: legacyId, pid: process.pid, wrapper_pid: process.pid }), { mode: 0o600 });
5761
5979
  for (const file of legacyFiles) fs.utimesSync(file, oldTime, oldTime);
5762
5980
  await waitFor(async () => {
5763
5981
  const data = await (await runtimeFetch('/api/sessions', {}, { root })).json();
@@ -5767,7 +5985,7 @@ async function bufferGcArbitrationWorkflow() {
5767
5985
  const siblingLiveId = `gc-sibling-external-${testId}`;
5768
5986
  const siblingLiveFiles = ['out', 'in', 'resize', 'meta']
5769
5987
  .map((suffix) => path.join(siblingBufs, `${siblingLiveId}.${suffix}`));
5770
- for (const file of siblingLiveFiles.slice(0, 3)) fs.writeFileSync(file, '');
5988
+ for (const file of siblingLiveFiles.slice(0, 3)) fs.writeFileSync(file, '', { mode: 0o600 });
5771
5989
  fs.writeFileSync(siblingLiveFiles[3], JSON.stringify({
5772
5990
  id: siblingLiveId,
5773
5991
  kind: 'shell',
@@ -5780,7 +5998,7 @@ async function bufferGcArbitrationWorkflow() {
5780
5998
  wrapper_identity: identity,
5781
5999
  cols: 120,
5782
6000
  rows: 40
5783
- }));
6001
+ }), { mode: 0o600 });
5784
6002
  await waitFor(async () => {
5785
6003
  const data = await (await runtimeFetch('/api/sessions', {}, { root: secondProjectRoot })).json();
5786
6004
  return (data.sessions || []).some((session) => session.id === siblingLiveId);
@@ -6079,7 +6297,7 @@ async function bufferGcArbitrationWorkflow() {
6079
6297
  base_url: server?.baseUrl || 'http://127.0.0.1:1',
6080
6298
  token: 'regression-token'
6081
6299
  };
6082
- fs.writeFileSync(path.join(isolatedRoot, '.hello-cc', 'runtime.json'), JSON.stringify(pointer));
6300
+ fs.writeFileSync(path.join(isolatedRoot, '.hello-cc', 'runtime.json'), JSON.stringify(pointer), { mode: 0o600 });
6083
6301
  const gc = run(process.execPath, [hccBin, '--root', isolatedRoot, '--json', 'gc', '--older-than', '0', '--yes'], { env: isolatedEnv });
6084
6302
  const payload = JSON.parse(gc);
6085
6303
  if (!fs.existsSync(orphan) || Number(payload.data?.deferred_buf_files || 0) < 1) {
@@ -6437,13 +6655,53 @@ async function shimTmuxWorkflow() {
6437
6655
  cwd: root,
6438
6656
  env: { ...shimEnv, HCC_REG_VALUE: 'shim-second' }
6439
6657
  });
6440
- parsePane(restarted);
6658
+ const restartedPane = parsePane(restarted);
6441
6659
  const secondEnvFile = path.join(outDir, 'shim-env-second');
6442
6660
  hcc(['inject', peer, `printf '%s\\n' "$HCC_REG_VALUE" > ${secondEnvFile}`]);
6443
6661
  await waitForFile(secondEnvFile, 'shim-second', 'shim env restart');
6444
6662
  const reentryFile = path.join(outDir, 'shim-reentry');
6445
- hcc(['inject', peer, `HCC_FAKE_STAY_ALIVE=0 HCC_REG_VALUE=shim-third ${sh(shim)} --resume shim-regression-session > ${sh(reentryFile)} 2>&1`]);
6446
- await waitForFileContent(reentryFile, 'fake-claude --resume shim-regression-session', 'shim tmux pane re-entry');
6663
+ const reentryTrace = path.join(outDir, 'shim-reentry-stages');
6664
+ const reentryStatus = path.join(outDir, 'shim-reentry-status');
6665
+ const reentryProvider = path.join(outDir, 'shim-reentry-provider');
6666
+ const reentryProbe = pathToFileURL(path.join(repoRoot, 'scripts', 'shim-reentry-probe.mjs')).href;
6667
+ const reentryEventId = withMeshDb(db => db.prepare('SELECT MAX(id) AS id FROM events').get().id || 0);
6668
+ const reentryStarted = Date.now();
6669
+ hcc(['inject', peer, `printf 'started\\n' > ${sh(reentryStatus)}; HCC_FAKE_STAY_ALIVE=0 HCC_REG_VALUE=shim-third HCC_FAKE_LOG=${sh(reentryProvider)} HCC_REGRESSION_REENTRY_TRACE=${sh(reentryTrace)} HCC_REGRESSION_REENTRY_BIN=${sh(hccBin)} NODE_OPTIONS="\${NODE_OPTIONS:+\${NODE_OPTIONS} }"${sh(`--import=${reentryProbe}`)} ${sh(shim)} --resume shim-regression-session > ${sh(reentryFile)} 2>&1; printf '%s\\n' "$?" >> ${sh(reentryStatus)}`]);
6670
+ await waitForFileContent(reentryFile, 'fake-claude --resume shim-regression-session', 'shim tmux pane re-entry', {
6671
+ ownedReentryOutput: true,
6672
+ diagnostics: () => {
6673
+ // Query only this fixture's pane and fixed event types. Full terminal
6674
+ // captures, process arguments, request bodies and environment are excluded.
6675
+ const paneResult = spawnSync('tmux', ['display-message', '-p', '-t', restartedPane,
6676
+ '#{pane_id}|#{pane_dead}|#{pane_current_command}'], {
6677
+ env, encoding: 'utf8', timeout: 1000, maxBuffer: 1024, stdio: ['ignore', 'pipe', 'ignore']
6678
+ });
6679
+ const paneParts = (paneResult.stdout || '').trim().split('|');
6680
+ const pane = {
6681
+ found: paneResult.status === 0 && /^%\d+$/.test(paneParts[0] || ''),
6682
+ dead: paneParts[1] === '1',
6683
+ command: /^[A-Za-z0-9_.-]{1,40}$/.test(paneParts[2] || '') ? paneParts[2] : 'unknown'
6684
+ };
6685
+ let status = [];
6686
+ try { status = fs.readFileSync(reentryStatus, 'utf8').slice(0, 32).trim().split('\n'); } catch {}
6687
+ let events = [];
6688
+ let db;
6689
+ try {
6690
+ db = new DatabaseSync(path.join(root, '.hello-cc', 'mesh.db'), { timeout: 250, readOnly: true });
6691
+ events = db.prepare(`SELECT type, created_at FROM events WHERE id > ? AND actor = ?
6692
+ AND type IN ('peer.attach.requested', 'tmux.session.attached', 'tmux.session.detached', 'tmux.session.exited')
6693
+ ORDER BY id DESC LIMIT 8`).all(reentryEventId, peer);
6694
+ } catch { events = [{ state: 'unavailable' }]; }
6695
+ finally { if (db) db.close(); }
6696
+ return {
6697
+ elapsed_ms: Date.now() - reentryStarted,
6698
+ shell_started: status[0] === 'started',
6699
+ shell_exit: /^\d{1,3}$/.test(status[1] || '') ? Number(status[1]) : null,
6700
+ provider_entered: fs.existsSync(reentryProvider),
6701
+ pane, stages: readReentryTrace(reentryTrace), events
6702
+ };
6703
+ }
6704
+ });
6447
6705
  hcc(['peer', 'stop', peer]);
6448
6706
 
6449
6707
  const exitedResume = 'shim-exited-session';
@@ -6563,15 +6821,18 @@ async function askBroadcastWorkflow() {
6563
6821
  }
6564
6822
  await waitForFile(dispatchFile, 'DISPATCH_OK', 'task dispatch injection');
6565
6823
  const broadcastFile = path.join(outDir, 'broadcast-ok');
6566
- hcc(['broadcast', `echo BROADCAST_OK > ${broadcastFile}`, '--from', 'human', '--inject']);
6824
+ hccJson(['broadcast', `echo BROADCAST_OK > ${broadcastFile}`, '--from', 'human', '--inject'], { diagnosticOperation: 'broadcast' });
6567
6825
  await waitForFile(broadcastFile, 'BROADCAST_OK', 'broadcast injection');
6568
6826
  }
6569
6827
 
6570
6828
  async function downGcPackWorkflow() {
6571
6829
  log('[9/13] down/gc/pack');
6572
- hccMaybe(['peer', 'stop', 'shell-a']);
6573
- hccMaybe(['peer', 'stop', 'shell-b']);
6574
- await stopRuntime();
6830
+ const peerStops = ['shell-a', 'shell-b'].map(peer => {
6831
+ const started = performance.now();
6832
+ const result = fixtureDownResult(hccMaybe(['--json', 'peer', 'stop', peer]));
6833
+ return { ...result, durationMs: Math.round(performance.now() - started) };
6834
+ });
6835
+ await stopRuntime({ peerStops });
6575
6836
  await waitFor(() => !fs.existsSync(path.join(root, '.hello-cc', 'runtime.json')), 'runtime cleanup', 5000);
6576
6837
  hcc(['gc', '--older-than', '0', '--yes']);
6577
6838
  const pack = JSON.parse(run('npm', ['pack', '--dry-run', '--json']));
@@ -6820,7 +7081,7 @@ async function syntaxAndHelp() {
6820
7081
  const integrationShimScriptSource = fs.readFileSync(path.join(repoRoot, 'lib', 'integrations', 'shims', 'script.mjs'), 'utf8');
6821
7082
  const shellPathSource = fs.readFileSync(path.join(repoRoot, 'lib', 'shell-path.mjs'), 'utf8');
6822
7083
  const webPeerActionsSource = fs.readFileSync(path.join(repoRoot, 'lib', 'web', 'peer-actions.mjs'), 'utf8');
6823
- const webUiTemplateSource = fs.readFileSync(path.join(repoRoot, 'lib', 'web', 'ui-template.mjs'), 'utf8');
7084
+ const webUiTemplateSource = ['ui-template.mjs', 'browser/core.mjs'].map(name => fs.readFileSync(path.join(repoRoot, 'lib', 'web', name), 'utf8')).join('\n');
6824
7085
  const tmuxSafetySource = fs.readFileSync(path.join(repoRoot, 'lib', 'core', 'peers', 'tmux-safety.mjs'), 'utf8');
6825
7086
  // cmdWeb moved whole to lib/web/runtime-main.mjs; subsystem modules extend it
6826
7087
  const cmdWebSource = [
@@ -6884,15 +7145,15 @@ async function syntaxAndHelp() {
6884
7145
  'sameResolvedPath(global.root, ctx.root)',
6885
7146
  'sameResolvedPath(global.db, ctx.dbPath)',
6886
7147
  'async function stopOrphanWebRuntimes(',
6887
- 'await stopOrphanWebRuntimes(ctx, existing.pid || null);',
6888
- 'await stopOrphanWebRuntimes(ctx);'
7148
+ 'await stopOrphanWebRuntimes(boundCtx, existing.pid || null);',
7149
+ 'await stopOrphanWebRuntimes(boundCtx);'
6889
7150
  ]) {
6890
7151
  if (!webStartupSource.includes(expected)) fail(`web startup guard missing: ${expected}`);
6891
7152
  }
6892
7153
  for (const expected of [
6893
7154
  'function listTmuxPanesOnce()',
6894
7155
  'let autoAttachScanInFlight = false;',
6895
- 'if (autoAttachScanInFlight) return;',
7156
+ 'if (isStopping() || autoAttachScanInFlight) return;',
6896
7157
  'const paneByPid = new Map();',
6897
7158
  'const attached = attachedTmuxState(ctx, db);',
6898
7159
  'function killOldTmuxForRebind(',
@@ -7252,7 +7513,7 @@ async function syntaxAndHelp() {
7252
7513
  !hccSource.includes("} from '../web/http.mjs'") ||
7253
7514
  !hccSource.includes("import * as webUiTemplate from '../web/ui-template.mjs'") ||
7254
7515
  !hccSource.includes('const VERSION = PACKAGE_META.version') ||
7255
- !hccSource.includes('writeGuidanceForRoot(ctx.root)')) {
7516
+ !hccSource.includes('writeGuidanceForRoot(ctx.root, { expectedSnapshot: ctx.initialRootIdentity })')) {
7256
7517
  fail('CLI still has duplicated package metadata, cli args, DB schema helpers, CLI runtime helpers, coordination state helpers, format helpers, runtime paths/state helpers, runtime client helpers, project context helpers, handoff helpers, timeline helpers, task liveness helpers, automation helpers, state render helpers, help text helpers, message store helpers, task store helpers, task CLI helpers, session launch helpers, provider command helpers, peer binding helpers, tmux helpers, lock helpers, team planning helpers, peer identity helpers, project registry helpers, web runtime/HTTP/UI helpers, or guidance wiring');
7257
7518
  }
7258
7519
  for (const expected of [
@@ -7740,7 +8001,7 @@ async function syntaxAndHelp() {
7740
8001
  fail('runtime state clearRuntime did not remove the reused-pid global pointer');
7741
8002
  }
7742
8003
 
7743
- fs.writeFileSync(globalRuntimeFile, '{bad');
8004
+ fs.writeFileSync(globalRuntimeFile, '{bad', { mode: 0o600 });
7744
8005
  if (runtimeState.readGlobalRuntimeFile() !== null) {
7745
8006
  fail('runtime state readGlobalRuntimeFile did not reject invalid JSON');
7746
8007
  }
@@ -9688,7 +9949,7 @@ async function syntaxAndHelp() {
9688
9949
  process.env.HOME = tlsHome;
9689
9950
  const webTls = await import(path.join(repoRoot, 'lib', 'web', 'tls.mjs'));
9690
9951
  tlsCredentials = webTls.ensureSelfSignedCert();
9691
- const tlsDir = path.join(tlsHome, '.hello-cc', 'tls');
9952
+ const tlsDir = path.join(fs.realpathSync(tlsHome), '.hello-cc', 'tls');
9692
9953
  const currentPointerPath = path.join(tlsDir, 'current.json');
9693
9954
  const currentGeneration = JSON.parse(fs.readFileSync(currentPointerPath, 'utf8')).generation;
9694
9955
  const generationNames = {
@@ -9724,27 +9985,28 @@ async function syntaxAndHelp() {
9724
9985
  }
9725
9986
  const originalTlsCert = tlsCredentials.cert;
9726
9987
  const originalTlsCertPath = tlsCredentials.certPath;
9727
- const originalReadFileSync = fs.readFileSync;
9728
- let pointerReads = 0;
9988
+ const originalLstatSync = fs.lstatSync;
9989
+ let pointerChecks = 0;
9729
9990
  let pointerSwitchedBeforeDelete = false;
9730
- fs.readFileSync = (file, ...args) => {
9991
+ fs.lstatSync = (file, ...args) => {
9731
9992
  if (path.resolve(String(file)) === path.resolve(currentPointerPath)) {
9732
- pointerReads += 1;
9733
- // Read 1 validates the old current; read 2 starts cleanup; read 3 is
9734
- // the deletion-time TOCTOU guard for the stale switched-current row.
9735
- if (pointerReads === 3) {
9993
+ pointerChecks += 1;
9994
+ // The private reader lstat-checks before and after each descriptor
9995
+ // read. Checks 1-2 validate the old current; 3-4 start cleanup; check
9996
+ // 5 is the deletion-time guard for the stale switched-current row.
9997
+ if (pointerChecks === 5) {
9736
9998
  const nextPointerPath = path.join(tlsDir, `.current-regression-${testId}.tmp`);
9737
9999
  fs.writeFileSync(nextPointerPath, `${JSON.stringify({ generation: generationNames.switchedCurrent })}\n`, { mode: 0o600 });
9738
10000
  fs.renameSync(nextPointerPath, currentPointerPath);
9739
10001
  pointerSwitchedBeforeDelete = true;
9740
10002
  }
9741
10003
  }
9742
- return originalReadFileSync(file, ...args);
10004
+ return originalLstatSync(file, ...args);
9743
10005
  };
9744
10006
  try {
9745
10007
  tlsCredentials = webTls.ensureSelfSignedCert();
9746
10008
  } finally {
9747
- fs.readFileSync = originalReadFileSync;
10009
+ fs.lstatSync = originalLstatSync;
9748
10010
  }
9749
10011
  const publishedGeneration = JSON.parse(fs.readFileSync(currentPointerPath, 'utf8')).generation;
9750
10012
  const remainingGenerations = fs.readdirSync(tlsDir, { withFileTypes: true })
@@ -9760,14 +10022,14 @@ async function syntaxAndHelp() {
9760
10022
  ].sort();
9761
10023
  if (tlsCredentials.cert !== originalTlsCert ||
9762
10024
  tlsCredentials.certPath !== originalTlsCertPath ||
9763
- !pointerSwitchedBeforeDelete || pointerReads < 4 ||
10025
+ !pointerSwitchedBeforeDelete || pointerChecks < 6 ||
9764
10026
  publishedGeneration !== generationNames.switchedCurrent ||
9765
10027
  !fs.existsSync(path.join(tlsDir, generationNames.initialCurrent, '.published')) ||
9766
10028
  !fs.existsSync(path.join(tlsDir, generationNames.switchedCurrent, '.published')) ||
9767
10029
  !fs.existsSync(path.join(tlsDir, generationNames.activeCreating, '.creating')) ||
9768
10030
  fs.existsSync(path.join(tlsDir, generationNames.deadCreating)) ||
9769
10031
  JSON.stringify(remainingGenerations) !== JSON.stringify(expectedRemainingGenerations)) {
9770
- fail(`TLS generation cleanup violated current/previous/candidate lifecycle protection:\n${JSON.stringify({ generationNames, pointerReads, pointerSwitchedBeforeDelete, publishedGeneration, remainingGenerations }, null, 2)}`);
10032
+ fail(`TLS generation cleanup violated current/previous/candidate lifecycle protection:\n${JSON.stringify({ generationNames, pointerChecks, pointerSwitchedBeforeDelete, publishedGeneration, remainingGenerations }, null, 2)}`);
9771
10033
  }
9772
10034
  } finally {
9773
10035
  if (savedTlsHome === undefined) delete process.env.HOME;
@@ -10328,9 +10590,9 @@ async function processEvidenceWorkflow() {
10328
10590
  const legacyProcess = spawn('sleep', ['30'], { stdio: 'ignore' });
10329
10591
  const legacyFiles = ['out', 'in', 'resize', 'meta']
10330
10592
  .map((suffix) => path.join(externalDir, `${legacyExternalId}.${suffix}`));
10331
- fs.writeFileSync(legacyFiles[0], 'legacy external\n');
10332
- fs.writeFileSync(legacyFiles[1], '');
10333
- fs.writeFileSync(legacyFiles[2], '');
10593
+ fs.writeFileSync(legacyFiles[0], 'legacy external\n', { mode: 0o600 });
10594
+ fs.writeFileSync(legacyFiles[1], '', { mode: 0o600 });
10595
+ fs.writeFileSync(legacyFiles[2], '', { mode: 0o600 });
10334
10596
  fs.writeFileSync(legacyFiles[3], JSON.stringify({
10335
10597
  id: legacyExternalId,
10336
10598
  kind: 'shell',
@@ -10341,7 +10603,7 @@ async function processEvidenceWorkflow() {
10341
10603
  wrapper_pid: legacyProcess.pid,
10342
10604
  cols: 120,
10343
10605
  rows: 40
10344
- }));
10606
+ }), { mode: 0o600 });
10345
10607
  await waitFor(async () => {
10346
10608
  const data = await (await runtimeFetch('/api/sessions', {}, { root })).json();
10347
10609
  return (data.sessions || []).some((session) => session.id === legacyExternalId);
@@ -11387,7 +11649,7 @@ async function main() {
11387
11649
  log('FULL_REGRESSION_OK');
11388
11650
  }
11389
11651
 
11390
- if (process.argv[1] && path.resolve(process.argv[1]) === import.meta.filename) main().catch((err) => {
11652
+ if (isRegressionEntry()) main().catch((err) => {
11391
11653
  const failure = fixtureFailureDiagnostic(err);
11392
11654
  const diagnostic = collectRuntimeDiagnostic({ failure, stop: lastStopDiagnostic });
11393
11655
  process.stderr.write(`[regression failure diagnostic] ${diagnostic}\n`);