@logicseek/hello-cc 1.0.1 → 1.0.2
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.
- package/CHANGELOG.md +105 -0
- package/README.md +4 -0
- package/README.zh-CN.md +4 -0
- package/docs/README.md +9 -0
- package/docs/README.zh-CN.md +9 -0
- package/docs/commands.md +53 -0
- package/docs/commands.zh-CN.md +48 -0
- package/docs/dsh.md +137 -0
- package/docs/dsh.zh-CN.md +139 -0
- package/docs/mcp.md +9 -0
- package/docs/mcp.zh-CN.md +23 -0
- package/docs/native.md +295 -0
- package/docs/native.zh-CN.md +253 -0
- package/docs/web-handoff.zh-CN.md +53 -0
- package/lib/cli/app.mjs +25 -6
- package/lib/cli/commands/doctor.mjs +17 -2
- package/lib/cli/commands/dsh.mjs +79 -0
- package/lib/cli/commands/hook.mjs +241 -90
- package/lib/cli/commands/lock.mjs +34 -19
- package/lib/cli/commands/mcp.mjs +28 -0
- package/lib/cli/commands/native.mjs +130 -0
- package/lib/cli/commands/run.mjs +66 -21
- package/lib/cli/commands/task.mjs +6 -3
- package/lib/cli-args.mjs +4 -0
- package/lib/coordination-state.mjs +15 -1
- package/lib/core/coordination/event-retention.mjs +14 -2
- package/lib/core/coordination/gc-plan.mjs +3 -13
- package/lib/core/coordination/locks.mjs +45 -0
- package/lib/core/coordination/results.mjs +79 -0
- package/lib/core/peers/bindings.mjs +9 -3
- package/lib/diagnostics/codex.mjs +121 -0
- package/lib/integrations/codex-interactions.mjs +114 -0
- package/lib/integrations/dsh-collaboration.mjs +170 -0
- package/lib/integrations/dsh-cordis.d.ts +19 -0
- package/lib/integrations/dsh-cordis.mjs +177 -0
- package/lib/integrations/dsh.bundle.yml +7 -0
- package/lib/integrations/dsh.mjs +287 -0
- package/lib/integrations/native/claude.mjs +451 -0
- package/lib/integrations/native/codex.mjs +242 -0
- package/lib/integrations/native/dsh-acp.mjs +343 -0
- package/lib/integrations/native/index.mjs +19 -0
- package/lib/integrations/native/interactions.mjs +53 -0
- package/lib/integrations/native/jsonrpc.mjs +185 -0
- package/lib/integrations/peers/identity.mjs +36 -10
- package/lib/integrations/peers/processes.mjs +22 -8
- package/lib/integrations/providers.mjs +6 -2
- package/lib/mcp/scope.mjs +104 -0
- package/lib/mcp/stdio.mjs +80 -0
- package/lib/mcp/tools.mjs +197 -0
- package/lib/process/identity.mjs +3 -1
- package/lib/release/release-notes.mjs +9 -0
- package/lib/runtime/native/client.mjs +38 -0
- package/lib/runtime/native/service.mjs +511 -0
- package/lib/runtime/native/store.mjs +121 -0
- package/lib/ui/help.mjs +5 -2
- package/lib/web/adoption-state.mjs +55 -0
- package/lib/web/auto-attach.mjs +18 -2
- package/lib/web/codex-app-server.mjs +557 -0
- package/lib/web/codex-sessions.mjs +287 -0
- package/lib/web/control-lease.mjs +204 -0
- package/lib/web/http-routes.mjs +197 -15
- package/lib/web/http.mjs +5 -4
- package/lib/web/native-sessions.mjs +286 -0
- package/lib/web/project-contexts.mjs +2 -1
- package/lib/web/pty-sessions.mjs +60 -33
- package/lib/web/runtime-main.mjs +98 -6
- package/lib/web/session-serialize.mjs +0 -0
- package/lib/web/task-results.mjs +76 -0
- package/lib/web/tmux-sessions.mjs +92 -11
- package/lib/web/tmux-stream.mjs +2 -2
- package/lib/web/ui-codex.mjs +550 -0
- package/lib/web/ui-command-palette.mjs +119 -0
- package/lib/web/ui-handoff.mjs +102 -0
- package/lib/web/ui-history.mjs +118 -0
- package/lib/web/ui-i18n.mjs +841 -0
- package/lib/web/ui-interactions.mjs +78 -0
- package/lib/web/ui-login.mjs +101 -0
- package/lib/web/ui-native.mjs +165 -0
- package/lib/web/ui-preferences.mjs +278 -0
- package/lib/web/ui-review.mjs +132 -0
- package/lib/web/ui-template.mjs +1106 -576
- package/lib/web/ui-terminal-find.mjs +98 -0
- package/lib/web/ui-workbench.mjs +94 -0
- package/lib/web/ui-workspace.mjs +222 -0
- package/package.json +11 -3
- package/scripts/check-free-identifiers.mjs +67 -8
- package/scripts/codex-diagnostics-installed-acceptance.mjs +72 -0
- package/scripts/dsh-cordis-acceptance.mjs +200 -0
- package/scripts/dsh-installed-acceptance.mjs +247 -0
- package/scripts/dsh-official-acceptance.mjs +180 -0
- package/scripts/dsh-web-startup-acceptance.mjs +87 -0
- package/scripts/github-release.mjs +3 -1
- package/scripts/native-live-acceptance.mjs +369 -0
- package/scripts/regression.mjs +243 -89
- package/scripts/web-handoff-installed-acceptance.mjs +98 -0
- package/scripts/web-native-interaction-acceptance.mjs +204 -0
- package/scripts/web-native-model-acceptance.mjs +138 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { randomUUID, createHash } from 'node:crypto';
|
|
6
|
+
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
7
|
+
import { execFileSync } from 'node:child_process';
|
|
8
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
9
|
+
import { createEventHelpers } from '../lib/db/events.mjs';
|
|
10
|
+
import { createConnectionHelpers } from '../lib/db/connection.mjs';
|
|
11
|
+
import { createPeerBindingStore } from '../lib/db/stores/peers.mjs';
|
|
12
|
+
import { createPeerHelpers } from '../lib/core/peers/peer-helpers.mjs';
|
|
13
|
+
import { createMessageStore } from '../lib/core/coordination/messages.mjs';
|
|
14
|
+
import { createEvidenceRuntime } from '../lib/core/peers/evidence-runtime.mjs';
|
|
15
|
+
import { detectBranch } from '../lib/runtime/project-context.mjs';
|
|
16
|
+
import { createCodexAdapter } from '../lib/integrations/native/codex.mjs';
|
|
17
|
+
import { JsonRpcProcess } from '../lib/integrations/native/jsonrpc.mjs';
|
|
18
|
+
import { createDshAcpAdapter } from '../lib/integrations/native/dsh-acp.mjs';
|
|
19
|
+
import { createClaudeAdapter } from '../lib/integrations/native/claude.mjs';
|
|
20
|
+
import { createNativeSessions } from '../lib/web/native-sessions.mjs';
|
|
21
|
+
import { startNativeService } from '../lib/runtime/native/service.mjs';
|
|
22
|
+
import { nativeRequest } from '../lib/runtime/native/client.mjs';
|
|
23
|
+
import { readNativePointer } from '../lib/runtime/native/store.mjs';
|
|
24
|
+
import { redactSecrets } from '../lib/shared/redact.mjs';
|
|
25
|
+
|
|
26
|
+
// Opt-in only: uses the caller's existing authentication for small real model
|
|
27
|
+
// turns. Every provider session and HCC database is created in a fresh sandbox.
|
|
28
|
+
// No existing session is resumed or interrupted. No packages are installed.
|
|
29
|
+
const args = process.argv.slice(2);
|
|
30
|
+
function option(name, fallback) { const i = args.indexOf(name); return i < 0 ? fallback : args[i + 1]; }
|
|
31
|
+
if (!args.includes('--run-live')) {
|
|
32
|
+
console.log('Usage: node scripts/native-live-acceptance.mjs --run-live [--provider codex|claude|dsh|all] [--codex-bin PATH] [--dsh-bin PATH] [--claude-sdk PATH] [--output PATH] [--permission-probe] [--cross-provider] [--communication-only]');
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
const repo = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
36
|
+
function sourceHashes(dir = path.join(repo, 'lib')) {
|
|
37
|
+
return Object.fromEntries(fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
|
38
|
+
const file = path.join(dir, entry.name);
|
|
39
|
+
return entry.isDirectory() ? Object.entries(sourceHashes(file)) : entry.name.endsWith('.mjs')
|
|
40
|
+
? [[path.relative(repo, file), createHash('sha256').update(fs.readFileSync(file)).digest('hex')]] : [];
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
const initialSources = sourceHashes();
|
|
44
|
+
const providers = option('--provider', 'codex') === 'all' ? ['codex', 'claude', 'dsh'] : [option('--provider', 'codex')];
|
|
45
|
+
assert.ok(providers.every((p) => ['codex', 'claude', 'dsh'].includes(p)), 'unknown provider');
|
|
46
|
+
assert.ok(!(args.includes('--cross-provider') || args.includes('--communication-only')) || providers.length >= 2, 'cross-provider acceptance requires --provider all');
|
|
47
|
+
const sandbox = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'hcc-native-live-')));
|
|
48
|
+
fs.chmodSync(sandbox, 0o700);
|
|
49
|
+
const root = path.join(sandbox, 'project');
|
|
50
|
+
fs.mkdirSync(root, { mode: 0o700 });
|
|
51
|
+
const ctx = { root, cwd: root, dbPath: path.join(root, '.hello-cc', 'mesh.db') };
|
|
52
|
+
const report = { startedAt: new Date().toISOString(), node: process.version, scope: 'isolated real authenticated native runtime and shared HCC bus',
|
|
53
|
+
sandbox, authentication: 'caller provider route, private temporary provider state', defaultClaudeSdkInstalled: null,
|
|
54
|
+
sourceFiles: initialSources,
|
|
55
|
+
providers: {}, checks: [], limitations: [], cleanup: {} };
|
|
56
|
+
const output = path.resolve(option('--output', path.join(sandbox, 'receipt.json')));
|
|
57
|
+
const credentials = [];
|
|
58
|
+
const observations = new Map();
|
|
59
|
+
let service;
|
|
60
|
+
let inspect;
|
|
61
|
+
function check(name, details = {}) { report.checks.push({ name, passed: true, ...details }); console.log(JSON.stringify({ check: name, ...details })); }
|
|
62
|
+
function flush() { fs.mkdirSync(path.dirname(output), { recursive: true }); fs.writeFileSync(output, JSON.stringify(redactSecrets(report), null, 2) + '\n', { mode: 0o600 }); }
|
|
63
|
+
function privateDir(name) { const dir = path.join(sandbox, name); fs.mkdirSync(dir, { mode: 0o700 }); credentials.push(dir); return dir; }
|
|
64
|
+
function isolatedCodexHome() {
|
|
65
|
+
const source = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
|
|
66
|
+
const dir = privateDir('codex-home');
|
|
67
|
+
const file = path.join(source, 'config.toml');
|
|
68
|
+
if (fs.existsSync(file)) {
|
|
69
|
+
// Preserve model/provider routing, discard user tools, plugins, hooks,
|
|
70
|
+
// project trust records and UI settings. Credentials are never printed.
|
|
71
|
+
let section = '';
|
|
72
|
+
const config = fs.readFileSync(file, 'utf8').split('\n').filter((line) => {
|
|
73
|
+
const header = line.match(/^\s*\[\[?([^\]]+)\]\]?\s*(?:#.*)?$/);
|
|
74
|
+
if (header) section = header[1].split('.')[0].replaceAll('"', '').replaceAll("'", '');
|
|
75
|
+
if (section) return section === 'model_providers';
|
|
76
|
+
return /^\s*(?:model(?:_provider|_reasoning_effort|_reasoning_summary|_verbosity|_catalog_json)?|cli_auth_credentials_store)\s*=/.test(line);
|
|
77
|
+
}).join('\n');
|
|
78
|
+
fs.writeFileSync(path.join(dir, 'config.toml'), config + '\n', { mode: 0o600 });
|
|
79
|
+
}
|
|
80
|
+
if (fs.existsSync(path.join(source, 'auth.json'))) {
|
|
81
|
+
fs.copyFileSync(path.join(source, 'auth.json'), path.join(dir, 'auth.json'));
|
|
82
|
+
fs.chmodSync(path.join(dir, 'auth.json'), 0o600);
|
|
83
|
+
}
|
|
84
|
+
return dir;
|
|
85
|
+
}
|
|
86
|
+
const now = () => Math.floor(Date.now() / 1000);
|
|
87
|
+
const events = createEventHelpers({ now });
|
|
88
|
+
const bindings = createPeerBindingStore({ now, ...events });
|
|
89
|
+
const evidence = createEvidenceRuntime({ now });
|
|
90
|
+
const peers = createPeerHelpers({ now, ...events, ...evidence, detectBranch });
|
|
91
|
+
const messages = createMessageStore({ now, ...events });
|
|
92
|
+
const connections = createConnectionHelpers({ now, ...bindings, redactedLogText: (s) => redactSecrets(s) });
|
|
93
|
+
const deps = { ...connections, ...events, ...bindings, ...peers, ...messages, ...evidence, detectBranch };
|
|
94
|
+
const homes = {};
|
|
95
|
+
const api = (method, route, body = null) => nativeRequest(ctx, method, route, body, { timeoutMs: 45000 });
|
|
96
|
+
const terminal = new Set(['completed', 'failed', 'uncertain']);
|
|
97
|
+
async function until(fn, timeoutMs = 120000, description = 'provider outcome') {
|
|
98
|
+
const end = Date.now() + timeoutMs;
|
|
99
|
+
let notice = Date.now() + 20000;
|
|
100
|
+
while (Date.now() < end) {
|
|
101
|
+
const value = await fn();
|
|
102
|
+
if (value) return value;
|
|
103
|
+
if (Date.now() >= notice) { console.log(JSON.stringify({ waiting: description })); notice += 20000; }
|
|
104
|
+
await delay(100);
|
|
105
|
+
}
|
|
106
|
+
throw new Error(`Timed out waiting for ${description}`);
|
|
107
|
+
}
|
|
108
|
+
async function delivery(peer, id) { return (await api('GET', `/deliveries?peer=${encodeURIComponent(peer)}`)).find((row) => row.message_id === id); }
|
|
109
|
+
async function send(peer, body, from = 'acceptance-observer') {
|
|
110
|
+
return api('POST', '/send', { peer, from, body, submissionId: randomUUID() });
|
|
111
|
+
}
|
|
112
|
+
async function finish(peer, sent) {
|
|
113
|
+
const row = await until(async () => { const row = await delivery(peer, sent.message_id); return terminal.has(row?.state) ? row : null; }, 120000, `${peer} message ${sent.message_id}`);
|
|
114
|
+
assert.equal(row.state, 'completed', JSON.stringify({ state: row.state, evidence: row.evidence }));
|
|
115
|
+
const reply = inspect.prepare("SELECT * FROM messages WHERE reply_to=? AND kind='reply'").get(sent.message_id);
|
|
116
|
+
assert.ok(reply, 'successful ask must write a reply');
|
|
117
|
+
assert.equal(reply.thread_id, sent.message_id, 'reply must retain the original thread');
|
|
118
|
+
assert.ok(inspect.prepare('SELECT read_at FROM message_reads WHERE peer=? AND message_id=?').get(peer, sent.message_id), 'completion must ACK');
|
|
119
|
+
return { row, reply };
|
|
120
|
+
}
|
|
121
|
+
async function runProvider(provider) {
|
|
122
|
+
const peer = `accept-${provider}`;
|
|
123
|
+
report.providers[provider] = { status: 'running', prompts: 0, route: 'original authenticated provider', observations: [],
|
|
124
|
+
approvalDenial: { requested: args.includes('--permission-probe'), exercised: false } };
|
|
125
|
+
const result = report.providers[provider];
|
|
126
|
+
try {
|
|
127
|
+
const binary = provider === 'codex' ? option('--codex-bin', 'codex') : provider === 'dsh' ? option('--dsh-bin', 'dsh') : undefined;
|
|
128
|
+
const started = await api('POST', '/workers', { peer, provider, ...(binary ? { binary } : {}) });
|
|
129
|
+
if (provider !== 'claude') {
|
|
130
|
+
const env = { ...process.env, PATH: `${path.dirname(process.execPath)}:${process.env.PATH || ''}`, ...(provider === 'dsh' ? { DSH_HOME: homes.dsh } : { CODEX_HOME: homes.codex }) };
|
|
131
|
+
result.version = execFileSync(binary, ['--version'], { env, encoding: 'utf8', timeout: 10000 }).trim();
|
|
132
|
+
}
|
|
133
|
+
result.capabilities = started.capabilities;
|
|
134
|
+
result.sessionId = started.sessionId;
|
|
135
|
+
result.observations.push({ opened: true, sessionIdInitiallyPresent: Boolean(started.sessionId) });
|
|
136
|
+
if (args.includes('--communication-only')) { result.status = 'ready'; result.lifecycleChecks = 'not exercised'; return; }
|
|
137
|
+
const marker = `HCC_MEMORY_${randomUUID().replaceAll('-', '').slice(0, 16)}`;
|
|
138
|
+
const body = `This is a native communication acceptance test. Remember this nonce in this conversation: ${marker}. Reply with only ${marker}. Do not use any tools, run commands, read files, delegate tasks, or modify files.`;
|
|
139
|
+
const first = await send(peer, body);
|
|
140
|
+
result.prompts++;
|
|
141
|
+
const duplicate = await api('POST', '/send', { peer, from: 'acceptance-observer', body, submissionId: first.submission_id });
|
|
142
|
+
assert.equal(duplicate.message_id, first.message_id);
|
|
143
|
+
const finished = await finish(peer, first);
|
|
144
|
+
assert.equal(finished.reply.body.trim(), marker);
|
|
145
|
+
check(`${provider}: real turn, correlated reply, ACK, idempotent queue`, { messageId: first.message_id, turnId: finished.row.turn_id });
|
|
146
|
+
const state = await api('GET', `/workers/${peer}/state`);
|
|
147
|
+
result.sessionId = state.snapshot.sessionId;
|
|
148
|
+
assert.ok(result.sessionId);
|
|
149
|
+
const second = await send(peer, 'Without using tools or reading files, return only the nonce you were asked to remember earlier.');
|
|
150
|
+
result.prompts++;
|
|
151
|
+
assert.equal((await finish(peer, second)).reply.body.trim(), marker);
|
|
152
|
+
check(`${provider}: persistent session retains previous turn`);
|
|
153
|
+
await api('POST', '/close', { peer });
|
|
154
|
+
const resumed = await api('POST', '/workers', { peer, provider, ...(binary ? { binary } : {}), resume: 'last' });
|
|
155
|
+
if (resumed.sessionId) assert.equal(resumed.sessionId, result.sessionId);
|
|
156
|
+
const third = await send(peer, 'This is after reconnecting your saved session. Without using tools or reading files, return only the nonce from the earlier conversation.');
|
|
157
|
+
result.prompts++;
|
|
158
|
+
assert.equal((await finish(peer, third)).reply.body.trim(), marker);
|
|
159
|
+
assert.equal((await api('GET', `/workers/${peer}/state`)).snapshot.sessionId, result.sessionId);
|
|
160
|
+
check(`${provider}: owned close/resume preserves identity and history`);
|
|
161
|
+
const index = observations.get(peer)?.length || 0;
|
|
162
|
+
const long = await send(peer, 'Do not use any tools. Immediately begin writing the numbers 1 through 100000, one per line, with no commentary. Keep writing until all numbers are emitted.');
|
|
163
|
+
result.prompts++;
|
|
164
|
+
if (provider === 'dsh') {
|
|
165
|
+
// ACP implementations may buffer model output until the turn ends.
|
|
166
|
+
// Interrupt an admitted running request rather than requiring a delta.
|
|
167
|
+
await until(async () => { const s = await api('GET', `/workers/${peer}/state`); return s.active_delivery?.message_id === long.message_id && s.snapshot.status === 'running'; }, 30000, 'DSH running ACP request');
|
|
168
|
+
await delay(500);
|
|
169
|
+
} else await until(() => observations.get(peer)?.slice(index).some((event) => ['delta', 'output'].includes(event.type) && event.text), 120000, `${provider} active streamed output`);
|
|
170
|
+
const active = await api('GET', `/workers/${peer}/state`);
|
|
171
|
+
assert.ok(active.active_delivery, 'interrupt requires a genuinely active delivery');
|
|
172
|
+
const interrupted = await api('POST', '/interrupt', { peer, turnId: active.snapshot.turnId });
|
|
173
|
+
const interruptedDelivery = await until(async () => { const row = await delivery(peer, long.message_id); return terminal.has(row?.state) ? row : null; }, 45000, `${provider} terminal interruption`);
|
|
174
|
+
assert.equal(interruptedDelivery.state, 'failed', 'interrupted work must not be marked successful');
|
|
175
|
+
assert.equal(inspect.prepare('SELECT COUNT(*) AS n FROM message_reads WHERE peer=? AND message_id=?').get(peer, long.message_id).n, 0);
|
|
176
|
+
assert.equal(inspect.prepare('SELECT COUNT(*) AS n FROM messages WHERE reply_to=?').get(long.message_id).n, 0);
|
|
177
|
+
check(`${provider}: active interruption has terminal evidence, no reply/ACK`, { requestStatus: interrupted.status, terminalState: interruptedDelivery.state });
|
|
178
|
+
if (args.includes('--permission-probe') && provider === 'codex') {
|
|
179
|
+
// Codex workspace-write normally permits the OS temporary directory.
|
|
180
|
+
// Use a fresh test-owned HOME subdirectory to actually cross the sandbox.
|
|
181
|
+
const boundary = fs.mkdtempSync(path.join(os.homedir(), '.hcc-native-live-permission-'));
|
|
182
|
+
fs.chmodSync(boundary, 0o700);
|
|
183
|
+
credentials.push(boundary);
|
|
184
|
+
const target = path.join(boundary, 'proof.txt');
|
|
185
|
+
const startIndex = observations.get(peer).length;
|
|
186
|
+
const permission = await send(peer, `Permission boundary acceptance test: use the command tool with sandbox_permissions=require_escalated to write the word PROOF to this absolute path outside your workspace: ${target}. Request approval for the write if sandbox restrictions require it. If permission is denied, stop and say DENIED; do not retry with another tool or technique. Do not read any other files.`);
|
|
187
|
+
result.prompts++;
|
|
188
|
+
const request = await until(async () => {
|
|
189
|
+
const state = await api('GET', `/workers/${peer}/state`);
|
|
190
|
+
const request = state.snapshot.pendingApprovals?.[0];
|
|
191
|
+
return request || observations.get(peer).slice(startIndex).find((event) => event.type === 'approval' && event.decision === 'decline');
|
|
192
|
+
}, 45000, 'real permission request');
|
|
193
|
+
if (request.status === 'pending') {
|
|
194
|
+
await api('POST', '/respond', { peer, executorId: request.executorId, sessionId: request.sessionId, turnId: request.turnId, requestId: request.requestId, decision: 'decline' });
|
|
195
|
+
check(`${provider}: pending real approval explicitly declined`, { method: request.method });
|
|
196
|
+
}
|
|
197
|
+
await finish(peer, permission);
|
|
198
|
+
assert.ok(observations.get(peer).slice(startIndex).some((event) => event.type === 'approval.resolved' && event.reason === 'answered' || event.type === 'approval' && event.decision === 'decline'), 'provider must actually request approval and receive denial');
|
|
199
|
+
assert.equal(fs.existsSync(target), false, 'denied write must not create the target');
|
|
200
|
+
result.approvalDenial.exercised = true;
|
|
201
|
+
check(`${provider}: real approval request denied and outside write absent`);
|
|
202
|
+
}
|
|
203
|
+
result.status = 'passed';
|
|
204
|
+
} catch (error) {
|
|
205
|
+
result.status = 'failed';
|
|
206
|
+
result.error = { code: error.code || 'ACCEPTANCE_FAILED', message: error.message };
|
|
207
|
+
const observed = observations.get(peer) || [];
|
|
208
|
+
result.observations.push(...observed.filter((event) => ['error', 'completed', 'approval', 'permission'].includes(event.type)).slice(-12));
|
|
209
|
+
console.log(JSON.stringify(redactSecrets({ provider, failure: result.error })));
|
|
210
|
+
} finally {
|
|
211
|
+
try { if (result.status !== 'ready') { await api('POST', '/close', { peer }); result.closed = true; } }
|
|
212
|
+
catch (error) { result.closed = false; result.closeError = { code: error.code, message: error.message }; }
|
|
213
|
+
flush();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async function runCommunications() {
|
|
217
|
+
const selected = providers.filter((provider) => ['passed', 'ready'].includes(report.providers[provider].status));
|
|
218
|
+
if (selected.length < 2) { report.limitations.push('Cross-provider test requires at least two successfully authenticated providers.'); return; }
|
|
219
|
+
const sessions = new Map();
|
|
220
|
+
const bridge = createNativeSessions({ sessions, sessionKey: (project, peer) => `${project.root}:${peer}`,
|
|
221
|
+
connectWebProject: (project) => deps.connect(project, { migrateRegistered: false }) });
|
|
222
|
+
report.communications = { status: 'running', pairs: [] };
|
|
223
|
+
try {
|
|
224
|
+
const alreadyOpen = (await api('GET', '/status')).workers.filter((worker) => worker.owned).map((worker) => worker.peer);
|
|
225
|
+
for (const provider of selected) {
|
|
226
|
+
if (alreadyOpen.includes(`accept-${provider}`)) continue;
|
|
227
|
+
const binary = provider === 'codex' ? option('--codex-bin', 'codex') : provider === 'dsh' ? option('--dsh-bin', 'dsh') : undefined;
|
|
228
|
+
await api('POST', '/workers', { peer: `accept-${provider}`, provider, ...(binary ? { binary } : {}), resume: 'last' });
|
|
229
|
+
}
|
|
230
|
+
const before = inspect.prepare('SELECT * FROM peer_bindings ORDER BY peer').all();
|
|
231
|
+
const views = await bridge.discoverNativeSessions(ctx);
|
|
232
|
+
assert.equal(views.length, selected.length);
|
|
233
|
+
assert.deepEqual(inspect.prepare('SELECT * FROM peer_bindings ORDER BY peer').all(), before);
|
|
234
|
+
check('Web: discovers real native workers and preserves their provider binding');
|
|
235
|
+
for (let index = 0; index < selected.length; index++) {
|
|
236
|
+
const fromProvider = selected[index], toProvider = selected[(index + 1) % selected.length];
|
|
237
|
+
const from = `accept-${fromProvider}`, to = `accept-${toProvider}`;
|
|
238
|
+
const marker = `HCC_CROSS_${randomUUID().replaceAll('-', '').slice(0, 16)}`;
|
|
239
|
+
const body = `Native cross-provider test. Return only ${marker}. Do not use tools, read files, delegate, or change files.`;
|
|
240
|
+
const input = { to, kind: 'ask', body };
|
|
241
|
+
const session = sessions.get(`${root}:${from}`);
|
|
242
|
+
const sent = await bridge.nativeAction(session, 'send', { submissionId: randomUUID(), text:
|
|
243
|
+
`Use only the hello_cc_scoped MCP hcc_message_send tool exactly once with these JSON arguments: ${JSON.stringify(input)}. Do not send by shell or any other transport. Do not manually read or ACK inbox messages. After the tool succeeds, return only SENT. No other tools or file changes.` });
|
|
244
|
+
report.providers[fromProvider].prompts++;
|
|
245
|
+
await until(async () => {
|
|
246
|
+
const state = await api('GET', `/workers/${from}/state`);
|
|
247
|
+
for (const request of state.snapshot.pendingApprovals || []) {
|
|
248
|
+
const args = request.params?.input || request.params?._meta?.tool_params || request.params?.toolCall?.rawInput;
|
|
249
|
+
const name = request.params?.tool || request.params?.message || request.params?.toolCall?.title || '';
|
|
250
|
+
const safe = String(name).includes('hcc_message_send') && args?.to === to && args?.body === body && args?.kind === 'ask';
|
|
251
|
+
await api('POST', '/respond', { peer: from, executorId: request.executorId, sessionId: request.sessionId,
|
|
252
|
+
turnId: request.turnId, requestId: request.requestId, decision: safe ? 'accept' : 'decline' });
|
|
253
|
+
assert.ok(safe, 'acceptance harness only grants the exact scoped test-message operation');
|
|
254
|
+
}
|
|
255
|
+
const row = await delivery(from, sent.message_id);
|
|
256
|
+
return terminal.has(row?.state) ? row : null;
|
|
257
|
+
}, 120000, `${fromProvider} scoped MCP send to ${toProvider}`);
|
|
258
|
+
const source = await finish(from, sent);
|
|
259
|
+
report.communications.lastSource = { fromProvider, response: source.reply.body,
|
|
260
|
+
observed: (observations.get(from) || []).slice(-25).filter((event) => !['delta', 'output'].includes(event.type)) };
|
|
261
|
+
const request = inspect.prepare('SELECT * FROM messages WHERE sender=? AND recipient=? AND body=?').get(from, to, body);
|
|
262
|
+
assert.ok(request, 'actual model MCP call must write the intended message as its own peer');
|
|
263
|
+
const reply = await finish(to, { message_id: request.id });
|
|
264
|
+
report.providers[toProvider].prompts++;
|
|
265
|
+
assert.equal(reply.reply.body.trim(), marker);
|
|
266
|
+
const receipt = await until(async () => { const row = await delivery(from, reply.reply.id); return terminal.has(row?.state) ? row : null; }, 120000, `${fromProvider} receives ${toProvider} reply`);
|
|
267
|
+
assert.equal(receipt.state, 'completed');
|
|
268
|
+
report.providers[fromProvider].prompts++;
|
|
269
|
+
assert.ok(inspect.prepare('SELECT read_at FROM message_reads WHERE peer=? AND message_id=?').get(from, reply.reply.id));
|
|
270
|
+
await delay(500);
|
|
271
|
+
assert.equal(inspect.prepare('SELECT COUNT(*) AS n FROM messages WHERE reply_to=?').get(reply.reply.id).n, 0);
|
|
272
|
+
report.communications.pairs.push({ fromProvider, toProvider, sentByModel: true, requestId: request.id,
|
|
273
|
+
replyId: reply.reply.id, replyConsumed: true, noReplyLoop: true });
|
|
274
|
+
check(`${fromProvider} -> ${toProvider}: actual scoped MCP send, model reply, reply consumption, no loop`,
|
|
275
|
+
{ requestId: request.id, replyId: reply.reply.id });
|
|
276
|
+
}
|
|
277
|
+
bridge.closeNativeBridge();
|
|
278
|
+
const status = await api('GET', '/status');
|
|
279
|
+
assert.equal(status.workers.filter((worker) => worker.owned).length, selected.length);
|
|
280
|
+
check('Web: closing the bridge preserves live native workers');
|
|
281
|
+
report.communications.status = 'passed';
|
|
282
|
+
for (const provider of selected) if (report.providers[provider].status === 'ready') report.providers[provider].status = 'passed';
|
|
283
|
+
} catch (error) {
|
|
284
|
+
report.communications.status = 'failed';
|
|
285
|
+
report.communications.error = { code: error.code || 'ACCEPTANCE_FAILED', message: error.message };
|
|
286
|
+
console.log(JSON.stringify({ communicationsFailure: report.communications.error }));
|
|
287
|
+
} finally {
|
|
288
|
+
bridge.closeNativeBridge();
|
|
289
|
+
for (const provider of selected) await api('POST', '/close', { peer: `accept-${provider}` }).catch(() => {});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
if (providers.includes('codex')) homes.codex = isolatedCodexHome();
|
|
295
|
+
if (providers.includes('claude')) homes.claude = privateDir('claude-home');
|
|
296
|
+
if (providers.includes('dsh')) homes.dsh = privateDir('dsh-home');
|
|
297
|
+
inspect = deps.connect(ctx, { migrateRegistered: false });
|
|
298
|
+
service = await startNativeService(ctx, deps, { adapterFactory: async (provider, options) => {
|
|
299
|
+
const peer = options.env.HCC_PEER;
|
|
300
|
+
const prior = options.onEvent;
|
|
301
|
+
const env = { ...options.env, PATH: `${path.dirname(process.execPath)}:${options.env.PATH || ''}`,
|
|
302
|
+
...(provider === 'codex' ? { CODEX_HOME: homes.codex } : {}),
|
|
303
|
+
...(provider === 'claude' ? { CLAUDE_CONFIG_DIR: homes.claude } : {}),
|
|
304
|
+
...(provider === 'dsh' ? { DSH_HOME: homes.dsh, DSH_TELEMETRY_DISABLED: '1' } : {}) };
|
|
305
|
+
const liveOptions = { ...options, env, timeoutMs: 30000, onEvent: (event) => {
|
|
306
|
+
// Keep streaming evidence bounded. Store only acceptance test text.
|
|
307
|
+
const list = observations.get(peer) || [];
|
|
308
|
+
list.push(redactSecrets(event));
|
|
309
|
+
if (list.length > 1500) list.shift();
|
|
310
|
+
observations.set(peer, list);
|
|
311
|
+
prior(event);
|
|
312
|
+
} };
|
|
313
|
+
if (provider === 'claude' && option('--claude-sdk')) {
|
|
314
|
+
const sdkPath = path.resolve(option('--claude-sdk'));
|
|
315
|
+
const sdk = await import(pathToFileURL(sdkPath));
|
|
316
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(path.dirname(sdkPath), 'package.json'), 'utf8'));
|
|
317
|
+
report.providers.claude.sdk = { version: pkg.version, executableSource: 'SDK default bundled CLI', entry: sdkPath, sha256: createHash('sha256').update(fs.readFileSync(sdkPath)).digest('hex'), dependencyResolution: 'explicit external SDK entry injection' };
|
|
318
|
+
return createClaudeAdapter({ ...liveOptions, query: sdk.query });
|
|
319
|
+
}
|
|
320
|
+
if (provider === 'claude') {
|
|
321
|
+
const adapter = createClaudeAdapter(liveOptions);
|
|
322
|
+
const open = adapter.open;
|
|
323
|
+
adapter.open = async (input) => {
|
|
324
|
+
try { const result = await open(input); report.defaultClaudeSdkInstalled = true; return result; }
|
|
325
|
+
catch (error) { if (error.code === 'NATIVE_SDK_MISSING') report.defaultClaudeSdkInstalled = false; throw error; }
|
|
326
|
+
};
|
|
327
|
+
return adapter;
|
|
328
|
+
}
|
|
329
|
+
if (provider === 'codex') return createCodexAdapter({ ...liveOptions, rpcFactory: (config) => new JsonRpcProcess({ ...config,
|
|
330
|
+
onRequest: (method, params, requestId) => {
|
|
331
|
+
const list = observations.get(peer) || [];
|
|
332
|
+
list.push(redactSecrets({ type: 'protocol.request', method, requestId, params }));
|
|
333
|
+
observations.set(peer, list);
|
|
334
|
+
return config.onRequest(method, params, requestId);
|
|
335
|
+
},
|
|
336
|
+
onNotification: (method, params) => {
|
|
337
|
+
if (params?.item?.type === 'mcpToolCall' || method.startsWith('mcp')) {
|
|
338
|
+
const list = observations.get(peer) || [];
|
|
339
|
+
list.push(redactSecrets({ type: 'protocol.mcp', method, item: params.item || params }));
|
|
340
|
+
observations.set(peer, list);
|
|
341
|
+
}
|
|
342
|
+
config.onNotification(method, params);
|
|
343
|
+
} }) });
|
|
344
|
+
return createDshAcpAdapter(liveOptions);
|
|
345
|
+
} });
|
|
346
|
+
for (const provider of providers) await runProvider(provider);
|
|
347
|
+
if (args.includes('--cross-provider') || args.includes('--communication-only')) await runCommunications();
|
|
348
|
+
report.limitations.push('The acceptance harness calls the same native service/adapters with an isolated environment factory; this does not prove CLI default dependency resolution or packaged distribution.');
|
|
349
|
+
report.limitations.push('No existing TUI/Desktop session, published build or business task acceptance was exercised.');
|
|
350
|
+
const unprobed = providers.filter(provider => !report.providers[provider]?.approvalDenial?.exercised);
|
|
351
|
+
if (unprobed.length) report.limitations.push(`Real command/file approval denial was not exercised for ${unprobed.join(', ')}; any MCP tool confirmation is recorded separately. This script supports --permission-probe only for Codex; installed dsh approval denial is covered by dsh-installed-acceptance.mjs --run-live.`);
|
|
352
|
+
} catch (error) {
|
|
353
|
+
report.fatalError = { code: error.code || 'ACCEPTANCE_FAILED', message: error.message };
|
|
354
|
+
} finally {
|
|
355
|
+
try { await service?.shutdown(); report.cleanup.runtimeStopped = !readNativePointer(ctx); }
|
|
356
|
+
catch (error) { report.cleanup.runtimeStopped = false; report.cleanup.error = { code: error.code, message: error.message }; }
|
|
357
|
+
inspect?.close();
|
|
358
|
+
// Remove only the test-owned credential/config copies, even after failures.
|
|
359
|
+
for (const dir of credentials) fs.rmSync(dir, { recursive: true, force: true });
|
|
360
|
+
report.cleanup.temporaryCredentialsRemoved = true;
|
|
361
|
+
const finalSources = sourceHashes();
|
|
362
|
+
report.sourceChangesDuringRun = Object.keys({ ...initialSources, ...finalSources }).filter((file) => initialSources[file] !== finalSources[file]);
|
|
363
|
+
report.sourceUnchangedDuringRun = report.sourceChangesDuringRun.length === 0;
|
|
364
|
+
report.sourceFiles = Object.fromEntries(Object.entries(initialSources).sort(([a], [b]) => a.localeCompare(b)));
|
|
365
|
+
report.completedAt = new Date().toISOString();
|
|
366
|
+
flush();
|
|
367
|
+
}
|
|
368
|
+
console.log(JSON.stringify({ receipt: output, providers: Object.fromEntries(Object.entries(report.providers).map(([p, r]) => [p, r.status])), cleanup: report.cleanup }));
|
|
369
|
+
process.exitCode = report.fatalError || report.communications?.status === 'failed' || !report.sourceUnchangedDuringRun || Object.values(report.providers).some((p) => p.status !== 'passed') || !report.cleanup.runtimeStopped ? 1 : 0;
|