@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
@@ -0,0 +1,193 @@
1
+ // Opt-in short real-provider checks. Every model sees only generated nonces.
2
+ // No existing session is resumed and no tools or repository writes are requested.
3
+ import assert from 'node:assert/strict';
4
+ import fs from 'node:fs';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import { randomUUID, createHash } from 'node:crypto';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { execFileSync } from 'node:child_process';
10
+ import { setTimeout as delay } from 'node:timers/promises';
11
+ import { createEventHelpers } from '../lib/db/events.mjs';
12
+ import { createConnectionHelpers } from '../lib/db/connection.mjs';
13
+ import { createPeerBindingStore } from '../lib/db/stores/peers.mjs';
14
+ import { createPeerHelpers } from '../lib/core/peers/peer-helpers.mjs';
15
+ import { createMessageStore } from '../lib/core/coordination/messages.mjs';
16
+ import { createEvidenceRuntime } from '../lib/core/peers/evidence-runtime.mjs';
17
+ import { detectBranch } from '../lib/runtime/project-context.mjs';
18
+ import { createNativeAdapter } from '../lib/integrations/native/index.mjs';
19
+ import { startNativeService } from '../lib/runtime/native/service.mjs';
20
+ import { nativeRequest } from '../lib/runtime/native/client.mjs';
21
+ import { readNativePointer } from '../lib/runtime/native/store.mjs';
22
+ import { redactSecrets } from '../lib/shared/redact.mjs';
23
+
24
+ const args = process.argv.slice(2);
25
+ if (!args.includes('--run-live') || args.includes('--help')) {
26
+ console.log('Usage: node scripts/native-bounded-live-acceptance.mjs --run-live [--provider codex|claude|dsh|all] [--codex-bin PATH] [--dsh-bin PATH] [--claude-package DIR] [--output FILE]');
27
+ console.log('Three short nonce-only turns per provider: first reply, remembered reply, owned close/resume. Existing caller credentials are used only in private temporary provider homes.');
28
+ process.exit(0);
29
+ }
30
+ const option = (name, fallback) => args.includes(name) ? args[args.indexOf(name) + 1] : fallback;
31
+ const selection = option('--provider', 'all');
32
+ const providers = selection === 'all' ? ['codex', 'claude', 'dsh'] : [selection];
33
+ assert.ok(providers.every(value => ['codex', 'claude', 'dsh'].includes(value)), 'unknown provider');
34
+ assert.ok(Number(process.versions.node.split('.')[0]) >= 24, 'Node 24+ required');
35
+ const repo = fileURLToPath(new URL('..', import.meta.url));
36
+ const sandbox = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'hcc-bounded-live-')));
37
+ fs.chmodSync(sandbox, 0o700);
38
+ const privateDir = name => { const dir = path.join(sandbox, name); fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); return dir; };
39
+ const root = privateDir('project'), taskHome = privateDir('home');
40
+ const homes = Object.fromEntries(['codex', 'claude', 'dsh'].map(provider => [provider, privateDir(provider + '-home')]));
41
+ const output = path.resolve(option('--output', path.join(os.tmpdir(), 'hcc-bounded-live-' + randomUUID() + '.json')));
42
+ assert.ok(!output.startsWith(sandbox + path.sep), 'Receipt must be outside the temporary directory, which is removed after validation');
43
+ const hash = file => createHash('sha256').update(fs.readFileSync(file)).digest('hex');
44
+ const originalCodexHome = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
45
+ const protectedFiles = ['config.toml', 'auth.json'].map(name => path.join(originalCodexHome, name)).filter(file => fs.existsSync(file));
46
+ const protectedBefore = Object.fromEntries(protectedFiles.map(file => [file, hash(file)]));
47
+ function sourceHashes() {
48
+ const files = {};
49
+ function walk(relative) {
50
+ const file = path.join(repo, relative);
51
+ if (fs.statSync(file).isDirectory()) for (const name of fs.readdirSync(file).sort()) walk(path.join(relative, name));
52
+ else files[relative] = hash(file);
53
+ }
54
+ for (const relative of ['lib/integrations/native', 'lib/runtime/native', 'scripts/native-bounded-live-acceptance.mjs']) walk(relative);
55
+ return files;
56
+ }
57
+ const report = { startedAt: new Date().toISOString(), node: process.version, sandbox, providers: {}, checks: [], cleanup: {},
58
+ sourceFiles: sourceHashes(), mode: 'Real provider calls through native service and default production adapter loading; no browser or tool invocation claim',
59
+ isolation: 'Private project, HOME and provider homes; only model routing/auth copied; caller account config hashes checked',
60
+ limitations: ['No real approval, interruption, cross-provider tool, published package, installed device or business workflow acceptance is claimed.'] };
61
+ const check = name => { report.checks.push(name); console.log('PASS ' + name); };
62
+ const flush = () => { fs.mkdirSync(path.dirname(output), { recursive: true }); fs.writeFileSync(output, JSON.stringify(redactSecrets(report), null, 2) + '\n', { mode: 0o600 }); };
63
+ const now = () => Math.floor(Date.now() / 1000);
64
+ const events = createEventHelpers({ now }), bindings = createPeerBindingStore({ now, ...events });
65
+ const evidence = createEvidenceRuntime({ now });
66
+ const peers = createPeerHelpers({ now, ...events, ...evidence, detectBranch });
67
+ const messages = createMessageStore({ now, ...events });
68
+ const connections = createConnectionHelpers({ now, ...bindings, redactedLogText: value => redactSecrets(value) });
69
+ const deps = { ...connections, ...events, ...bindings, ...peers, ...messages, ...evidence, detectBranch };
70
+ const ctx = { root, cwd: root, dbPath: path.join(root, '.hello-cc', 'mesh.db') };
71
+ const api = (method, route, body) => nativeRequest(ctx, method, route, body, { timeoutMs: 45000 });
72
+ const observations = new Map();
73
+ let service, db;
74
+ function prepareCodex() {
75
+ const file = path.join(originalCodexHome, 'config.toml');
76
+ if (fs.existsSync(file)) {
77
+ let section = '';
78
+ const filtered = fs.readFileSync(file, 'utf8').split('\n').filter(line => {
79
+ const heading = line.match(/^\s*\[\[?([^\]]+)\]\]?\s*(?:#.*)?$/);
80
+ if (heading) section = heading[1].split('.')[0].replaceAll('"', '').replaceAll("'", '');
81
+ return section ? section === 'model_providers' : /^\s*(?:model(?:_provider|_reasoning_effort|_reasoning_summary|_verbosity|_catalog_json)?|cli_auth_credentials_store)\s*=/.test(line);
82
+ });
83
+ const quote = value => "'" + value.replaceAll("'", "'\\''") + "'";
84
+ let authSection = '', count = 0;
85
+ const config = filtered.map(line => {
86
+ const heading = line.match(/^\s*\[\[?([^\]]+)\]\]?\s*(?:#.*)?$/); if (heading) authSection = heading[1];
87
+ const command = authSection.endsWith('.auth') && line.match(/^\s*command\s*=\s*((?:"(?:[^"\\]|\\.)*"|'[^']*'))\s*(?:#.*)?$/);
88
+ if (!command) return line;
89
+ const original = command[1].startsWith("'") ? command[1].slice(1, -1) : JSON.parse(command[1]);
90
+ const wrapper = path.join(privateDir('credential-commands'), 'command-' + ++count);
91
+ fs.writeFileSync(wrapper, '#!/bin/sh\nexport HOME=' + quote(os.homedir()) + '\nexec ' + quote(original) + ' "$@"\n', { mode: 0o700 });
92
+ return 'command = ' + JSON.stringify(wrapper);
93
+ }).join('\n');
94
+ report.authenticationHelperHomePreserved = count > 0;
95
+ fs.writeFileSync(path.join(homes.codex, 'config.toml'), config + '\n', { mode: 0o600 });
96
+ }
97
+ const auth = path.join(originalCodexHome, 'auth.json');
98
+ if (fs.existsSync(auth)) { fs.copyFileSync(auth, path.join(homes.codex, 'auth.json')); fs.chmodSync(path.join(homes.codex, 'auth.json'), 0o600); }
99
+ }
100
+ async function complete(peer, sent, expected) {
101
+ const deadline = Date.now() + 90000; let notice = Date.now() + 20000;
102
+ while (Date.now() < deadline) {
103
+ const state = await api('GET', '/workers/' + peer + '/state');
104
+ assert.equal(state.snapshot.pendingApprovals?.length || 0, 0, 'Nonce-only turn must not request tools or approval');
105
+ const row = state.deliveries.find(value => value.message_id === sent.message_id);
106
+ if (['completed', 'failed', 'uncertain'].includes(row?.state)) {
107
+ assert.equal(row.state, 'completed', JSON.stringify({ state: row.state, evidence: row.evidence }));
108
+ const reply = db.prepare("SELECT * FROM messages WHERE reply_to=? AND kind='reply'").get(sent.message_id);
109
+ assert.ok(reply, 'successful ask must write a correlated reply');
110
+ assert.equal(reply.thread_id, sent.message_id); assert.equal(reply.body.trim(), expected);
111
+ assert.ok(db.prepare('SELECT read_at FROM message_reads WHERE peer=? AND message_id=?').get(peer, sent.message_id));
112
+ return { messageId: sent.message_id, turnId: row.turn_id, replyId: reply.id, exactNonce: true, acked: true };
113
+ }
114
+ if (Date.now() >= notice) { console.log('WAIT ' + peer + ' short nonce reply'); notice += 20000; }
115
+ await delay(150);
116
+ }
117
+ throw new Error('Timed out waiting for bounded real-provider nonce turn');
118
+ }
119
+ try {
120
+ if (providers.includes('codex')) prepareCodex();
121
+ if (providers.includes('claude') && option('--claude-package')) {
122
+ const installed = fs.realpathSync(option('--claude-package'));
123
+ const pkg = JSON.parse(fs.readFileSync(path.join(installed, 'package.json'), 'utf8'));
124
+ assert.equal(pkg.name, '@anthropic-ai/claude-agent-sdk');
125
+ const scoped = path.join(root, 'node_modules/@anthropic-ai'); fs.mkdirSync(scoped, { recursive: true, mode: 0o700 });
126
+ fs.symlinkSync(installed, path.join(scoped, 'claude-agent-sdk'), 'dir');
127
+ report.claudeSdk = { version: pkg.version, entrySha256: hash(path.join(installed, 'sdk.mjs')), queryInjected: false, resolution: 'Default loader through optional SDK in temporary worker project' };
128
+ }
129
+ db = deps.connect(ctx, { migrateRegistered: false });
130
+ service = await startNativeService(ctx, deps, { adapterFactory: (provider, options) => {
131
+ const peer = options.env.HCC_PEER;
132
+ return createNativeAdapter(provider, { ...options, timeoutMs: 30000, env: { ...options.env,
133
+ PATH: path.dirname(process.execPath) + ':' + process.env.PATH, HOME: taskHome,
134
+ CODEX_HOME: homes.codex, CLAUDE_CONFIG_DIR: homes.claude, DSH_HOME: homes.dsh,
135
+ DSH_TELEMETRY_DISABLED: '1', HCC_SHIM_ENSURED: '1', HCC_SHIM_NO_ATTACH: '1' },
136
+ onEvent: event => {
137
+ const list = observations.get(peer) || [];
138
+ list.push({ type: event.type, turnId: event.turnId || null, sessionId: event.sessionId || null,
139
+ ...(event.type === 'error' ? { code: event.code, message: redactSecrets(event.message) } : {}) });
140
+ if (list.length > 200) list.shift(); observations.set(peer, list); options.onEvent(event);
141
+ } });
142
+ } });
143
+ for (const provider of providers) {
144
+ const peer = 'bounded-' + provider;
145
+ const result = report.providers[provider] = { status: 'running', prompts: 0, turns: [] };
146
+ try {
147
+ if (provider === 'dsh') assert.ok(process.env.DEEPSEEK_API_KEY, 'Existing DEEPSEEK_API_KEY is required');
148
+ const binary = provider === 'codex' ? option('--codex-bin', 'codex') : provider === 'dsh' ? option('--dsh-bin', 'dsh') : null;
149
+ if (binary) result.version = execFileSync(binary, ['--version'], { encoding: 'utf8', timeout: 10000, env: { ...process.env, HOME: taskHome, CODEX_HOME: homes.codex, DSH_HOME: homes.dsh } }).trim();
150
+ const start = () => api('POST', '/workers', { peer, provider, ...(binary ? { binary } : {}) });
151
+ await start();
152
+ const nonce = 'HCC_NONCE_' + randomUUID().replaceAll('-', '').slice(0, 16);
153
+ const prompts = [
154
+ 'Remember this nonce in this conversation: ' + nonce + '. Reply with only that nonce. Do not use tools, run commands, read files, delegate or modify files.',
155
+ 'Without tools or reading files, reply only with the nonce you were asked to remember earlier.',
156
+ 'After reconnecting this saved session, reply only with the nonce from earlier. Do not use tools or read files.'
157
+ ];
158
+ for (let index = 0; index < prompts.length; index++) {
159
+ if (index === 2) { await api('POST', '/close', { peer }); await api('POST', '/workers', { peer, provider, ...(binary ? { binary } : {}), resume: 'last' }); }
160
+ const input = { peer, from: 'bounded-acceptance', body: prompts[index], submissionId: randomUUID() };
161
+ const sent = await api('POST', '/send', input); result.prompts++;
162
+ if (index === 0) assert.equal((await api('POST', '/send', input)).message_id, sent.message_id);
163
+ result.turns.push(await complete(peer, sent, nonce));
164
+ const state = await api('GET', '/workers/' + peer + '/state');
165
+ assert.ok(state.snapshot.sessionId); if (result.sessionId) assert.equal(state.snapshot.sessionId, result.sessionId);
166
+ result.sessionId = state.snapshot.sessionId;
167
+ check(provider + ': ' + ['real reply and idempotent queue', 'memory in existing owned session', 'owned close/resume keeps identity and memory'][index]);
168
+ }
169
+ result.status = 'passed';
170
+ } catch (error) {
171
+ result.status = 'failed'; result.error = { code: error.code || 'ACCEPTANCE_FAILED', message: redactSecrets(error.message) };
172
+ console.log(JSON.stringify({ provider, failure: result.error }));
173
+ } finally {
174
+ result.events = observations.get(peer) || [];
175
+ try { await api('POST', '/close', { peer }); result.closed = true; } catch (error) { result.closed = false; result.closeError = redactSecrets(error.message); }
176
+ flush();
177
+ }
178
+ }
179
+ } catch (error) { report.fatalError = { code: error.code || 'ACCEPTANCE_FAILED', message: redactSecrets(error.message) }; }
180
+ finally {
181
+ try { await service?.shutdown(); report.cleanup.runtimeStopped = !readNativePointer(ctx); }
182
+ catch (error) { report.cleanup.runtimeStopped = false; report.cleanup.runtimeError = redactSecrets(error.message); }
183
+ db?.close();
184
+ report.protectedConfigUnchanged = protectedFiles.every(file => fs.existsSync(file) && hash(file) === protectedBefore[file]);
185
+ const after = sourceHashes();
186
+ report.sourceChanges = [...new Set([...Object.keys(report.sourceFiles), ...Object.keys(after)])].filter(file => report.sourceFiles[file] !== after[file]);
187
+ try { fs.rmSync(sandbox, { recursive: true, force: true }); report.cleanup.temporaryProjectAndCredentialsRemoved = !fs.existsSync(sandbox); }
188
+ catch (error) { report.cleanup.temporaryProjectAndCredentialsRemoved = false; report.cleanup.directoryError = redactSecrets(error.message); }
189
+ report.success = !report.fatalError && providers.every(provider => report.providers[provider]?.status === 'passed') && report.protectedConfigUnchanged && !report.sourceChanges.length && report.cleanup.runtimeStopped && report.cleanup.temporaryProjectAndCredentialsRemoved;
190
+ report.finishedAt = new Date().toISOString(); flush();
191
+ }
192
+ console.log(JSON.stringify({ receipt: output, success: report.success, providers: Object.fromEntries(Object.entries(report.providers).map(([name, result]) => [name, result.status])), cleanup: report.cleanup }));
193
+ process.exitCode = report.success ? 0 : 1;
@@ -0,0 +1,49 @@
1
+ import fs from 'node:fs';
2
+ import net from 'node:net';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { createHash } from 'node:crypto';
6
+
7
+ function candidatePorts(root) {
8
+ // Keep the fixture's bind-only availability check aligned with the four
9
+ // deterministic candidates in lib/shared/file-lock.mjs.
10
+ const key = path.join(root, '.hello-cc', 'native', 'service-owner');
11
+ const ports = [];
12
+ for (let index = 0; ports.length < 4; index += 1) {
13
+ const input = index === 0 ? key : `hcc-file-lock-port-v1\0${index}\0${key}`;
14
+ const port = 20_000 + createHash('sha256').update(input).digest().readUInt32BE(0) % 40_000;
15
+ if (!ports.includes(port)) ports.push(port);
16
+ }
17
+ return ports;
18
+ }
19
+
20
+ // This removes environmental collisions from fixtures; it does not relax the
21
+ // product's conservative refusal of an unidentified listener. Bind only our
22
+ // candidate ports, never connect to or inspect an existing process.
23
+ export async function createNativeFixtureRoot(prefix, { projectSubdir = '' } = {}) {
24
+ for (let attempt = 0; attempt < 50; attempt += 1) {
25
+ const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), prefix)));
26
+ const listeners = [];
27
+ let failure;
28
+ try {
29
+ for (const port of candidatePorts(path.join(root, projectSubdir))) {
30
+ const listener = net.createServer();
31
+ listeners.push(listener);
32
+ await new Promise((resolve, reject) => {
33
+ listener.once('error', reject);
34
+ listener.listen({ host: '127.0.0.1', port, exclusive: true }, resolve);
35
+ });
36
+ }
37
+ } catch (error) { failure = error; }
38
+ finally {
39
+ await Promise.all(listeners.map((listener) => new Promise((resolve, reject) => listener.close((error) => {
40
+ if (error && error.code !== 'ERR_SERVER_NOT_RUNNING') reject(error);
41
+ else resolve();
42
+ }))));
43
+ }
44
+ if (!failure) return root;
45
+ fs.rmSync(root, { recursive: true, force: true });
46
+ if (failure.code !== 'EADDRINUSE') throw failure;
47
+ }
48
+ throw new Error('Could not allocate a native test root with four available ownership ports');
49
+ }