@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.
- package/CHANGELOG.md +72 -0
- package/docs/codex-account.zh-CN.md +38 -0
- package/docs/codex-protocol.md +59 -0
- package/docs/commands.md +9 -0
- package/docs/commands.zh-CN.md +7 -0
- package/docs/macos-process-identity.md +2 -0
- package/docs/native.md +179 -14
- package/docs/native.zh-CN.md +136 -6
- package/docs/web-handoff.zh-CN.md +118 -8
- package/lib/cli/app.mjs +47 -4
- package/lib/cli/commands/coordination.mjs +14 -4
- package/lib/cli/commands/dsh.mjs +1 -1
- package/lib/cli/commands/gc.mjs +22 -15
- package/lib/cli/commands/hook.mjs +19 -3
- package/lib/cli/commands/install.mjs +80 -10
- package/lib/cli/commands/misc.mjs +36 -9
- package/lib/cli/commands/native.mjs +4 -38
- package/lib/cli/commands/peer.mjs +3 -0
- package/lib/cli/commands/run.mjs +13 -6
- package/lib/cli/commands/tmux.mjs +1 -2
- package/lib/cli-runtime.mjs +19 -2
- package/lib/core/coordination/results.mjs +27 -10
- package/lib/core/sessions/hook-root-identity.mjs +47 -0
- package/lib/core/sessions/launch.mjs +7 -1
- package/lib/db/connection.mjs +140 -42
- package/lib/db/schema.mjs +59 -0
- package/lib/diagnostics/codex-protocol.mjs +105 -0
- package/lib/guidance.mjs +21 -36
- package/lib/integrations/codex-account.mjs +143 -0
- package/lib/integrations/codex-interactions.mjs +13 -11
- package/lib/integrations/dsh-collaboration.mjs +6 -12
- package/lib/integrations/dsh-cordis.mjs +17 -13
- package/lib/integrations/dsh.mjs +16 -11
- package/lib/integrations/mcp-elicitation.mjs +116 -0
- package/lib/integrations/mcp-url-elicitation.mjs +25 -0
- package/lib/integrations/native/claude.mjs +24 -6
- package/lib/integrations/native/codex.mjs +38 -7
- package/lib/integrations/native/dsh-acp.mjs +67 -24
- package/lib/integrations/native/index.mjs +4 -1
- package/lib/integrations/native/interactions.mjs +2 -1
- package/lib/integrations/native/jsonrpc.mjs +37 -5
- package/lib/integrations/native/telemetry.mjs +30 -0
- package/lib/process/identity.mjs +4 -0
- package/lib/process/pinned-cwd.mjs +302 -0
- package/lib/process/pinned-guidance.mjs +138 -0
- package/lib/process/pinned-project-state.mjs +105 -0
- package/lib/process/selected-cwd-identity.mjs +95 -0
- package/lib/runtime/buffer-evidence.mjs +2 -1
- package/lib/runtime/client.mjs +9 -0
- package/lib/runtime/native/client.mjs +4 -1
- package/lib/runtime/native/history.mjs +140 -0
- package/lib/runtime/native/launcher.mjs +106 -0
- package/lib/runtime/native/service.mjs +64 -8
- package/lib/runtime/native/store.mjs +83 -58
- package/lib/runtime/paths.mjs +17 -2
- package/lib/runtime/private-file.mjs +76 -0
- package/lib/runtime/private-state.mjs +546 -0
- package/lib/runtime/project-context.mjs +2 -1
- package/lib/runtime/project-path.mjs +92 -20
- package/lib/runtime/project-trust.mjs +79 -0
- package/lib/runtime/projects.mjs +9 -3
- package/lib/runtime/state-migration.mjs +697 -0
- package/lib/runtime/state.mjs +69 -27
- package/lib/shared/automatic-result-key.mjs +6 -0
- package/lib/shared/json-file.mjs +55 -6
- package/lib/shared/redact.mjs +2 -1
- package/lib/ui/help.mjs +6 -4
- package/lib/web/agent-defaults.mjs +110 -0
- package/lib/web/auto-attach.mjs +3 -2
- package/lib/web/browser/contracts.d.ts +10 -0
- package/lib/web/browser/core.mjs +1997 -0
- package/lib/web/browser/project-data.mjs +158 -0
- package/lib/web/browser/session-sync.mjs +82 -0
- package/lib/web/browser-assets.mjs +19 -0
- package/lib/web/buffer-gc-runtime.mjs +2 -2
- package/lib/web/codex-app-server.mjs +111 -37
- package/lib/web/codex-sessions.mjs +61 -12
- package/lib/web/context-files.mjs +82 -0
- package/lib/web/cookie-auth.mjs +51 -3
- package/lib/web/external-sessions.mjs +76 -9
- package/lib/web/http-routes.mjs +238 -25
- package/lib/web/http.mjs +50 -4
- package/lib/web/liveness-reaper.mjs +2 -2
- package/lib/web/native-sessions.mjs +323 -14
- package/lib/web/project-contexts.mjs +236 -44
- package/lib/web/project-file-write.mjs +249 -0
- package/lib/web/project-files.mjs +268 -0
- package/lib/web/pty-sessions.mjs +11 -3
- package/lib/web/runtime-main.mjs +132 -80
- package/lib/web/session-serialize.mjs +87 -8
- package/lib/web/session-state-sync.mjs +98 -0
- package/lib/web/shutdown-diagnostics.mjs +38 -0
- package/lib/web/shutdown-response.mjs +21 -0
- package/lib/web/shutdown-state.mjs +91 -0
- package/lib/web/startup.mjs +103 -35
- package/lib/web/task-results.mjs +19 -19
- package/lib/web/tls.mjs +84 -33
- package/lib/web/tmux-sessions.mjs +73 -9
- package/lib/web/tmux-stream.mjs +36 -8
- package/lib/web/ui-agent-defaults.mjs +149 -0
- package/lib/web/ui-agent-start.mjs +330 -0
- package/lib/web/ui-codex-account.mjs +81 -0
- package/lib/web/ui-codex.mjs +105 -85
- package/lib/web/ui-command-palette.mjs +21 -5
- package/lib/web/ui-file-writes.mjs +220 -0
- package/lib/web/ui-files.mjs +231 -0
- package/lib/web/ui-handoff.mjs +10 -2
- package/lib/web/ui-history.mjs +175 -105
- package/lib/web/ui-i18n.mjs +280 -8
- package/lib/web/ui-interactions.mjs +107 -6
- package/lib/web/ui-native-timeline.mjs +326 -0
- package/lib/web/ui-native.mjs +93 -43
- package/lib/web/ui-review.mjs +5 -3
- package/lib/web/ui-safe-markdown.mjs +79 -0
- package/lib/web/ui-session-tools.mjs +224 -0
- package/lib/web/ui-template.mjs +29 -1968
- package/lib/web/ui-terminal-find.mjs +5 -3
- package/lib/web/ui-workbench.mjs +5 -3
- package/lib/web/ui-workspace.mjs +3 -0
- package/package.json +8 -2
- package/scripts/benchmark-session-sync.mjs +53 -0
- package/scripts/codex-protocol.mjs +88 -0
- package/scripts/dsh-approval-layers-acceptance.mjs +172 -0
- package/scripts/dsh-package-provenance.mjs +118 -0
- package/scripts/mcp-form-live-fixture.mjs +81 -0
- package/scripts/native-bounded-live-acceptance.mjs +193 -0
- package/scripts/native-fixture-root.mjs +49 -0
- package/scripts/native-installed-acceptance.mjs +611 -0
- package/scripts/native-stability-checks.mjs +40 -0
- package/scripts/regression-cookie-expiry.mjs +70 -0
- package/scripts/regression-http-transport.mjs +9 -0
- package/scripts/regression-terminal-marker.mjs +95 -0
- package/scripts/regression.mjs +481 -219
- package/scripts/shim-reentry-probe.mjs +105 -0
- package/scripts/web-acceptance-diagnostics.mjs +90 -0
- package/scripts/web-codex-account-acceptance.mjs +178 -0
- package/scripts/web-file-preview-checks.mjs +302 -0
- package/scripts/web-mcp-form-acceptance.mjs +134 -0
- package/scripts/web-mcp-form-live-acceptance.mjs +368 -0
- package/scripts/web-native-handoff-checks.mjs +128 -0
- package/scripts/web-native-interaction-acceptance.mjs +219 -18
- package/scripts/web-native-interaction-diagnostics.mjs +80 -0
- package/scripts/web-native-model-acceptance.mjs +22 -7
- package/scripts/web-session-tools-checks.mjs +112 -0
- package/scripts/web-workbench-acceptance.mjs +435 -0
- package/scripts/web-workbench-installed-acceptance.mjs +122 -0
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
// Opt-in acceptance of a locally installed HCC package through its public CLI.
|
|
2
|
+
// All projects, provider homes and processes are test-owned. No package install
|
|
3
|
+
// or publication occurs here; --run-live uses existing provider authentication.
|
|
4
|
+
import assert from 'node:assert/strict';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import net from 'node:net';
|
|
9
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
11
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
12
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
13
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
14
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
15
|
+
import { nativeRequest } from '../lib/runtime/native/client.mjs';
|
|
16
|
+
import { readNativePointer, nativePaths } from '../lib/runtime/native/store.mjs';
|
|
17
|
+
import { stabilityOptions, assertStabilityEvidence, stabilityPayload } from './native-stability-checks.mjs';
|
|
18
|
+
import { redactSecrets } from '../lib/shared/redact.mjs';
|
|
19
|
+
|
|
20
|
+
const args = process.argv.slice(2);
|
|
21
|
+
const option = (name, fallback) => args.includes(name) ? args[args.indexOf(name) + 1] : fallback;
|
|
22
|
+
if (!args.includes('--run-live')) {
|
|
23
|
+
console.log('Usage: node scripts/native-installed-acceptance.mjs --run-live --codex-bin PATH --dsh-bin PATH [--archive FILE] [--browser] [--task] [--stability [--stability-only]] [--output FILE]');
|
|
24
|
+
console.log('Install the HCC archive and optional Claude SDK first. Browser checks need HCC_ACCEPTANCE_PLAYWRIGHT; models consume existing provider quota.');
|
|
25
|
+
process.exit(0);
|
|
26
|
+
}
|
|
27
|
+
const stabilityConfig = stabilityOptions(args);
|
|
28
|
+
const stabilityOnly = args.includes('--stability-only');
|
|
29
|
+
const repo = fs.realpathSync(fileURLToPath(new URL('..', import.meta.url)));
|
|
30
|
+
const cli = path.join(repo, 'bin/hcc.mjs');
|
|
31
|
+
const digest = file => createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
32
|
+
function sourceHashes(dir = path.join(repo, 'lib')) {
|
|
33
|
+
return Object.fromEntries(fs.readdirSync(dir, { withFileTypes: true }).flatMap(entry => {
|
|
34
|
+
const file = path.join(dir, entry.name);
|
|
35
|
+
return entry.isDirectory() ? Object.entries(sourceHashes(file)) : entry.name.endsWith('.mjs') ? [[path.relative(repo, file), digest(file)]] : [];
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
const initialSources = sourceHashes();
|
|
39
|
+
const sandbox = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'hcc-installed-native-')));
|
|
40
|
+
fs.chmodSync(sandbox, 0o700);
|
|
41
|
+
const root = path.join(sandbox, 'project');
|
|
42
|
+
const ctx = { root, cwd: root, dbPath: path.join(root, '.hello-cc/mesh.db') };
|
|
43
|
+
const output = path.resolve(option('--output', path.join(sandbox, 'receipt.json')));
|
|
44
|
+
const report = { startedAt: new Date().toISOString(), scope: 'installed public CLI, authenticated lifecycle, cross-provider messages, optional browser, bounded coding task and opt-in sustained stability',
|
|
45
|
+
package: { directory: repo, version: JSON.parse(fs.readFileSync(path.join(repo, 'package.json'))).version,
|
|
46
|
+
...(option('--archive') ? { archiveSha256: digest(path.resolve(option('--archive'))) } : {}) },
|
|
47
|
+
node: process.version, sandbox, providers: {}, checks: [], cleanup: {}, sourceFiles: initialSources,
|
|
48
|
+
acceptanceMode: stabilityOnly ? 'stability-only' : 'full', factoryInjected: false, queryInjected: false, authentication: 'existing route copied into test-owned private homes', screenshots: [], pageErrors: [], consoleErrors: [], limitations: [] };
|
|
49
|
+
const credentials = [];
|
|
50
|
+
const originalCodexHome = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
|
|
51
|
+
const protectedFiles = ['config.toml', 'auth.json'].map(name => path.join(originalCodexHome, name)).filter(file => fs.existsSync(file));
|
|
52
|
+
const protectedHashes = protectedFiles.map(file => [file, digest(file)]);
|
|
53
|
+
let env, db, browser, runtimeStarted = false, webStarted = false;
|
|
54
|
+
const homes = {};
|
|
55
|
+
const binaries = { codex: option('--codex-bin', '/opt/homebrew/bin/codex'), dsh: option('--dsh-bin', 'dsh') };
|
|
56
|
+
const tmux = process.env.HCC_ACCEPTANCE_TMUX || '/opt/homebrew/bin/tmux';
|
|
57
|
+
const socket = `hcc-installed-${randomUUID()}`;
|
|
58
|
+
const terminal = new Set(['completed', 'failed', 'uncertain']);
|
|
59
|
+
function check(name, details = {}) { report.checks.push({ name, passed: true, ...details }); console.log(JSON.stringify({ check: name, ...details })); }
|
|
60
|
+
function flush() { fs.mkdirSync(path.dirname(output), { recursive: true }); fs.writeFileSync(output, JSON.stringify(redactSecrets(report), null, 2) + '\n', { mode: 0o600 }); }
|
|
61
|
+
function privateDir(name) { const dir = path.join(sandbox, name); credentials.push(dir); fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); return dir; }
|
|
62
|
+
function hcc(...params) {
|
|
63
|
+
const result = spawnSync(process.execPath, [cli, '--root', root, '--json', ...params], { cwd: root, env, encoding: 'utf8', timeout: 45000 });
|
|
64
|
+
if (result.status !== 0) throw new Error(`Installed CLI ${params.slice(0, 2).join(' ')} failed: ${redactSecrets(result.stderr || result.stdout)}`);
|
|
65
|
+
if (!result.stdout.trim()) return null;
|
|
66
|
+
if (params[0] !== 'native') return { output: result.stdout.trim() };
|
|
67
|
+
const reply = JSON.parse(result.stdout);
|
|
68
|
+
assert.equal(reply.ok, true, 'CLI response must confirm success');
|
|
69
|
+
return reply.data;
|
|
70
|
+
}
|
|
71
|
+
const api = (method, route, body = null) => nativeRequest(ctx, method, route, body, { timeoutMs: 45000 });
|
|
72
|
+
const observedEvents = new Map();
|
|
73
|
+
const eventCursors = new Map();
|
|
74
|
+
async function state(peer) {
|
|
75
|
+
const current = await api('GET', `/workers/${encodeURIComponent(peer)}/state?after=${eventCursors.get(peer) || 0}`);
|
|
76
|
+
const collected = [...(observedEvents.get(peer) || []), ...current.events].slice(-500);
|
|
77
|
+
if (current.events.length) eventCursors.set(peer, current.events.at(-1).id);
|
|
78
|
+
observedEvents.set(peer, collected);
|
|
79
|
+
return { ...current, events: collected };
|
|
80
|
+
}
|
|
81
|
+
async function until(predicate, label, timeoutMs = 150000) {
|
|
82
|
+
const deadline = Date.now() + timeoutMs; let nextNotice = Date.now() + 25000;
|
|
83
|
+
while (Date.now() < deadline) {
|
|
84
|
+
const result = await predicate(); if (result) return result;
|
|
85
|
+
if (Date.now() >= nextNotice) { console.log(JSON.stringify({ waiting: label })); nextNotice += 25000; }
|
|
86
|
+
await delay(150);
|
|
87
|
+
}
|
|
88
|
+
throw new Error(`Timed out: ${label}`);
|
|
89
|
+
}
|
|
90
|
+
function message(peer, body, from = 'acceptance-local') {
|
|
91
|
+
report.providers[peer.slice(8)].userSubmissions++;
|
|
92
|
+
return hcc('native', 'send', '--peer', peer, '--from', from, '--body', body);
|
|
93
|
+
}
|
|
94
|
+
async function finish(peer, sent, handleApproval) {
|
|
95
|
+
const receipt = await until(async () => {
|
|
96
|
+
const current = await state(peer);
|
|
97
|
+
for (const request of current.snapshot.pendingApprovals || []) {
|
|
98
|
+
assert.ok(handleApproval, `Unexpected approval ${request.method}`);
|
|
99
|
+
await handleApproval(request, current);
|
|
100
|
+
}
|
|
101
|
+
const row = current.deliveries.find(value => value.message_id === sent.message_id);
|
|
102
|
+
return terminal.has(row?.state) ? row : null;
|
|
103
|
+
}, `${peer} message ${sent.message_id}`);
|
|
104
|
+
assert.equal(receipt.state, 'completed', JSON.stringify(redactSecrets(receipt.detail || receipt.evidence || receipt)));
|
|
105
|
+
const reply = db.prepare("SELECT * FROM messages WHERE reply_to=? AND kind='reply'").get(sent.message_id);
|
|
106
|
+
assert.ok(reply, 'successful request must write a reply');
|
|
107
|
+
assert.equal(reply.thread_id, sent.message_id);
|
|
108
|
+
assert.ok(db.prepare('SELECT read_at FROM message_reads WHERE peer=? AND message_id=?').get(peer, sent.message_id));
|
|
109
|
+
return { receipt, reply };
|
|
110
|
+
}
|
|
111
|
+
function respond(peer, request, decision = 'decline') {
|
|
112
|
+
return hcc('native', 'respond', '--peer', peer, '--request', String(request.requestId), '--decision', decision);
|
|
113
|
+
}
|
|
114
|
+
async function start(provider, resume = false) {
|
|
115
|
+
return hcc('native', 'start', '--peer', `install-${provider}`, '--provider', provider,
|
|
116
|
+
...(provider === 'claude' ? [] : ['--binary', binaries[provider]]), ...(resume ? ['--resume', 'last'] : []));
|
|
117
|
+
}
|
|
118
|
+
async function lifecycle(provider) {
|
|
119
|
+
const peer = `install-${provider}`;
|
|
120
|
+
const result = report.providers[provider] = { status: 'running', userSubmissions: 0, launcher: 'installed public hcc native start CLI' };
|
|
121
|
+
await start(provider);
|
|
122
|
+
result.version = provider === 'claude' ? report.claudeSdk.version : execFileSync(binaries[provider], ['--version'], { env, encoding: 'utf8', timeout: 10000 }).trim();
|
|
123
|
+
const nonce = `HCC_INSTALLED_${randomUUID().replaceAll('-', '').slice(0, 16)}`;
|
|
124
|
+
const body = `Remember ${nonce} in this conversation. Reply with only ${nonce}. Do not use tools or read files.`;
|
|
125
|
+
const first = message(peer, body);
|
|
126
|
+
const duplicate = await api('POST', '/send', { peer, from: 'acceptance-local', body, submissionId: first.submission_id });
|
|
127
|
+
assert.equal(duplicate.message_id, first.message_id);
|
|
128
|
+
assert.equal((await finish(peer, first)).reply.body.trim(), nonce);
|
|
129
|
+
result.sessionId = (await state(peer)).snapshot.sessionId; assert.ok(result.sessionId);
|
|
130
|
+
check(`${provider}: installed CLI real reply, correlation, ACK, submission deduplication`);
|
|
131
|
+
const second = message(peer, 'Without tools, return only the full original test marker from the earlier conversation, including its HCC_INSTALLED_ prefix and the original hexadecimal suffix.');
|
|
132
|
+
assert.equal((await finish(peer, second)).reply.body.trim(), nonce);
|
|
133
|
+
check(`${provider}: continuing turn retains context`);
|
|
134
|
+
hcc('native', 'close', '--peer', peer); await start(provider, true);
|
|
135
|
+
const resumed = message(peer, 'Without tools, return only the full original test marker from the earlier conversation, including its HCC_INSTALLED_ prefix and the original hexadecimal suffix.');
|
|
136
|
+
assert.equal((await finish(peer, resumed)).reply.body.trim(), nonce);
|
|
137
|
+
assert.equal((await state(peer)).snapshot.sessionId, result.sessionId);
|
|
138
|
+
check(`${provider}: installed close/resume retains session identity and history`);
|
|
139
|
+
const long = message(peer, 'No tools. Immediately write the numbers 1 through 100000, one per line. Continue until every number is written.');
|
|
140
|
+
await until(async () => {
|
|
141
|
+
const current = await state(peer);
|
|
142
|
+
return current.active_delivery?.message_id === long.message_id && ['busy', 'running'].includes(current.snapshot.status);
|
|
143
|
+
}, `${provider} active model request`, 45000);
|
|
144
|
+
// Runtime inspection deliberately omits streamed delta/output payloads.
|
|
145
|
+
// A matching busy/running provider turn is authoritative admission evidence;
|
|
146
|
+
// interrupt it immediately rather than waiting for an unpersisted delta.
|
|
147
|
+
if (provider === 'dsh') await delay(500);
|
|
148
|
+
const active = await state(peer); assert.ok(active.active_delivery);
|
|
149
|
+
const interrupt = hcc('native', 'interrupt', '--peer', peer, '--turn', active.snapshot.turnId);
|
|
150
|
+
const interrupted = await until(async () => {
|
|
151
|
+
const row = (await state(peer)).deliveries.find(value => value.message_id === long.message_id);
|
|
152
|
+
return terminal.has(row?.state) ? row : null;
|
|
153
|
+
}, `${provider} interruption terminal evidence`);
|
|
154
|
+
assert.equal(interrupted.state, 'failed');
|
|
155
|
+
assert.equal(db.prepare('SELECT COUNT(*) AS n FROM messages WHERE reply_to=?').get(long.message_id).n, 0);
|
|
156
|
+
assert.equal(db.prepare('SELECT COUNT(*) AS n FROM message_reads WHERE peer=? AND message_id=?').get(peer, long.message_id).n, 0);
|
|
157
|
+
check(`${provider}: active interruption has terminal evidence without reply or ACK`, { interruptStatus: interrupt.status });
|
|
158
|
+
// Failed/interrupted turns may leave the provider unavailable; reopen only
|
|
159
|
+
// this test-owned saved session before the subsequent communication checks.
|
|
160
|
+
hcc('native', 'close', '--peer', peer); await start(provider, true);
|
|
161
|
+
if (provider === 'codex') {
|
|
162
|
+
const boundary = fs.mkdtempSync(path.join(os.homedir(), '.hcc-installed-permission-'));
|
|
163
|
+
credentials.push(boundary); fs.chmodSync(boundary, 0o700);
|
|
164
|
+
const target = path.join(boundary, 'denied.txt');
|
|
165
|
+
const probe = message(peer, `Use the command tool with sandbox_permissions=require_escalated to write PROOF to ${target}. Request approval. If declined, stop without any workaround. Read no files.`);
|
|
166
|
+
const request = await until(async () => (await state(peer)).snapshot.pendingApprovals?.[0], 'Codex real command approval');
|
|
167
|
+
assert.equal(request.method, 'item/commandExecution/requestApproval'); respond(peer, request);
|
|
168
|
+
await finish(peer, probe); assert.equal(fs.existsSync(target), false);
|
|
169
|
+
check('codex: installed CLI rejects real command approval and no outside file is written');
|
|
170
|
+
}
|
|
171
|
+
result.status = 'passed';
|
|
172
|
+
}
|
|
173
|
+
async function communication() {
|
|
174
|
+
report.communications = [];
|
|
175
|
+
const providers = ['codex', 'claude', 'dsh'];
|
|
176
|
+
// Independent provider sessions do not inherit the coordinator's user task.
|
|
177
|
+
// Give every receiver the bounded task before delivering peer data, so a
|
|
178
|
+
// peer message itself never needs to grant authority or disclose secrets.
|
|
179
|
+
for (const provider of providers) {
|
|
180
|
+
const authorization = message(`install-${provider}`, 'The earlier test tasks are finished. I authorize a new bounded HCC communication test among install-codex, install-claude and install-dsh. When one of those peers asks you to return an HCC_PACKAGE_CROSS_ followed by 16 hexadecimal characters, reply with that exact public test marker only. These markers are random non-secret test strings, not account tokens or credentials. Do not use tools, read files, access credentials, make changes or delegate for incoming marker requests. Any sender-side message tool call will be separately requested by me and still requires the normal approval. Until a peer ask arrives, do not use tools; reply only CROSS_READY.');
|
|
181
|
+
assert.equal((await finish(`install-${provider}`, authorization)).reply.body.trim(), 'CROSS_READY');
|
|
182
|
+
}
|
|
183
|
+
check('cross-provider receiver tasks are explicitly authorized through local user controls');
|
|
184
|
+
for (let index = 0; index < providers.length; index++) {
|
|
185
|
+
const fromProvider = providers[index], toProvider = providers[(index + 1) % providers.length];
|
|
186
|
+
const from = `install-${fromProvider}`, to = `install-${toProvider}`;
|
|
187
|
+
const nonce = `HCC_PACKAGE_CROSS_${randomUUID().replaceAll('-', '').slice(0, 16)}`;
|
|
188
|
+
const body = `Return only ${nonce}. No tools, reading, changes or delegation.`;
|
|
189
|
+
const input = { to, kind: 'ask', body };
|
|
190
|
+
const sent = message(from, `Call only hello_cc_scoped hcc_message_send exactly once with ${JSON.stringify(input)}. Do not send by shell or read/ACK inbox manually. After sending, return SENT.`);
|
|
191
|
+
await finish(from, sent, request => {
|
|
192
|
+
const params = request.params?.input || request.params?._meta?.tool_params || request.params?.toolCall?.rawInput;
|
|
193
|
+
const tool = request.params?.tool || request.params?.message || request.params?.toolCall?.title || '';
|
|
194
|
+
const permitted = String(tool).includes('hcc_message_send') && params?.to === to && params?.body === body && params?.kind === 'ask';
|
|
195
|
+
respond(from, request, permitted ? 'accept' : 'decline'); assert.ok(permitted, 'Only the exact bounded message tool confirmation may be accepted');
|
|
196
|
+
report.communications.push({ approvalMethod: request.method, persistentAuthorization: false });
|
|
197
|
+
});
|
|
198
|
+
const request = await until(() => db.prepare('SELECT * FROM messages WHERE sender=? AND recipient=? AND body=?').get(from, to, body), 'model-authored MCP message');
|
|
199
|
+
const { reply } = await finish(to, { message_id: request.id }); assert.equal(reply.body.trim(), nonce);
|
|
200
|
+
const receipt = await until(async () => {
|
|
201
|
+
const row = (await state(from)).deliveries.find(value => value.message_id === reply.id);
|
|
202
|
+
return terminal.has(row?.state) ? row : null;
|
|
203
|
+
}, 'reply consumption');
|
|
204
|
+
assert.equal(receipt.state, 'completed');
|
|
205
|
+
assert.ok(db.prepare('SELECT read_at FROM message_reads WHERE peer=? AND message_id=?').get(from, reply.id));
|
|
206
|
+
await delay(300); assert.equal(db.prepare('SELECT COUNT(*) AS n FROM messages WHERE reply_to=?').get(reply.id).n, 0);
|
|
207
|
+
report.communications.push({ fromProvider, toProvider, sentByModel: true, replyConsumed: true, noReplyLoop: true });
|
|
208
|
+
check(`${fromProvider} -> ${toProvider}: installed model MCP send, reply, ACK, no loop`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
async function stability() {
|
|
212
|
+
const providers = ['codex', 'claude', 'dsh'];
|
|
213
|
+
const begun = Date.now();
|
|
214
|
+
const run = report.stability = { config: stabilityConfig, startedAt: new Date().toISOString(),
|
|
215
|
+
completed: false, cycles: [], probes: [], recoveries: [], samples: [], failures: [], automaticReplay: false };
|
|
216
|
+
const markers = new Map(), previous = new Map(), sessions = new Map();
|
|
217
|
+
const observedPids = new Set();
|
|
218
|
+
function sample(label) {
|
|
219
|
+
const pointer = readNativePointer(ctx);
|
|
220
|
+
assert.ok(pointer?.pid, 'stability runtime must remain available');
|
|
221
|
+
const result = spawnSync('ps', ['-axo', 'pid=,ppid=,rss=,comm='], { encoding: 'utf8', timeout: 10000 });
|
|
222
|
+
const rows = result.status === 0 ? result.stdout.split('\n').flatMap(line => {
|
|
223
|
+
const match = line.match(/^\s*(\d+)\s+(\d+)\s+(\d+)\s+(.+)$/);
|
|
224
|
+
return match ? [{ pid: Number(match[1]), ppid: Number(match[2]), rssKiB: Number(match[3]), executable: match[4] }] : [];
|
|
225
|
+
}) : [];
|
|
226
|
+
const owned = new Set([pointer.pid]);
|
|
227
|
+
for (let changed = true; changed;) {
|
|
228
|
+
changed = false;
|
|
229
|
+
for (const row of rows) if (owned.has(row.ppid) && !owned.has(row.pid)) { owned.add(row.pid); changed = true; }
|
|
230
|
+
}
|
|
231
|
+
const processes = rows.filter(row => owned.has(row.pid));
|
|
232
|
+
for (const row of processes) observedPids.add(row.pid);
|
|
233
|
+
run.samples.push({ at: new Date().toISOString(), elapsedMs: Date.now() - begun, label,
|
|
234
|
+
daemonPid: pointer.pid, generation: pointer.generation, processes,
|
|
235
|
+
totalRssKiB: processes.reduce((total, row) => total + row.rssKiB, 0),
|
|
236
|
+
resourceDataAvailable: result.status === 0,
|
|
237
|
+
meshDbBytes: fs.statSync(ctx.dbPath).size, nativeDbBytes: fs.statSync(nativePaths(ctx).db).size });
|
|
238
|
+
flush();
|
|
239
|
+
}
|
|
240
|
+
async function probe(provider, prompt, expected, kind) {
|
|
241
|
+
const peer = `install-${provider}`;
|
|
242
|
+
const item = { provider, kind, startedAt: new Date().toISOString(), expected, passed: false };
|
|
243
|
+
run.probes.push(item); flush();
|
|
244
|
+
try {
|
|
245
|
+
const sent = message(peer, prompt); Object.assign(item, { messageId: sent.message_id, submissionId: sent.submission_id });
|
|
246
|
+
const { receipt, reply } = await finish(peer, sent);
|
|
247
|
+
const current = await state(peer);
|
|
248
|
+
Object.assign(item, { state: receipt.state, actual: reply.body.trim(), replyId: reply.id,
|
|
249
|
+
replyCount: db.prepare("SELECT COUNT(*) AS n FROM messages WHERE reply_to=? AND kind='reply'").get(sent.message_id).n,
|
|
250
|
+
ackCount: db.prepare('SELECT COUNT(*) AS n FROM message_reads WHERE peer=? AND message_id=?').get(peer, sent.message_id).n,
|
|
251
|
+
replyThread: reply.thread_id, sessionId: current.snapshot.sessionId, owner: current.owner, turnId: receipt.turn_id });
|
|
252
|
+
item.contextPayload = stabilityPayload(item.actual); item.responseFormatExact = item.contextPayload === item.actual;
|
|
253
|
+
assertStabilityEvidence(item);
|
|
254
|
+
assert.equal(item.sessionId, sessions.get(provider), 'stability must retain the original provider session');
|
|
255
|
+
item.passed = true; item.elapsedMs = Date.now() - Date.parse(item.startedAt);
|
|
256
|
+
return sent;
|
|
257
|
+
} catch (error) {
|
|
258
|
+
item.error = { code: error.code || 'STABILITY_FAILED', message: error.message };
|
|
259
|
+
run.failures.push({ ...item }); throw error;
|
|
260
|
+
} finally { flush(); }
|
|
261
|
+
}
|
|
262
|
+
async function together(action) {
|
|
263
|
+
const results = await Promise.allSettled(providers.map(action));
|
|
264
|
+
const rejected = results.filter(result => result.status === 'rejected');
|
|
265
|
+
if (rejected.length) throw rejected[0].reason;
|
|
266
|
+
}
|
|
267
|
+
async function resumeAll(kind) {
|
|
268
|
+
const record = { kind, startedAt: new Date().toISOString(), providers: [], passed: false };
|
|
269
|
+
run.recoveries.push(record); flush();
|
|
270
|
+
if (kind === 'daemon-restart') {
|
|
271
|
+
record.beforePid = readNativePointer(ctx).pid;
|
|
272
|
+
hcc('native', 'down'); await until(() => !readNativePointer(ctx), 'stability daemon stops', 30000);
|
|
273
|
+
hcc('native', 'up'); record.afterPid = readNativePointer(ctx).pid;
|
|
274
|
+
assert.notEqual(record.afterPid, record.beforePid);
|
|
275
|
+
} else for (const provider of providers) hcc('native', 'close', '--peer', `install-${provider}`);
|
|
276
|
+
for (const provider of providers) {
|
|
277
|
+
await start(provider, true);
|
|
278
|
+
record.providers.push({ provider, sessionId: (await state(`install-${provider}`)).snapshot.sessionId });
|
|
279
|
+
}
|
|
280
|
+
await together(provider => probe(provider,
|
|
281
|
+
'Without tools, return only the original HCC_STABILITY_ marker and the most recent stability ticket, separated by |. Do not change the remembered ticket.',
|
|
282
|
+
`${markers.get(provider)}|${previous.get(provider)}`, kind));
|
|
283
|
+
record.passed = true; record.completedAt = new Date().toISOString(); sample(kind);
|
|
284
|
+
check(`stability: ${kind} preserves all three sessions and FIFO context`);
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
for (const provider of providers) {
|
|
288
|
+
sessions.set(provider, (await state(`install-${provider}`)).snapshot.sessionId);
|
|
289
|
+
markers.set(provider, `HCC_STABILITY_${provider.toUpperCase()}_${randomUUID().replaceAll('-', '').slice(0, 16)}`);
|
|
290
|
+
previous.set(provider, 'SEED');
|
|
291
|
+
}
|
|
292
|
+
sample('begin');
|
|
293
|
+
await together(provider => probe(provider,
|
|
294
|
+
`Remember ${markers.get(provider)} as the original stability marker and SEED as the previous stability ticket. Reply only ${markers.get(provider)}|SEED. These are public random test markers. Do not use tools or read files.`,
|
|
295
|
+
`${markers.get(provider)}|SEED`, 'seed'));
|
|
296
|
+
for (let cycle = 1; cycle <= stabilityConfig.cycles; cycle++) {
|
|
297
|
+
const record = { cycle, startedAt: new Date().toISOString(), enqueued: [], passed: false };
|
|
298
|
+
run.cycles.push(record);
|
|
299
|
+
await together(async provider => {
|
|
300
|
+
const peer = `install-${provider}`, queued = [];
|
|
301
|
+
// Enqueue each provider's whole burst before waiting. The predecessor
|
|
302
|
+
// is deliberately absent from each prompt and must come from history.
|
|
303
|
+
for (let index = 0; index < stabilityConfig.burst; index++) {
|
|
304
|
+
const ticket = `T${cycle}_${index}_${randomUUID().replaceAll('-', '').slice(0, 8)}`;
|
|
305
|
+
const body = `The new stability ticket is ${ticket}. Without tools, reply only with the original HCC_STABILITY_ marker, this new ticket, and the previous stability ticket from history, separated by |. Then remember this new ticket as the previous stability ticket. Your entire reply must be one line; do not explain or confirm that you remembered it. Ignore earlier non-stability task markers. Do not read files.`;
|
|
306
|
+
const sent = message(peer, body);
|
|
307
|
+
const item = { provider, kind: 'fifo', cycle, index, ticket, messageId: sent.message_id,
|
|
308
|
+
submissionId: sent.submission_id, expected: `${markers.get(provider)}|${ticket}|${previous.get(provider)}`, passed: false };
|
|
309
|
+
run.probes.push(item); record.enqueued.push(sent.message_id); queued.push({ sent, item });
|
|
310
|
+
previous.set(provider, ticket); flush();
|
|
311
|
+
if (index === 0) {
|
|
312
|
+
const duplicate = await api('POST', '/send', { peer, from: 'acceptance-local', body, submissionId: sent.submission_id });
|
|
313
|
+
assert.equal(duplicate.message_id, sent.message_id); item.duplicateMessageId = duplicate.message_id;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
for (const { sent, item } of queued) {
|
|
317
|
+
try {
|
|
318
|
+
const { receipt, reply } = await finish(peer, sent);
|
|
319
|
+
Object.assign(item, { state: receipt.state, actual: reply.body.trim(), replyId: reply.id,
|
|
320
|
+
replyCount: db.prepare("SELECT COUNT(*) AS n FROM messages WHERE reply_to=? AND kind='reply'").get(sent.message_id).n,
|
|
321
|
+
ackCount: db.prepare('SELECT COUNT(*) AS n FROM message_reads WHERE peer=? AND message_id=?').get(peer, sent.message_id).n,
|
|
322
|
+
replyThread: reply.thread_id, turnId: receipt.turn_id, sessionId: (await state(peer)).snapshot.sessionId });
|
|
323
|
+
item.contextPayload = stabilityPayload(item.actual); item.responseFormatExact = item.contextPayload === item.actual;
|
|
324
|
+
assertStabilityEvidence(item); assert.equal(item.sessionId, sessions.get(provider)); item.passed = true;
|
|
325
|
+
} catch (error) {
|
|
326
|
+
item.error = { code: error.code || 'STABILITY_FAILED', message: error.message };
|
|
327
|
+
run.failures.push({ ...item }); throw error;
|
|
328
|
+
} finally { flush(); }
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
record.passed = true; record.completedAt = new Date().toISOString(); sample(`cycle-${cycle}`);
|
|
332
|
+
check(`stability: cycle ${cycle} completes ${providers.length * stabilityConfig.burst} queued turns in order with one reply/ACK each`);
|
|
333
|
+
if (cycle % stabilityConfig.resumeEvery === 0) await resumeAll('worker-close-resume');
|
|
334
|
+
if (cycle === Math.ceil(stabilityConfig.cycles / 2)) await resumeAll('daemon-restart');
|
|
335
|
+
const idleUntil = Date.now() + stabilityConfig.idleMs;
|
|
336
|
+
while (Date.now() < idleUntil) {
|
|
337
|
+
await delay(Math.min(10000, idleUntil - Date.now()));
|
|
338
|
+
for (const provider of providers) {
|
|
339
|
+
const current = await state(`install-${provider}`);
|
|
340
|
+
assert.equal(current.snapshot.sessionId, sessions.get(provider)); assert.equal(current.active_delivery, null);
|
|
341
|
+
assert.equal(current.snapshot.pendingApprovals?.length || 0, 0);
|
|
342
|
+
}
|
|
343
|
+
sample(`cycle-${cycle}-idle`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const nativeDb = new DatabaseSync(nativePaths(ctx).db, { readOnly: true });
|
|
347
|
+
try {
|
|
348
|
+
run.databaseAudit = {
|
|
349
|
+
meshQuickCheck: db.prepare('PRAGMA quick_check').get().quick_check,
|
|
350
|
+
nativeQuickCheck: nativeDb.prepare('PRAGMA quick_check').get().quick_check,
|
|
351
|
+
eventRows: nativeDb.prepare('SELECT COUNT(*) AS n FROM provider_events').get().n,
|
|
352
|
+
unfinished: nativeDb.prepare("SELECT COUNT(*) AS n FROM deliveries WHERE state NOT IN ('completed','failed','uncertain')").get().n
|
|
353
|
+
};
|
|
354
|
+
assert.equal(run.databaseAudit.meshQuickCheck, 'ok'); assert.equal(run.databaseAudit.nativeQuickCheck, 'ok');
|
|
355
|
+
assert.ok(run.databaseAudit.eventRows <= 2001, 'persisted diagnostic events must stay bounded');
|
|
356
|
+
assert.equal(run.databaseAudit.unfinished, 0);
|
|
357
|
+
for (const item of run.probes) {
|
|
358
|
+
const delivery = nativeDb.prepare('SELECT * FROM deliveries WHERE peer=? AND message_id=?').get(`install-${item.provider}`, item.messageId);
|
|
359
|
+
assert.equal(delivery?.state, 'completed'); assert.equal(delivery.submission_id, item.submissionId);
|
|
360
|
+
assert.equal(db.prepare("SELECT COUNT(*) AS n FROM messages WHERE reply_to=? AND kind='reply'").get(item.replyId).n, 0, 'no automatic reply loop');
|
|
361
|
+
}
|
|
362
|
+
} finally { nativeDb.close(); }
|
|
363
|
+
run.completed = true; check('stability: all persisted deliveries, database integrity, bounded events and no reply loops verified');
|
|
364
|
+
} catch (error) {
|
|
365
|
+
run.error = { code: error.code || 'STABILITY_FAILED', message: error.message };
|
|
366
|
+
run.failureState = [];
|
|
367
|
+
for (const provider of providers) {
|
|
368
|
+
try {
|
|
369
|
+
const current = await state(`install-${provider}`);
|
|
370
|
+
run.failureState.push({ provider, snapshot: current.snapshot, activeDelivery: current.active_delivery,
|
|
371
|
+
recentDeliveries: current.deliveries.slice(0, 8), recentEvents: current.events.slice(-16) });
|
|
372
|
+
} catch (failure) { run.failureState.push({ provider, error: failure.message }); }
|
|
373
|
+
}
|
|
374
|
+
throw error;
|
|
375
|
+
} finally {
|
|
376
|
+
run.elapsedMs = Date.now() - begun; run.completedAt = new Date().toISOString(); run.observedOwnedPids = [...observedPids];
|
|
377
|
+
report.limitations.push('Stability is a bounded authenticated workload and idle observation, not a 24-hour soak or a concurrency capacity benchmark. Resource samples are measurements, not proof of no leak.');
|
|
378
|
+
flush();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function browserFlow() {
|
|
383
|
+
assert.ok(process.env.HCC_ACCEPTANCE_PLAYWRIGHT, 'Set HCC_ACCEPTANCE_PLAYWRIGHT to the installed Playwright entry');
|
|
384
|
+
hcc('up', '--no-discover', '--no-guidance');
|
|
385
|
+
const port = await new Promise(resolve => { const listener = net.createServer(); listener.listen(0, '127.0.0.1', () => { const value = listener.address().port; listener.close(() => resolve(value)); }); });
|
|
386
|
+
hcc('web', '--local', '--port', String(port), '--no-discover', '--no-guidance'); webStarted = true;
|
|
387
|
+
const { chromium } = await import(process.env.HCC_ACCEPTANCE_PLAYWRIGHT);
|
|
388
|
+
browser = await chromium.launch({ headless: true, ...(process.env.HCC_ACCEPTANCE_CHROME ? { executablePath: process.env.HCC_ACCEPTANCE_CHROME } : {}) });
|
|
389
|
+
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
|
|
390
|
+
page.on('pageerror', error => report.pageErrors.push(error.message));
|
|
391
|
+
page.on('console', entry => { if (entry.type() === 'error') report.consoleErrors.push(entry.text()); });
|
|
392
|
+
page.on('dialog', dialog => dialog.accept());
|
|
393
|
+
const url = `http://127.0.0.1:${port}`;
|
|
394
|
+
await page.goto(url + '/?token=' + env.HCC_WEB_TOKEN);
|
|
395
|
+
assert.deepEqual(report.pageErrors, [], 'Browser initialization must succeed');
|
|
396
|
+
assert.deepEqual(report.consoleErrors, [], 'Installed browser assets must load');
|
|
397
|
+
await page.waitForFunction(() => window.hccHandoff?.sessions?.some(value => value.type === 'native'));
|
|
398
|
+
const title = await page.title(); assert.ok(/hello|hcc/i.test(title));
|
|
399
|
+
assert.ok((await page.locator('body').innerText()).length > 100);
|
|
400
|
+
const session = await page.evaluate(() => window.hccHandoff.sessions.find(value => value.type === 'native' && value.id === 'install-claude')?.id);
|
|
401
|
+
assert.ok(session);
|
|
402
|
+
if (await page.evaluate(() => window.hccHandoff.active) !== session) await page.locator(`#sessions [data-id="${session}"] .session-select`).click();
|
|
403
|
+
await page.waitForFunction(id => window.hccHandoff?.session?.id === id && Boolean(window.hccHandoff.actionToken), session);
|
|
404
|
+
if (!await page.evaluate(() => window.hccHandoff.canControl)) await page.locator('#claimControlBtn').click();
|
|
405
|
+
await page.waitForFunction(() => window.hccHandoff.canControl);
|
|
406
|
+
const identity = (await state('install-claude')).snapshot.sessionId;
|
|
407
|
+
const target = path.join(root, 'web-proof.txt');
|
|
408
|
+
const before = (await state('install-claude')).deliveries.map(value => value.message_id);
|
|
409
|
+
await page.locator('#nativeDraft').fill(`Use Write to write exactly HCC_INSTALLED_WEB_OK to ${target}. Wait for permission. Do not use Bash or workaround. End with WEB_OK.`);
|
|
410
|
+
await page.locator('#nativeSend').click(); report.providers.claude.userSubmissions++;
|
|
411
|
+
const delivery = await until(async () => (await state('install-claude')).deliveries.find(value => !before.includes(value.message_id)), 'Web submission');
|
|
412
|
+
const request = await until(async () => {
|
|
413
|
+
const current = await state('install-claude');
|
|
414
|
+
const approval = current.snapshot.pendingApprovals?.[0];
|
|
415
|
+
if (approval) return approval;
|
|
416
|
+
const row = current.deliveries.find(value => value.message_id === delivery.message_id);
|
|
417
|
+
assert.ok(!terminal.has(row?.state), 'Web task finished without the required Write approval: ' + (db.prepare('SELECT body FROM messages WHERE reply_to=?').get(delivery.message_id)?.body || row?.detail || row?.state));
|
|
418
|
+
return null;
|
|
419
|
+
}, 'Web Write approval');
|
|
420
|
+
assert.equal(request.method, 'claude/canUseTool'); assert.equal(request.params.tool, 'Write'); assert.equal(request.params.input.file_path, target);
|
|
421
|
+
await page.locator('#nativeApprovals button[data-decision="accept"]').waitFor({ timeout: 15000 });
|
|
422
|
+
const approvalShot = path.join(sandbox, 'web-approval.png'); await page.screenshot({ path: approvalShot }); report.screenshots.push(approvalShot);
|
|
423
|
+
await page.locator('#nativeApprovals button[data-decision="accept"]').click();
|
|
424
|
+
// A DOM click dispatches the async Web action; wait for the exact permission
|
|
425
|
+
// request to be resolved before asserting that no further approvals remain.
|
|
426
|
+
await until(async () => !(await state('install-claude')).snapshot.pendingApprovals?.some(value => value.requestId === request.requestId), 'Web approval response confirmation', 30000);
|
|
427
|
+
await finish('install-claude', delivery);
|
|
428
|
+
assert.equal(fs.readFileSync(target, 'utf8').trim(), 'HCC_INSTALLED_WEB_OK');
|
|
429
|
+
assert.equal((await state('install-claude')).snapshot.sessionId, identity);
|
|
430
|
+
await page.locator('#nativeRead').click();
|
|
431
|
+
await page.waitForFunction(id => {
|
|
432
|
+
const expected = '#' + id + ' · ' + window.hccHandoff.tr('native.delivery.completed', 'completed');
|
|
433
|
+
return [...document.querySelectorAll('#nativeReceipts strong')].some(element => element.textContent.trim() === expected);
|
|
434
|
+
}, delivery.message_id);
|
|
435
|
+
const screenshot = path.join(sandbox, 'web-completed.png'); await page.screenshot({ path: screenshot }); report.screenshots.push(screenshot);
|
|
436
|
+
await page.setViewportSize({ width: 390, height: 844 });
|
|
437
|
+
const mobileShot = path.join(sandbox, 'web-mobile.png'); await page.screenshot({ path: mobileShot }); report.screenshots.push(mobileShot);
|
|
438
|
+
await page.setViewportSize({ width: 1440, height: 1000 });
|
|
439
|
+
await page.locator('#releaseControlBtn').click(); await page.waitForFunction(() => !window.hccHandoff.canControl);
|
|
440
|
+
await browser.close(); browser = null;
|
|
441
|
+
hcc('down'); webStarted = false;
|
|
442
|
+
const followup = message('install-claude', 'Without tools, return only INSTALLED_LOCAL_RETURN_OK.');
|
|
443
|
+
assert.equal((await finish('install-claude', followup)).reply.body.trim(), 'INSTALLED_LOCAL_RETURN_OK');
|
|
444
|
+
assert.equal((await state('install-claude')).snapshot.sessionId, identity);
|
|
445
|
+
assert.equal((await api('GET', '/status')).workers.filter(value => value.owned).length, 3);
|
|
446
|
+
assert.deepEqual(report.pageErrors, []); assert.deepEqual(report.consoleErrors, []);
|
|
447
|
+
report.browser = { implementation: 'Browser plugin not available; installed Chrome with Playwright', url, title, viewports: ['1440x1000', '390x844'], realApproval: true, providerSessionRetained: true, independentWorkersSurviveWebShutdown: true };
|
|
448
|
+
check('installed Web: actual message, Write approval, file result, receipts and local return on the same session');
|
|
449
|
+
}
|
|
450
|
+
async function codingTask() {
|
|
451
|
+
const moduleFile = path.join(root, 'invoice.mjs');
|
|
452
|
+
const testFile = path.join(root, 'invoice.test.mjs');
|
|
453
|
+
fs.writeFileSync(testFile, `import { strict as assert } from 'node:assert';\nimport { test } from 'node:test';\nimport { invoiceTotal } from './invoice.mjs';\ntest('empty invoice', () => assert.deepEqual(invoiceTotal([], 0), { subtotalCents: 0, taxCents: 0, totalCents: 0 }));\ntest('multiple lines and tax rounding', () => assert.deepEqual(invoiceTotal([{ unitCents: 199, quantity: 3 }, { unitCents: 500, quantity: 2 }], 825), { subtotalCents: 1597, taxCents: 132, totalCents: 1729 }));\ntest('half-cent rounds up', () => assert.equal(invoiceTotal([{ unitCents: 1, quantity: 1 }], 5000).taxCents, 1));\ntest('reject fractional/negative values', () => { for (const line of [{unitCents: -1, quantity: 1}, {unitCents: 10, quantity: 0}, {unitCents: 1.5, quantity: 1}]) assert.throws(() => invoiceTotal([line], 0)); assert.throws(() => invoiceTotal([], 10001)); });\ntest('reject total overflow', () => assert.throws(() => invoiceTotal([{ unitCents: Number.MAX_SAFE_INTEGER, quantity: 2 }], 0)));\n`);
|
|
454
|
+
const contractSha256 = digest(testFile);
|
|
455
|
+
const spec = `Implement invoice.mjs exporting invoiceTotal(lines, taxBasisPoints). All money is integer cents. unitCents is a nonnegative safe integer, quantity a positive safe integer, taxBasisPoints an integer 0..10000. Throw on invalid inputs or safe-integer overflow. Return {subtotalCents,taxCents,totalCents}. Tax rounds the subtotal times basis points divided by 10000 to nearest cent, halves up; use exact integer arithmetic. Read invoice.test.mjs but do not modify it. Only write invoice.mjs using the file edit/apply_patch tool. You may read invoice.test.mjs with a file read tool or one cat command; do not run tests or any other commands. End with IMPLEMENTED.`;
|
|
456
|
+
const submitted = message('install-codex', spec);
|
|
457
|
+
await finish('install-codex', submitted, request => {
|
|
458
|
+
const files = request.params?.changes || request.params?.toolCall?.rawInput;
|
|
459
|
+
const allowed = request.method === 'item/fileChange/requestApproval' && JSON.stringify(files || request.params).includes('invoice.mjs');
|
|
460
|
+
respond('install-codex', request, allowed ? 'accept' : 'decline'); assert.ok(allowed, 'Unexpected implementation approval');
|
|
461
|
+
});
|
|
462
|
+
assert.ok(fs.existsSync(moduleFile));
|
|
463
|
+
assert.equal(digest(testFile), contractSha256, 'Model must not change the independent contract tests');
|
|
464
|
+
const test = spawnSync(process.execPath, ['--test', testFile], { cwd: root, env, encoding: 'utf8', timeout: 20000 });
|
|
465
|
+
report.task = { name: 'bounded invoice implementation with independent contract tests and cross-provider review', firstTestExitCode: test.status, testLog: redactSecrets(test.stdout + test.stderr), changedFile: 'invoice.mjs', moduleSha256: digest(moduleFile) };
|
|
466
|
+
assert.equal(test.status, 0, report.task.testLog);
|
|
467
|
+
const reviewFile = path.join(root, 'review.json');
|
|
468
|
+
const reviewBody = `Review invoice.mjs against invoice.test.mjs. Read only these project files. Use Write to create review.json with JSON {"passed":true,"reviewer":"claude","issues":[]} only if the arithmetic, validation, overflow and tests satisfy the contract; otherwise record passed:false and concrete issues. Do not modify implementation/tests or run commands. Return REVIEWED.`;
|
|
469
|
+
const delegation = message('install-claude', `I authorize a bounded review task in this project. When install-codex asks you to review invoice.mjs against invoice.test.mjs, read those two files and write review.json only, waiting for explicit Write approval. Do not modify implementation/tests or run commands. Until the handoff arrives, do not use tools; reply only REVIEW_READY.`);
|
|
470
|
+
assert.equal((await finish('install-claude', delegation)).reply.body.trim(), 'REVIEW_READY');
|
|
471
|
+
const input = { to: 'install-claude', kind: 'ask', body: reviewBody };
|
|
472
|
+
const handoff = message('install-codex', `Implementation is complete. Send it for independent review using only hello_cc_scoped hcc_message_send exactly once with ${JSON.stringify(input)}. Return REVIEW_SENT. No other tools.`);
|
|
473
|
+
const allowedReview = request => {
|
|
474
|
+
const target = typeof request.params?.input?.file_path === 'string' ? path.resolve(root, request.params.input.file_path) : null;
|
|
475
|
+
const allowed = request.method === 'claude/canUseTool' && ((request.params.tool === 'Write' && target === reviewFile) || (request.params.tool === 'Read' && [moduleFile, testFile].includes(target)));
|
|
476
|
+
respond('install-claude', request, allowed ? 'accept' : 'decline'); assert.ok(allowed, 'Only the two authorized Read paths and review.json Write may be accepted');
|
|
477
|
+
};
|
|
478
|
+
const handed = await until(async () => {
|
|
479
|
+
for (const request of (await state('install-codex')).snapshot.pendingApprovals || []) {
|
|
480
|
+
const values = request.params?.input || request.params?._meta?.tool_params || request.params?.toolCall?.rawInput;
|
|
481
|
+
assert.deepEqual(values, input); respond('install-codex', request, 'accept');
|
|
482
|
+
}
|
|
483
|
+
for (const request of (await state('install-claude')).snapshot.pendingApprovals || []) allowedReview(request);
|
|
484
|
+
return db.prepare('SELECT * FROM messages WHERE sender=? AND recipient=? AND body=?').get('install-codex', 'install-claude', reviewBody);
|
|
485
|
+
}, 'real implementation-review handoff');
|
|
486
|
+
await finish('install-codex', handoff);
|
|
487
|
+
const { reply: reviewReply } = await finish('install-claude', { message_id: handed.id }, allowedReview);
|
|
488
|
+
// Finish the handoff in both directions before cleanup. A completed review
|
|
489
|
+
// file alone does not prove the originating worker consumed its reply.
|
|
490
|
+
const consumed = await until(async () => {
|
|
491
|
+
const row = (await state('install-codex')).deliveries.find(value => value.message_id === reviewReply.id);
|
|
492
|
+
return terminal.has(row?.state) ? row : null;
|
|
493
|
+
}, 'implementation worker consumes the final review reply');
|
|
494
|
+
assert.equal(consumed.state, 'completed');
|
|
495
|
+
assert.ok(db.prepare('SELECT read_at FROM message_reads WHERE peer=? AND message_id=?').get('install-codex', reviewReply.id));
|
|
496
|
+
await delay(300);
|
|
497
|
+
assert.equal(db.prepare('SELECT COUNT(*) AS n FROM messages WHERE reply_to=?').get(reviewReply.id).n, 0);
|
|
498
|
+
report.task.reviewReplyConsumed = true; report.task.reviewReplyAcked = true; report.task.noReplyLoop = true;
|
|
499
|
+
const review = JSON.parse(fs.readFileSync(reviewFile, 'utf8')); assert.deepEqual(review, { passed: true, reviewer: 'claude', issues: [] });
|
|
500
|
+
assert.equal(digest(testFile), contractSha256, 'Review must preserve the independent contract tests');
|
|
501
|
+
assert.equal(digest(moduleFile), report.task.moduleSha256); assert.equal(spawnSync(process.execPath, ['--test', testFile], { env, cwd: root }).status, 0);
|
|
502
|
+
report.task.review = review; report.task.crossProviderHandoff = true; report.task.finalTestsPassed = 5; report.task.contractTestsUnchanged = true; report.task.contractSha256 = contractSha256;
|
|
503
|
+
const artifact = path.join(sandbox, 'task-artifacts'); fs.mkdirSync(artifact);
|
|
504
|
+
for (const file of [moduleFile, testFile, reviewFile]) fs.copyFileSync(file, path.join(artifact, path.basename(file)));
|
|
505
|
+
report.task.artifactDirectory = artifact;
|
|
506
|
+
check('installed coding task: model implementation passes five independent tests and receives model-authored cross-provider review');
|
|
507
|
+
}
|
|
508
|
+
try {
|
|
509
|
+
fs.mkdirSync(root, { mode: 0o700 });
|
|
510
|
+
for (const provider of ['codex', 'claude', 'dsh']) homes[provider] = privateDir(`${provider}-home`);
|
|
511
|
+
const configFile = path.join(originalCodexHome, 'config.toml');
|
|
512
|
+
if (fs.existsSync(configFile)) {
|
|
513
|
+
let section = '';
|
|
514
|
+
const config = fs.readFileSync(configFile, 'utf8').split('\n').filter(line => {
|
|
515
|
+
const heading = line.match(/^\s*\[\[?([^\]]+)\]\]?\s*(?:#.*)?$/);
|
|
516
|
+
if (heading) section = heading[1].split('.')[0].replaceAll('"', '').replaceAll("'", '');
|
|
517
|
+
return section ? section === 'model_providers' : /^\s*(?:model(?:_provider|_reasoning_effort|_reasoning_summary|_verbosity|_catalog_json)?|cli_auth_credentials_store)\s*=/.test(line);
|
|
518
|
+
}).join('\n');
|
|
519
|
+
// Keep HCC's global registry isolated while allowing the caller's existing
|
|
520
|
+
// credential commands to resolve their HOME-bound account state. Only the
|
|
521
|
+
// private copy is rewritten; the model process retains the isolated HOME.
|
|
522
|
+
const quoteAuth = value => "'" + value.replaceAll("'", "'\\''") + "'";
|
|
523
|
+
const authCommands = privateDir('credential-commands');
|
|
524
|
+
let authSection = '', wrappedCommands = 0;
|
|
525
|
+
const isolatedConfig = config.split('\n').map(line => {
|
|
526
|
+
const heading = line.match(/^\s*\[\[?([^\]]+)\]\]?\s*(?:#.*)?$/);
|
|
527
|
+
if (heading) authSection = heading[1];
|
|
528
|
+
const command = authSection.endsWith('.auth') && line.match(/^\s*command\s*=\s*((?:"(?:[^"\\]|\\.)*"|'[^']*'))\s*(?:#.*)?$/);
|
|
529
|
+
if (!command) return line;
|
|
530
|
+
const original = command[1].startsWith("'") ? command[1].slice(1, -1) : JSON.parse(command[1]);
|
|
531
|
+
const wrapper = path.join(authCommands, `command-${++wrappedCommands}`);
|
|
532
|
+
fs.writeFileSync(wrapper, `#!/bin/sh\nexport HOME=${quoteAuth(os.homedir())}\nexec ${quoteAuth(original)} "$@"\n`, { mode: 0o700 });
|
|
533
|
+
return `command = ${JSON.stringify(wrapper)}`;
|
|
534
|
+
}).join('\n');
|
|
535
|
+
report.authenticationHelperHomePreserved = wrappedCommands > 0;
|
|
536
|
+
fs.writeFileSync(path.join(homes.codex, 'config.toml'), isolatedConfig + '\n', { mode: 0o600 });
|
|
537
|
+
}
|
|
538
|
+
const auth = path.join(originalCodexHome, 'auth.json');
|
|
539
|
+
if (fs.existsSync(auth)) { fs.copyFileSync(auth, path.join(homes.codex, 'auth.json')); fs.chmodSync(path.join(homes.codex, 'auth.json'), 0o600); }
|
|
540
|
+
const installedRequire = createRequire(path.join(repo, 'package.json'));
|
|
541
|
+
const sdkEntry = installedRequire.resolve('@anthropic-ai/claude-agent-sdk');
|
|
542
|
+
const sdkPackage = JSON.parse(fs.readFileSync(path.join(path.dirname(sdkEntry), 'package.json')));
|
|
543
|
+
report.claudeSdk = { version: sdkPackage.version, defaultResolution: sdkEntry, entrySha256: digest(sdkEntry), queryInjected: false };
|
|
544
|
+
const userHome = privateDir('user-home');
|
|
545
|
+
const bin = privateDir('bin');
|
|
546
|
+
const quote = value => "'" + value.replaceAll("'", "'\\''") + "'";
|
|
547
|
+
fs.writeFileSync(path.join(bin, 'tmux'), `#!/bin/sh\nexec ${quote(tmux)} -L ${quote(socket)} "$@"\n`, { mode: 0o700 });
|
|
548
|
+
env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith('HCC_')));
|
|
549
|
+
Object.assign(env, { PATH: `${bin}:${path.dirname(process.execPath)}:${process.env.PATH}`, CODEX_HOME: homes.codex, CLAUDE_CONFIG_DIR: homes.claude,
|
|
550
|
+
HOME: userHome, DSH_HOME: homes.dsh, DSH_TELEMETRY_DISABLED: '1', HCC_SHIM_ENSURED: '1', HCC_SHIM_NO_ATTACH: '1', HCC_NO_AUTO_INSTALL_TMUX: '1', HCC_WEB_TOKEN: randomUUID() });
|
|
551
|
+
report.hccGlobalHomeIsolated = true;
|
|
552
|
+
hcc('native', 'up'); runtimeStarted = true;
|
|
553
|
+
db = new DatabaseSync(ctx.dbPath, { readOnly: true });
|
|
554
|
+
check('locally installed package starts its own native daemon through the public CLI', { runtimePid: readNativePointer(ctx)?.pid });
|
|
555
|
+
if (stabilityOnly) {
|
|
556
|
+
for (const provider of ['codex', 'claude', 'dsh']) {
|
|
557
|
+
const peer = `install-${provider}`;
|
|
558
|
+
report.providers[provider] = { status: 'running', userSubmissions: 0, launcher: 'installed public hcc native start CLI' };
|
|
559
|
+
await start(provider);
|
|
560
|
+
report.providers[provider].version = provider === 'claude' ? report.claudeSdk.version : execFileSync(binaries[provider], ['--version'], { env, encoding: 'utf8', timeout: 10000 }).trim();
|
|
561
|
+
await finish(peer, message(peer, 'Without tools, reply only STABILITY_READY. This is a bounded public stability test.'));
|
|
562
|
+
report.providers[provider].sessionId = (await state(peer)).snapshot.sessionId;
|
|
563
|
+
assert.ok(report.providers[provider].sessionId);
|
|
564
|
+
report.providers[provider].status = 'passed';
|
|
565
|
+
check(`${provider}: stability-only real session bootstrap`);
|
|
566
|
+
}
|
|
567
|
+
report.limitations.push('Stability-only skips the baseline interruption, command approval and model MCP communication scenarios; their evidence must be reviewed separately.');
|
|
568
|
+
} else {
|
|
569
|
+
for (const provider of ['codex', 'claude', 'dsh']) await lifecycle(provider);
|
|
570
|
+
await communication();
|
|
571
|
+
}
|
|
572
|
+
if (stabilityConfig) await stability();
|
|
573
|
+
if (args.includes('--browser')) await browserFlow();
|
|
574
|
+
if (args.includes('--task')) await codingTask();
|
|
575
|
+
assert.deepEqual(sourceHashes(), initialSources); report.sourceUnchangedDuringRun = true;
|
|
576
|
+
report.completed = true;
|
|
577
|
+
} catch (error) {
|
|
578
|
+
report.completed = false; report.error = { code: error.code || 'ACCEPTANCE_FAILED', message: error.message }; process.exitCode = 1;
|
|
579
|
+
console.error(JSON.stringify(redactSecrets({ acceptanceFailure: report.error })));
|
|
580
|
+
report.failureState = [];
|
|
581
|
+
for (const provider of Object.keys(report.providers)) {
|
|
582
|
+
try {
|
|
583
|
+
const current = await state(`install-${provider}`);
|
|
584
|
+
report.failureState.push({ provider, snapshot: current.snapshot, activeDelivery: current.active_delivery,
|
|
585
|
+
recentDeliveries: current.deliveries.slice(0, 8), recentEvents: current.events.slice(-20) });
|
|
586
|
+
} catch (failure) { report.failureState.push({ provider, error: failure.message }); }
|
|
587
|
+
}
|
|
588
|
+
if (browser) for (const context of browser.contexts()) for (const page of context.pages()) {
|
|
589
|
+
try { const file = path.join(sandbox, 'browser-failure.png'); await page.screenshot({ path: file }); report.screenshots.push(file); } catch {}
|
|
590
|
+
}
|
|
591
|
+
} finally {
|
|
592
|
+
await browser?.close().catch(() => {});
|
|
593
|
+
if (webStarted) { try { hcc('down'); } catch {} }
|
|
594
|
+
if (runtimeStarted) {
|
|
595
|
+
try { hcc('native', 'down'); await until(() => !readNativePointer(ctx), 'native daemon shutdown', 30000); report.cleanup.runtimeStopped = true; }
|
|
596
|
+
catch (error) { report.cleanup.runtimeStopped = false; report.cleanup.error = error.message; process.exitCode = 1; report.completed = false; }
|
|
597
|
+
} else report.cleanup.runtimeStopped = !readNativePointer(ctx);
|
|
598
|
+
db?.close();
|
|
599
|
+
spawnSync(tmux, ['-L', socket, 'kill-server'], { stdio: 'ignore' });
|
|
600
|
+
report.cleanup.protectedConfigUnchanged = protectedHashes.every(([file, hash]) => fs.existsSync(file) && digest(file) === hash);
|
|
601
|
+
for (const directory of credentials) fs.rmSync(directory, { recursive: true, force: true });
|
|
602
|
+
report.cleanup.temporaryCredentialsRemoved = credentials.every(directory => !fs.existsSync(directory));
|
|
603
|
+
const finalSources = sourceHashes();
|
|
604
|
+
report.sourceChangesDuringRun = Object.keys({ ...initialSources, ...finalSources }).filter(file => initialSources[file] !== finalSources[file]);
|
|
605
|
+
report.sourceUnchangedDuringRun = report.sourceChangesDuringRun.length === 0;
|
|
606
|
+
if (!Object.values(report.cleanup).every(value => value === true) || !report.sourceUnchangedDuringRun) { process.exitCode = 1; report.completed = false; }
|
|
607
|
+
report.limitations.push('Local package installation was exercised; no npm publication or employee-device rollout was performed.');
|
|
608
|
+
report.limitations.push('The coding task uses an independent test project and contract tests; it is not stakeholder acceptance of a production business workflow.');
|
|
609
|
+
report.completedAt = new Date().toISOString(); flush();
|
|
610
|
+
}
|
|
611
|
+
console.log(JSON.stringify({ receipt: output, completed: report.completed, cleanup: report.cleanup }));
|