@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,200 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import http from 'node:http';
|
|
6
|
+
import { randomUUID, createHash } from 'node:crypto';
|
|
7
|
+
import { createRequire } from 'node:module';
|
|
8
|
+
import { spawnSync } from 'node:child_process';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
11
|
+
import { JsonRpcProcess } from '../lib/integrations/native/jsonrpc.mjs';
|
|
12
|
+
import { createDshEnvironment, ensureDshIntegration } from '../lib/integrations/dsh.mjs';
|
|
13
|
+
import { checkDshRuntime } from '../lib/integrations/dsh-cordis.mjs';
|
|
14
|
+
import { redactSecrets } from '../lib/shared/redact.mjs';
|
|
15
|
+
|
|
16
|
+
// Runs the published Harness, AgentLoop, tool policies and persistence. Default:
|
|
17
|
+
// a deterministic local Messages model. --run-live uses the original DeepSeek
|
|
18
|
+
// route from environment, without writing/copying credentials or user config.
|
|
19
|
+
const install = process.argv[2];
|
|
20
|
+
if (!install) throw new Error('Usage: node scripts/dsh-cordis-acceptance.mjs /isolated/dsh/install [--run-live] [--bundle]');
|
|
21
|
+
const live = process.argv.includes('--run-live');
|
|
22
|
+
const bundle = process.argv.includes('--bundle');
|
|
23
|
+
if (live && !process.env.DEEPSEEK_API_KEY) throw new Error('Live acceptance requires an existing DEEPSEEK_API_KEY');
|
|
24
|
+
const requireOfficial = createRequire(path.join(path.resolve(install), 'package.json'));
|
|
25
|
+
const binary = requireOfficial.resolve('@deepseek-ai/dsh/lib/bin.js');
|
|
26
|
+
checkDshRuntime(binary);
|
|
27
|
+
const repo = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
28
|
+
const sandbox = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'hcc-dsh-cordis-acceptance-')));
|
|
29
|
+
const rootA = path.join(sandbox, 'project A'), rootB = path.join(sandbox, 'project B');
|
|
30
|
+
const home = path.join(sandbox, 'home');
|
|
31
|
+
for (const dir of [rootA, rootB, home]) fs.mkdirSync(dir, { mode: 0o700 });
|
|
32
|
+
const env = createDshEnvironment(process.env);
|
|
33
|
+
Object.assign(env, { HOME: home, DSH_HOME: path.join(home, 'dsh'), DSH_TELEMETRY_DISABLED: '1',
|
|
34
|
+
PATH: path.dirname(process.execPath) + path.delimiter + (env.PATH || '') });
|
|
35
|
+
const marker = 'CORDIS_' + randomUUID().replaceAll('-', '').slice(0, 16);
|
|
36
|
+
const receipt = { startedAt: new Date().toISOString(), baseline: '0.2.0-rc.2', node: process.version,
|
|
37
|
+
loading: bundle ? 'npm tarball installed into isolated ACP profile' : 'project overlay',
|
|
38
|
+
validation: live ? 'original authenticated DeepSeek model' : 'deterministic local Messages model; real official Harness',
|
|
39
|
+
sandbox, checks: [], cleanup: {}, sourceHashes: {} };
|
|
40
|
+
for (const name of ['lib/integrations/dsh-cordis.mjs', 'lib/integrations/dsh-collaboration.mjs',
|
|
41
|
+
'lib/mcp/tools.mjs', 'lib/integrations/native/jsonrpc.mjs', 'scripts/dsh-cordis-acceptance.mjs']) {
|
|
42
|
+
receipt.sourceHashes[name] = createHash('sha256').update(fs.readFileSync(path.join(repo, name))).digest('hex');
|
|
43
|
+
}
|
|
44
|
+
const output = path.join(sandbox, 'receipt.json');
|
|
45
|
+
function check(name, details = {}) { receipt.checks.push({ name, passed: true, ...details }); }
|
|
46
|
+
function read(root, sql, ...params) {
|
|
47
|
+
const db = new DatabaseSync(path.join(root, '.hello-cc', 'mesh.db'));
|
|
48
|
+
try { return db.prepare(sql).all(...params).map(row => ({ ...row })); } finally { db.close(); }
|
|
49
|
+
}
|
|
50
|
+
function hcc(root, args) {
|
|
51
|
+
const result = spawnSync(process.execPath, [path.join(repo, 'bin/hcc.mjs'), '--root', root, '--json', ...args],
|
|
52
|
+
{ cwd: root, env, encoding: 'utf8', timeout: 15000 });
|
|
53
|
+
assert.equal(result.status, 0, redactSecrets(result.stderr + result.stdout));
|
|
54
|
+
const value = JSON.parse(result.stdout); return value.data ?? value;
|
|
55
|
+
}
|
|
56
|
+
let rpc, server;
|
|
57
|
+
const modelRequests = [], notices = [], permissions = [];
|
|
58
|
+
let taskId, peerA;
|
|
59
|
+
let stage = 0;
|
|
60
|
+
function stream(res, model, block) {
|
|
61
|
+
res.writeHead(200, { 'content-type': 'text/event-stream' });
|
|
62
|
+
const event = (type, value) => res.write(`event: ${type}\ndata: ${JSON.stringify({ type, ...value })}\n\n`);
|
|
63
|
+
event('message_start', { message: { id: 'msg-' + randomUUID(), type: 'message', role: 'assistant', model,
|
|
64
|
+
content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: 1, output_tokens: 0 } } });
|
|
65
|
+
event('content_block_start', { index: 0, content_block: block.type === 'tool_use' ? { ...block, input: {} } : block });
|
|
66
|
+
if (block.type === 'tool_use') event('content_block_delta', { index: 0, delta: { type: 'input_json_delta', partial_json: JSON.stringify(block.input) } });
|
|
67
|
+
event('content_block_stop', { index: 0 });
|
|
68
|
+
event('message_delta', { delta: { stop_reason: block.type === 'tool_use' ? 'tool_use' : 'end_turn', stop_sequence: null }, usage: { output_tokens: 1 } });
|
|
69
|
+
event('message_stop', {}); res.end();
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const setup = ensureDshIntegration({ root: rootA, cwd: rootA }, { mode: 'cordis' });
|
|
73
|
+
const policy = path.join(sandbox, 'policy.mjs');
|
|
74
|
+
fs.writeFileSync(policy, `export const inject = ['tools'];\nexport function apply(ctx) {\n ctx.on('tools/pre-execute', async (exec, next) => {\n if (exec.name === 'hcc_message_send' && exec.arguments.body === 'DENY_WRITE') return {kind:'deny',reason:'acceptance denies this write'};\n if (exec.name === 'hcc_message_send' && exec.arguments.body === 'ASK_WRITE') return {kind:'ask',reason:'acceptance requires approval'};\n return next();\n });\n}\n`);
|
|
75
|
+
const policyPatch = path.join(sandbox, 'policy.yml');
|
|
76
|
+
fs.writeFileSync(policyPatch, `- insert:\n - id: acceptance-policy\n name: ${JSON.stringify(policy)}\n`);
|
|
77
|
+
const argv = ['--profile', 'acp', ...(bundle ? [] : ['--patch', setup.patchPath]), '--patch', policyPatch];
|
|
78
|
+
if (bundle) {
|
|
79
|
+
const packed = spawnSync('npm', ['pack', '--ignore-scripts', '--json', '--pack-destination', sandbox],
|
|
80
|
+
{ cwd: repo, env, encoding: 'utf8', timeout: 30000 });
|
|
81
|
+
assert.equal(packed.status, 0, packed.stderr);
|
|
82
|
+
const metadata = JSON.parse(packed.stdout)[0];
|
|
83
|
+
assert.ok(metadata.files.some(file => file.path === 'lib/integrations/dsh.bundle.yml'));
|
|
84
|
+
assert.ok(metadata.files.some(file => file.path === 'lib/integrations/dsh-cordis.d.ts'));
|
|
85
|
+
const unpack = path.join(sandbox, 'installed'); fs.mkdirSync(unpack);
|
|
86
|
+
const extracted = spawnSync('tar', ['-xzf', path.join(sandbox, metadata.filename), '-C', unpack],
|
|
87
|
+
{ env, encoding: 'utf8', timeout: 10000 });
|
|
88
|
+
assert.equal(extracted.status, 0, extracted.stderr);
|
|
89
|
+
const profile = path.join(env.DSH_HOME, 'profiles', 'acp');
|
|
90
|
+
const packageDirectory = path.join(profile, 'node_modules', '@logicseek');
|
|
91
|
+
fs.mkdirSync(packageDirectory, { recursive: true, mode: 0o700 });
|
|
92
|
+
fs.symlinkSync(path.join(unpack, 'package'), path.join(packageDirectory, 'hello-cc'));
|
|
93
|
+
fs.writeFileSync(path.join(profile, 'package.json'), JSON.stringify({ name: 'isolated-cordis-profile', private: true,
|
|
94
|
+
dependencies: { '@logicseek/hello-cc': 'file:' + path.join(unpack, 'package') },
|
|
95
|
+
dsh: { profile: { bundles: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-acp-app', '@logicseek/hello-cc'] } } }));
|
|
96
|
+
fs.writeFileSync(path.join(profile, 'cordis.yml'), '[]\n');
|
|
97
|
+
receipt.package = { filename: metadata.filename, shasum: metadata.shasum, fileCount: metadata.files.length };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!live) {
|
|
101
|
+
server = http.createServer(async (req, res) => {
|
|
102
|
+
try {
|
|
103
|
+
const chunks = []; for await (const chunk of req) chunks.push(chunk);
|
|
104
|
+
const body = JSON.parse(Buffer.concat(chunks).toString());
|
|
105
|
+
modelRequests.push(body);
|
|
106
|
+
const serialized = JSON.stringify(body.messages);
|
|
107
|
+
const primary = serialized.includes('RUN_COLLABORATION_A');
|
|
108
|
+
const actions = [
|
|
109
|
+
['hcc_state', {}], ['hcc_task_next', {}], ['hcc_lock_acquire', { resource: 'cordis-proof.txt', task_id: taskId }],
|
|
110
|
+
['hcc_message_send', { to: 'acceptance-coordinator', body: marker, task_id: taskId }],
|
|
111
|
+
['hcc_handoff', { task_id: taskId, summary: marker, to: 'acceptance-coordinator', tests: ['official Harness tool round trip'] }],
|
|
112
|
+
['hcc_message_send', { to: 'acceptance-coordinator', body: 'DENY_WRITE' }],
|
|
113
|
+
['hcc_message_send', { to: 'acceptance-coordinator', body: 'ASK_WRITE' }]
|
|
114
|
+
];
|
|
115
|
+
const action = primary ? actions[stage++] : null;
|
|
116
|
+
if (action) {
|
|
117
|
+
assert.ok(body.tools.some(tool => tool.name === action[0]), 'model must receive the registered collaboration tool');
|
|
118
|
+
stream(res, body.model, { type: 'tool_use', id: 'tool-' + randomUUID(), name: action[0], input: action[1] });
|
|
119
|
+
} else stream(res, body.model, { type: 'text', text: primary ? marker : 'B_SESSION_OK' });
|
|
120
|
+
} catch (error) { res.writeHead(500); res.end(JSON.stringify({ error: { type: 'api_error', message: error.message } })); }
|
|
121
|
+
});
|
|
122
|
+
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
123
|
+
env.DEEPSEEK_API_KEY = 'isolated-local-model';
|
|
124
|
+
env.DEEPSEEK_BASE_URL = `http://127.0.0.1:${server.address().port}`;
|
|
125
|
+
}
|
|
126
|
+
rpc = new JsonRpcProcess({ binary, args: argv, cwd: rootA, env, timeoutMs: 180000,
|
|
127
|
+
onNotification: (method, params) => { if (method === 'session/update') notices.push(params); },
|
|
128
|
+
onRequest: (method, params) => {
|
|
129
|
+
if (method !== 'session/request_permission') throw new Error(`Unexpected server request ${method}`);
|
|
130
|
+
permissions.push({ method, requested: true });
|
|
131
|
+
const rejected = params.options.find(option => option.kind === 'reject_once');
|
|
132
|
+
return rejected ? { outcome: { outcome: 'selected', optionId: rejected.optionId } } : { outcome: { outcome: 'cancelled' } };
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
await rpc.start();
|
|
136
|
+
const initialized = await rpc.request('initialize', { protocolVersion: 1, clientCapabilities: {}, clientInfo: { name: 'hello-cc-cordis-acceptance', version: '1.0.1' } });
|
|
137
|
+
assert.equal(initialized.protocolVersion, 1);
|
|
138
|
+
const a = await rpc.request('session/new', { cwd: rootA, mcpServers: [] });
|
|
139
|
+
const b = await rpc.request('session/new', { cwd: rootB, mcpServers: [] });
|
|
140
|
+
assert.notEqual(a.sessionId, b.sessionId);
|
|
141
|
+
peerA = read(rootA, 'SELECT peer FROM peer_bindings WHERE provider_session_id=?', a.sessionId)[0].peer;
|
|
142
|
+
const peerB = read(rootB, 'SELECT peer FROM peer_bindings WHERE provider_session_id=?', b.sessionId)[0].peer;
|
|
143
|
+
check('two Agents in one official runtime have independent project, peer and raw session bindings', { peerA, peerB });
|
|
144
|
+
hcc(rootA, ['register', '--peer', 'acceptance-coordinator', '--kind', 'shell']);
|
|
145
|
+
hcc(rootA, ['task', 'create', '--from', 'acceptance-coordinator', '--title', marker]);
|
|
146
|
+
taskId = read(rootA, 'SELECT id FROM tasks WHERE title=?', marker)[0].id;
|
|
147
|
+
hcc(rootA, ['msg', 'send', '--from', 'acceptance-coordinator', '--to', peerA, '--body', 'INBOX_ONLY_FOR_A_' + marker]);
|
|
148
|
+
const promptA = `RUN_COLLABORATION_A. This is an isolated acceptance task. Use hcc_state, then hcc_task_next to claim task #${taskId}. Acquire the lock cordis-proof.txt for that task using hcc_lock_acquire. Send the exact body ${marker} to acceptance-coordinator using hcc_message_send, with task_id ${taskId}. Record a hcc_handoff for that task to acceptance-coordinator, with summary ${marker}. Then call hcc_message_send once with body DENY_WRITE and once with body ASK_WRITE to acceptance-coordinator to test refusal handling. Respect both refusals; do not retry or bypass them. Finally reply only ${marker}. Do not use Bash, files, other tools or delegate.`;
|
|
149
|
+
const completed = await rpc.request('session/prompt', { sessionId: a.sessionId, prompt: [{ type: 'text', text: promptA }] }, { timeoutMs: 180000 });
|
|
150
|
+
assert.equal(completed.stopReason, 'end_turn');
|
|
151
|
+
const task = read(rootA, 'SELECT * FROM tasks WHERE id=?', taskId)[0];
|
|
152
|
+
assert.equal(task.owner, peerA);
|
|
153
|
+
const locks = read(rootA, 'SELECT owner, task_id FROM locks WHERE resource=? OR base_resource=?', 'cordis-proof.txt', 'cordis-proof.txt');
|
|
154
|
+
assert.ok(locks.length, 'The model must acquire the requested resource, including a scoped lock key');
|
|
155
|
+
assert.equal(locks[0].owner, peerA);
|
|
156
|
+
assert.equal(locks[0].task_id, taskId);
|
|
157
|
+
assert.equal(read(rootA, 'SELECT sender FROM messages WHERE body=?', marker)[0].sender, peerA);
|
|
158
|
+
assert.ok(read(rootA, 'SELECT * FROM handoffs WHERE task_id=?', taskId).length);
|
|
159
|
+
check('model commits task claim, scoped lock, peer message and handoff through shared collaboration services', { taskId });
|
|
160
|
+
assert.equal(read(rootA, "SELECT * FROM messages WHERE body IN ('DENY_WRITE', 'ASK_WRITE')").length, 0);
|
|
161
|
+
assert.ok(permissions.length >= 1, 'ask must reach the ACP permission handler');
|
|
162
|
+
check('deny and ask policy remain effective, rejected writes do not execute', { permissionRequests: permissions.length });
|
|
163
|
+
assert.ok(read(rootA, 'SELECT * FROM message_reads WHERE peer=?', peerA).length);
|
|
164
|
+
assert.equal(read(rootB, 'SELECT * FROM messages').length, 0);
|
|
165
|
+
const promptB = 'RUN_SESSION_B. Use hcc_state to read your own project. Do not use any other tools. Reply only B_SESSION_OK.';
|
|
166
|
+
await rpc.request('session/prompt', { sessionId: b.sessionId, prompt: [{ type: 'text', text: promptB }] }, { timeoutMs: 180000 });
|
|
167
|
+
check('committed context ACKs only its Agent inbox; other project remains isolated');
|
|
168
|
+
if (!live) {
|
|
169
|
+
assert.match(JSON.stringify(modelRequests[0].messages), new RegExp('INBOX_ONLY_FOR_A_' + marker));
|
|
170
|
+
const requestsB = modelRequests.filter(body => JSON.stringify(body.messages).includes('RUN_SESSION_B'));
|
|
171
|
+
assert.ok(requestsB.length);
|
|
172
|
+
for (const body of requestsB) assert.doesNotMatch(JSON.stringify(body.messages), new RegExp(marker));
|
|
173
|
+
check('actual first model request contains awaited collaboration context, foreign Agent context never appears');
|
|
174
|
+
}
|
|
175
|
+
await rpc.request('session/close', { sessionId: a.sessionId });
|
|
176
|
+
assert.equal(read(rootA, 'SELECT status FROM peers WHERE id=?', peerA)[0].status, 'exited');
|
|
177
|
+
assert.equal(read(rootB, 'SELECT status FROM peers WHERE id=?', peerB)[0].status, 'idle');
|
|
178
|
+
await rpc.request('session/prompt', { sessionId: b.sessionId, prompt: [{ type: 'text', text: promptB }] }, { timeoutMs: 180000 });
|
|
179
|
+
check('closing one Agent preserves the live sibling and marks only its own peer exited');
|
|
180
|
+
await rpc.request('session/resume', { sessionId: a.sessionId, cwd: rootA, mcpServers: [] });
|
|
181
|
+
assert.equal(read(rootA, 'SELECT provider_session_id FROM peer_bindings WHERE peer=?', peerA)[0].provider_session_id, a.sessionId);
|
|
182
|
+
check('resume re-establishes the original raw session and peer without duplicate live ownership');
|
|
183
|
+
await rpc.request('session/close', { sessionId: a.sessionId });
|
|
184
|
+
await rpc.request('session/close', { sessionId: b.sessionId });
|
|
185
|
+
receipt.passed = true;
|
|
186
|
+
} catch (error) {
|
|
187
|
+
receipt.passed = false;
|
|
188
|
+
receipt.error = { name: error.name, message: redactSecrets(error.message) };
|
|
189
|
+
process.exitCode = 1;
|
|
190
|
+
} finally {
|
|
191
|
+
try { await rpc?.close(); receipt.cleanup.ownedRuntimeStopped = true; } catch { receipt.cleanup.ownedRuntimeStopped = false; process.exitCode = 1; }
|
|
192
|
+
if (server) { server.closeAllConnections(); await new Promise(resolve => server.close(resolve)); }
|
|
193
|
+
receipt.cleanup.localModelStopped = !server?.listening;
|
|
194
|
+
receipt.modelRequests = live ? null : modelRequests.length;
|
|
195
|
+
receipt.permissionRequests = permissions.length;
|
|
196
|
+
receipt.updateKinds = [...new Set(notices.map(value => value.update?.sessionUpdate).filter(Boolean))];
|
|
197
|
+
receipt.completedAt = new Date().toISOString();
|
|
198
|
+
fs.writeFileSync(output, JSON.stringify(redactSecrets(receipt), null, 2) + '\n', { mode: 0o600 });
|
|
199
|
+
console.log(JSON.stringify({ receipt: output, ...redactSecrets(receipt) }, null, 2));
|
|
200
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
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 { createHash, randomUUID } from 'node:crypto';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
9
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
10
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
11
|
+
import { JsonRpcProcess } from '../lib/integrations/native/jsonrpc.mjs';
|
|
12
|
+
import { createDshEnvironment } from '../lib/integrations/dsh.mjs';
|
|
13
|
+
import { checkDshRuntime } from '../lib/integrations/dsh-cordis.mjs';
|
|
14
|
+
import { redactSecrets } from '../lib/shared/redact.mjs';
|
|
15
|
+
|
|
16
|
+
// Uses actual npm/pnpm installations and the installed public CLI. Every
|
|
17
|
+
// profile, project and process belongs to this run. Real inference is opt-in.
|
|
18
|
+
const args = process.argv.slice(2);
|
|
19
|
+
const install = args[0];
|
|
20
|
+
const opt = (name, fallback) => { const i = args.indexOf(name); return i < 0 ? fallback : args[i + 1]; };
|
|
21
|
+
if (!install || install === '--help') {
|
|
22
|
+
console.log('Usage: node scripts/dsh-installed-acceptance.mjs /isolated/dsh/install [--run-live] [--tarball PATH] [--output-dir DIR] [--npm-cache DIR] [--pnpm-store DIR]');
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
const live = args.includes('--run-live');
|
|
26
|
+
if (live && !process.env.DEEPSEEK_API_KEY) throw new Error('Live acceptance requires the existing DEEPSEEK_API_KEY');
|
|
27
|
+
const repo = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
28
|
+
const resolveOfficial = createRequire(path.join(path.resolve(install), 'package.json'));
|
|
29
|
+
const binary = resolveOfficial.resolve('@deepseek-ai/dsh/lib/bin.js');
|
|
30
|
+
checkDshRuntime(binary);
|
|
31
|
+
const directory = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'hcc-dsh-installed-')));
|
|
32
|
+
fs.chmodSync(directory, 0o700);
|
|
33
|
+
const home = path.join(directory, 'home'), root = path.join(directory, 'project'), cordisRoot = path.join(directory, 'cordis-project');
|
|
34
|
+
for (const dir of [home, root, cordisRoot]) fs.mkdirSync(dir, { mode: 0o700 });
|
|
35
|
+
const prefix = path.join(directory, 'npm-install');
|
|
36
|
+
const env = createDshEnvironment(process.env);
|
|
37
|
+
Object.assign(env, { HOME: home, DSH_HOME: path.join(home, 'dsh'), DSH_TELEMETRY_DISABLED: '1',
|
|
38
|
+
PATH: [path.dirname(process.execPath), path.join(path.resolve(install), 'node_modules/.bin'), path.join(prefix, 'node_modules/.bin'), env.PATH || ''].join(path.delimiter) });
|
|
39
|
+
const noModelEnv = { ...env, DEEPSEEK_API_KEY: 'isolated-no-model-placeholder' };
|
|
40
|
+
const output = path.resolve(opt('--output-dir', directory));
|
|
41
|
+
fs.mkdirSync(output, { recursive: true });
|
|
42
|
+
const receiptPath = path.join(output, 'dsh-installed-receipt.json');
|
|
43
|
+
const receipt = { startedAt: new Date().toISOString(), node: process.version, baseline: '0.2.0-rc.2', directory,
|
|
44
|
+
validation: live ? 'installed public CLI, original authenticated DeepSeek model' : 'installed public CLI and official profile lifecycle; no model call',
|
|
45
|
+
checks: [], artifacts: {}, cleanup: {}, limitations: ['No npm publication, global installation, employee device or genuine business task acceptance.'] };
|
|
46
|
+
function check(name, data = {}) { receipt.checks.push({ name, passed: true, ...data }); flush(); console.log(JSON.stringify({ check: name, ...data })); }
|
|
47
|
+
function flush() { fs.writeFileSync(receiptPath, JSON.stringify(redactSecrets(receipt), null, 2) + '\n', { mode: 0o600 }); }
|
|
48
|
+
async function run(label, executable, argv, { cwd = directory, childEnv = env, timeout = 120000, omitOutput = false, allowFailure = false } = {}) {
|
|
49
|
+
const ownsProcessGroup = process.platform !== 'win32';
|
|
50
|
+
const child = spawn(executable, argv, { cwd, env: childEnv, detached: ownsProcessGroup, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
51
|
+
const stop = signal => {
|
|
52
|
+
try { if (ownsProcessGroup) process.kill(-child.pid, signal); else child.kill(signal); }
|
|
53
|
+
catch (error) { if (error.code !== 'ESRCH') throw error; }
|
|
54
|
+
};
|
|
55
|
+
let stdout = '', stderr = '', timeoutReached = false;
|
|
56
|
+
const timer = setTimeout(() => { timeoutReached = true; stop('SIGTERM'); }, timeout);
|
|
57
|
+
const forced = setTimeout(() => { if (timeoutReached) stop('SIGKILL'); }, timeout + 5000);
|
|
58
|
+
const progress = setInterval(() => console.log(JSON.stringify({ running: label })), 20000);
|
|
59
|
+
child.stdout.on('data', bytes => { stdout = (stdout + bytes).slice(-2000000); });
|
|
60
|
+
child.stderr.on('data', bytes => { stderr = (stderr + bytes).slice(-2000000); });
|
|
61
|
+
try {
|
|
62
|
+
const status = await new Promise((resolve, reject) => { child.once('error', reject); child.once('close', (code, signal) => resolve({ code, signal })); });
|
|
63
|
+
fs.writeFileSync(path.join(directory, label + '.log'), omitOutput ? `exit=${status.code}; output omitted\n` : redactSecrets(stdout + stderr), { mode: 0o600 });
|
|
64
|
+
if (!allowFailure) assert.equal(status.code, 0, `${label} failed (${status.signal || status.code}): ${redactSecrets(stderr).slice(-2000)}`);
|
|
65
|
+
return { ...status, stdout, stderr };
|
|
66
|
+
} finally { clearTimeout(timer); clearTimeout(forced); clearInterval(progress); }
|
|
67
|
+
}
|
|
68
|
+
const hash = file => createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
69
|
+
function read(project, sql, ...params) {
|
|
70
|
+
const db = new DatabaseSync(path.join(project, '.hello-cc/mesh.db'), { readOnly: true });
|
|
71
|
+
try { return db.prepare(sql).all(...params).map(row => ({ ...row })); } finally { db.close(); }
|
|
72
|
+
}
|
|
73
|
+
let cli, api, rpc, nativeUp = false;
|
|
74
|
+
const peer = 'installed-dsh', marker = 'INSTALLED_' + randomUUID().replaceAll('-', '').slice(0, 16);
|
|
75
|
+
const denyTarget = path.join(root, 'permission-denied.txt');
|
|
76
|
+
async function hcc(argv, project = root) {
|
|
77
|
+
const result = await run('hcc-' + argv.slice(0, 2).join('-'), process.execPath, [cli, '--root', project, '--json', ...argv], { cwd: project, timeout: 30000 });
|
|
78
|
+
const parsed = JSON.parse(result.stdout); return parsed.data ?? parsed;
|
|
79
|
+
}
|
|
80
|
+
async function until(fn, label, timeout = 120000) {
|
|
81
|
+
const deadline = Date.now() + timeout; let notice = Date.now() + 20000;
|
|
82
|
+
while (Date.now() < deadline) { const value = await fn(); if (value) return value;
|
|
83
|
+
if (Date.now() > notice) { console.log(JSON.stringify({ waiting: label })); notice += 20000; } await delay(250); }
|
|
84
|
+
throw new Error('Timed out: ' + label);
|
|
85
|
+
}
|
|
86
|
+
async function send(body, task) { return hcc(['native', 'send', '--peer', peer, '--from', 'acceptance-coordinator', '--body', body, ...(task ? ['--task', String(task)] : [])]); }
|
|
87
|
+
async function finish(message, { decline = false, label = 'native turn' } = {}) {
|
|
88
|
+
return until(async () => {
|
|
89
|
+
const pending = await hcc(['native', 'requests', '--peer', peer]);
|
|
90
|
+
for (const request of pending.filter(item => item.status === 'pending')) {
|
|
91
|
+
const serialized = JSON.stringify(request.params || request);
|
|
92
|
+
if (decline) assert.ok(serialized.includes(denyTarget), 'The refused ACP request must name the test-owned target file');
|
|
93
|
+
else assert.ok(serialized.includes(root) || /hcc_|hello_cc_scoped/.test(serialized), 'Only acceptance-owned file or collaboration operations may be approved');
|
|
94
|
+
const decision = decline ? 'decline' : 'accept';
|
|
95
|
+
await hcc(['native', 'respond', '--peer', peer, '--request', String(request.requestId), '--decision', decision]);
|
|
96
|
+
receipt.approvals ||= []; receipt.approvals.push({ method: request.method, decision, requestId: request.requestId, turnId: request.turnId, ...(decline ? { target: denyTarget } : {}) });
|
|
97
|
+
}
|
|
98
|
+
const deliveries = await hcc(['native', 'deliveries', '--peer', peer]);
|
|
99
|
+
const row = (Array.isArray(deliveries) ? deliveries : deliveries.deliveries).find(item => item.message_id === message.message_id);
|
|
100
|
+
if (!row || !['completed', 'failed', 'unknown'].includes(row.state)) return false;
|
|
101
|
+
assert.equal(row.state, 'completed', JSON.stringify(row));
|
|
102
|
+
const replies = read(root, 'SELECT body FROM messages WHERE reply_to=?', message.message_id);
|
|
103
|
+
assert.equal(replies.length, 1); assert.equal(read(root, 'SELECT * FROM message_reads WHERE peer=? AND message_id=?', peer, message.message_id).length, 1);
|
|
104
|
+
return { row, reply: replies[0].body };
|
|
105
|
+
}, label, 180000);
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
let tarball = opt('--tarball');
|
|
109
|
+
if (!tarball) {
|
|
110
|
+
const packed = await run('npm-pack', 'npm', ['pack', '--ignore-scripts', '--json', '--pack-destination', directory], { cwd: repo });
|
|
111
|
+
const meta = JSON.parse(packed.stdout)[0]; tarball = path.join(directory, meta.filename);
|
|
112
|
+
receipt.package = { filename: meta.filename, fileCount: meta.files.length, shasum: meta.shasum };
|
|
113
|
+
}
|
|
114
|
+
tarball = fs.realpathSync(tarball);
|
|
115
|
+
receipt.package = { ...receipt.package, filename: path.basename(tarball), sha256: hash(tarball) };
|
|
116
|
+
const candidate = path.join(output, path.basename(tarball));
|
|
117
|
+
if (candidate !== tarball) fs.copyFileSync(tarball, candidate);
|
|
118
|
+
receipt.artifacts.tarball = candidate;
|
|
119
|
+
receipt.platform = { os: process.platform, architecture: process.arch };
|
|
120
|
+
receipt.officialLauncher = binary;
|
|
121
|
+
receipt.packageInstallPrefix = prefix;
|
|
122
|
+
await run('npm-install', 'npm', ['install', '--prefix', prefix, '--cache', path.resolve(opt('--npm-cache', path.join(directory, 'npm-cache'))), '--no-audit', '--no-fund', tarball], { childEnv: noModelEnv, timeout: 240000 });
|
|
123
|
+
const pkgDir = path.join(prefix, 'node_modules/@logicseek/hello-cc');
|
|
124
|
+
cli = path.join(pkgDir, 'bin/hcc.mjs');
|
|
125
|
+
const requireInstalled = createRequire(path.join(pkgDir, 'package.json'));
|
|
126
|
+
const pty = requireInstalled('node-pty');
|
|
127
|
+
const terminal = pty.spawn('/bin/sh', ['-c', 'printf INSTALLED_PTY_READY'], { cwd: root, env: noModelEnv, name: 'xterm', cols: 80, rows: 24 });
|
|
128
|
+
let ptyText = ''; terminal.onData(text => { ptyText += text; });
|
|
129
|
+
await new Promise(resolve => terminal.onExit(resolve)); assert.match(ptyText, /INSTALLED_PTY_READY/);
|
|
130
|
+
check('fresh npm package installation and installed Node 24 PTY dependency load');
|
|
131
|
+
const sourceFiles = ['bin/hcc.mjs','lib/cli/commands/dsh.mjs','lib/cli/commands/native.mjs','lib/integrations/dsh.mjs','lib/integrations/dsh-cordis.mjs','lib/integrations/dsh-collaboration.mjs','lib/integrations/dsh.bundle.yml','lib/integrations/native/dsh-acp.mjs','lib/integrations/native/jsonrpc.mjs','lib/runtime/native/service.mjs','lib/runtime/native/client.mjs','lib/mcp/tools.mjs'];
|
|
132
|
+
receipt.installedSourceHashes = Object.fromEntries(sourceFiles.map(file => [file, hash(path.join(pkgDir, file))]));
|
|
133
|
+
for (const file of sourceFiles) assert.equal(hash(path.join(repo, file)), receipt.installedSourceHashes[file], 'Installed runtime must match current source: ' + file);
|
|
134
|
+
check('installed Cordis, ACP, MCP and native service match current source hashes');
|
|
135
|
+
await run('installed-cli-help', process.execPath, [cli, 'dsh', '--help'], { childEnv: noModelEnv });
|
|
136
|
+
await hcc(['dsh', 'setup', '--mode', 'cordis']);
|
|
137
|
+
const firstConfig = hash(path.join(root, '.hello-cc/dsh/cordis.patch.yml'));
|
|
138
|
+
await hcc(['dsh', 'setup']); assert.equal(hash(path.join(root, '.hello-cc/dsh/cordis.patch.yml')), firstConfig);
|
|
139
|
+
const dshStatus = await hcc(['dsh', 'status']); assert.equal(dshStatus.mode, 'cordis');
|
|
140
|
+
check('installed CLI setup is idempotent; default PATH resolves the pinned official dsh launcher');
|
|
141
|
+
|
|
142
|
+
const profile = 'hcc-cordis-acceptance';
|
|
143
|
+
await run('profile-init', process.execPath, [binary, '--profile', profile, '--from-default-profile', 'acp', '--dump-config'], { childEnv: noModelEnv, omitOutput: true });
|
|
144
|
+
const profileDir = path.join(env.DSH_HOME, 'profiles', profile);
|
|
145
|
+
const overlay = path.join(profileDir, 'cordis.patch.yml');
|
|
146
|
+
if (!fs.existsSync(overlay)) fs.writeFileSync(overlay, '[]\n');
|
|
147
|
+
fs.appendFileSync(overlay, '\n# acceptance-owned override must survive package management\n');
|
|
148
|
+
const overlayHash = hash(overlay);
|
|
149
|
+
const manifest = () => JSON.parse(fs.readFileSync(path.join(profileDir, 'package.json')));
|
|
150
|
+
// Pin the only native build required by this candidate in the private profile.
|
|
151
|
+
fs.appendFileSync(path.join(profileDir, 'pnpm-workspace.yaml'), '\nallowBuilds:\n node-pty@1.2.0-beta.15: true\n');
|
|
152
|
+
receipt.approvedBuilds = ['node-pty@1.2.0-beta.15'];
|
|
153
|
+
const pnpmStore = path.resolve(opt('--pnpm-store', path.join(directory, 'pnpm-store')));
|
|
154
|
+
for (let i = 0; i < 2; i++) await run('plugin-add-' + i, process.execPath, [binary, 'plugin', '--profile', profile, 'add', tarball, '--store-dir', pnpmStore], { childEnv: noModelEnv, timeout: 900000 });
|
|
155
|
+
assert.equal(manifest().dsh.profile.bundles.filter(name => name === '@logicseek/hello-cc').length, 1);
|
|
156
|
+
assert.equal(hash(overlay), overlayHash);
|
|
157
|
+
const config = await run('installed-profile-config', process.execPath, [binary, '--profile', profile, '--dump-config'], { childEnv: noModelEnv, omitOutput: true });
|
|
158
|
+
assert.match(config.stdout, /dsh-cordis\.mjs/);
|
|
159
|
+
check('official plugin add and repeated add install one Cordis bundle and preserve profile overrides');
|
|
160
|
+
rpc = new JsonRpcProcess({ binary, args: ['--profile', profile], cwd: cordisRoot, env: live ? env : noModelEnv, timeoutMs: 30000 });
|
|
161
|
+
await rpc.start();
|
|
162
|
+
await rpc.request('initialize', { protocolVersion: 1, clientCapabilities: {}, clientInfo: { name: 'hcc-installed-acceptance', version: '1.0.1' } });
|
|
163
|
+
const session = await rpc.request('session/new', { cwd: cordisRoot, mcpServers: [] });
|
|
164
|
+
const binding = read(cordisRoot, 'SELECT * FROM peer_bindings WHERE provider_session_id=?', session.sessionId)[0];
|
|
165
|
+
assert.equal(binding.transport, 'cordis');
|
|
166
|
+
if (live) {
|
|
167
|
+
await rpc.request('session/prompt', { sessionId: session.sessionId, prompt: [{ type: 'text', text: `Use hcc_state, then hcc_message_send once to acceptance-coordinator with exact body ${marker}. Reply only ${marker}. Do not use other tools or files.` }] }, { timeoutMs: 180000 });
|
|
168
|
+
assert.equal(read(cordisRoot, 'SELECT sender FROM messages WHERE body=?', marker)[0].sender, binding.peer);
|
|
169
|
+
check('officially installed Cordis bundle performs a real model collaboration tool round trip');
|
|
170
|
+
}
|
|
171
|
+
await rpc.request('session/close', { sessionId: session.sessionId });
|
|
172
|
+
await rpc.close(); rpc = null;
|
|
173
|
+
check('officially installed bundle creates and disposes a real Agent without duplicate ownership');
|
|
174
|
+
const beforeRemovalHash = hash(overlay);
|
|
175
|
+
receipt.profileOverlayHashes = { beforeAdd: overlayHash, beforeRemove: beforeRemovalHash };
|
|
176
|
+
await run('plugin-remove', process.execPath, [binary, 'plugin', '--profile', profile, 'remove', '@logicseek/hello-cc', '--store-dir', pnpmStore], { childEnv: noModelEnv, timeout: 900000 });
|
|
177
|
+
assert.ok(!manifest().dsh.profile.bundles.includes('@logicseek/hello-cc'));
|
|
178
|
+
assert.equal(hash(overlay), beforeRemovalHash);
|
|
179
|
+
receipt.profileOverlayHashes.afterRemove = hash(overlay);
|
|
180
|
+
await run('profile-after-remove', process.execPath, [binary, '--profile', profile, '--dump-config'], { childEnv: noModelEnv, omitOutput: true });
|
|
181
|
+
check('official plugin remove removes bundle metadata and preserves other profile configuration');
|
|
182
|
+
|
|
183
|
+
const acpProfile = path.join(env.DSH_HOME, 'profiles/acp');
|
|
184
|
+
await run('native-profile-init', process.execPath, [binary, '--profile', 'acp', '--dump-config'], { childEnv: noModelEnv, omitOutput: true });
|
|
185
|
+
const policy = path.join(directory, 'approval-policy.mjs');
|
|
186
|
+
fs.writeFileSync(policy, `export const inject=['tools'];\nexport function apply(ctx){ctx.on('tools/pre-execute',async(exec,next)=>{const args=exec.arguments||{};if((['write','str_replace_editor'].includes(exec.name)&&[args.file_path,args.path].includes(${JSON.stringify(denyTarget)}))||(exec.name==='bash'&&String(args.command||'').includes(${JSON.stringify(denyTarget)})))return {kind:'ask',reason:'acceptance requires explicit approval for this test-owned write'};return next();});}\n`);
|
|
187
|
+
fs.writeFileSync(path.join(acpProfile, 'cordis.patch.yml'), `- insert:\n - id: installed-approval-policy\n name: ${JSON.stringify(policy)}\n`);
|
|
188
|
+
// The project overlay is not loaded by native ACP. Only the approval policy
|
|
189
|
+
// enters its private profile; Cordis/hooks never duplicate the native owner.
|
|
190
|
+
const nativeClient = await import(pathToFileURL(path.join(pkgDir, 'lib/runtime/native/client.mjs')));
|
|
191
|
+
const ctx = { root, dbPath: path.join(root, '.hello-cc/mesh.db') };
|
|
192
|
+
api = (method, route, body) => nativeClient.nativeRequest(ctx, method, route, body);
|
|
193
|
+
await hcc(['native', 'up']); nativeUp = true;
|
|
194
|
+
const started = await hcc(['native', 'start', '--peer', peer, '--provider', 'dsh']);
|
|
195
|
+
assert.equal(started.provider, 'dsh'); assert.ok(started.sessionId); assert.ok(started.capabilities.approvals);
|
|
196
|
+
receipt.nativeSessionId = started.sessionId;
|
|
197
|
+
check('installed public native CLI starts the default dsh ACP adapter and scoped MCP without factory injection');
|
|
198
|
+
if (live) {
|
|
199
|
+
const permission = await send(`Use the write tool once with file_path ${denyTarget} and content PROOF. This is a permission refusal test. If refused, do not retry or bypass the refusal; reply only DENIED. Do not read or modify anything else.`);
|
|
200
|
+
await finish(permission, { decline: true, label: 'installed real native approval refusal' });
|
|
201
|
+
assert.ok(receipt.approvals?.some(item => item.decision === 'decline' && item.method === 'session/request_permission'));
|
|
202
|
+
assert.equal(fs.existsSync(denyTarget), false);
|
|
203
|
+
check('installed native CLI receives a real ACP file approval, declines it, and the file remains absent');
|
|
204
|
+
fs.writeFileSync(path.join(root, 'orders.json'), JSON.stringify([{ sku:'A',quantity:3,unit_price:12.5 },{ sku:'B',quantity:2,unit_price:8 },{ sku:'A',quantity:1,unit_price:12.5 }]));
|
|
205
|
+
const task = await hcc(['task','create','--from','acceptance-coordinator','--title','Summarize isolated order fixture']);
|
|
206
|
+
const reportFile = path.join(root, 'order-summary.json');
|
|
207
|
+
const work = await send(`This is an isolated order-report task #${task.id}. Use the hello_cc_scoped MCP hcc_task_next to claim it and hcc_lock_acquire for order-summary.json with task_id ${task.id}. Read only orders.json. Compute total quantity and revenue from the input records, aggregate quantity and revenue per SKU, and write ${reportFile} as a JSON object with keys total_quantity,total_revenue,by_sku (by_sku maps SKU to {quantity,revenue}). Use native read/write tools. Send the exact body ${marker} to acceptance-coordinator using MCP hcc_message_send with task_id ${task.id}. Record a local hcc_result_record with task_id ${task.id}, title "Order fixture report", status passed, evidence ["${reportFile}"]; record hcc_handoff to acceptance-coordinator with the same evidence in changed_files. Release your order-summary.json lock. Do not use any other files, external requests or delegation. Reply only ${marker}.`, task.id);
|
|
208
|
+
const finished = await finish(work, { label:'installed native scoped MCP task and file workflow' });
|
|
209
|
+
assert.match(finished.reply, new RegExp(marker));
|
|
210
|
+
const actual = JSON.parse(fs.readFileSync(reportFile));
|
|
211
|
+
assert.deepEqual(actual, {total_quantity:6,total_revenue:66,by_sku:{A:{quantity:4,revenue:50},B:{quantity:2,revenue:16}}});
|
|
212
|
+
const taskRow = read(root, 'SELECT * FROM tasks WHERE id=?', task.id)[0]; assert.equal(taskRow.owner, peer);
|
|
213
|
+
assert.equal(read(root,'SELECT * FROM messages WHERE sender=? AND recipient=? AND body=? AND reply_to IS NULL',peer,'acceptance-coordinator',marker).length,1);
|
|
214
|
+
assert.ok(read(root,'SELECT * FROM handoffs WHERE task_id=? AND from_peer=?',task.id,peer).length);
|
|
215
|
+
assert.ok(read(root,"SELECT * FROM events WHERE type='task.result.recorded' AND task_id=? AND actor=?",task.id,peer).length);
|
|
216
|
+
assert.equal(read(root,'SELECT * FROM locks WHERE owner=? AND task_id=?',peer,task.id).length,0);
|
|
217
|
+
const completion = await send(`The coordinator independently verified order-summary.json: quantity 6, revenue 66 and correct per-SKU totals. Finish your owned task #${task.id} using your session-specific hcc CLI command: task done --peer ${peer} --id ${task.id} --summary ${marker}. Use only that hcc task command; no other tools or file changes. Reply only ${marker}.`);
|
|
218
|
+
await finish(completion, { label:'installed task completion after independent artifact verification' });
|
|
219
|
+
assert.equal(read(root,'SELECT status FROM tasks WHERE id=?',task.id)[0].status,'done');
|
|
220
|
+
const reportCopy = path.join(output, 'order-summary.json');
|
|
221
|
+
if (reportCopy !== reportFile) fs.copyFileSync(reportFile, reportCopy);
|
|
222
|
+
receipt.artifacts.orderReport = reportCopy; receipt.taskId=task.id;
|
|
223
|
+
receipt.workflow = { expected: actual, owner: peer, status: 'done', proactiveMessageCount: 1, resultRecorded: true, handoffRecorded: true, locksReleased: true, scope: 'isolated representative fixture; not business signoff' };
|
|
224
|
+
check('real installed worker claims a task, writes correct report, records evidence and handoff, releases lock and completes task');
|
|
225
|
+
} else receipt.limitations.push('Real model approval and task workflow require --run-live and were not executed.');
|
|
226
|
+
await hcc(['native','close','--peer',peer]);
|
|
227
|
+
assert.equal(read(root,'SELECT status FROM peers WHERE id=?',peer)[0].status,'exited');
|
|
228
|
+
check('installed public close retires only its own native worker');
|
|
229
|
+
receipt.passed = true;
|
|
230
|
+
} catch (error) { receipt.passed = false; receipt.failure={code:error.code||'ACCEPTANCE_FAILED',message:redactSecrets(error.message),stack:redactSecrets(error.stack)}; process.exitCode=1; }
|
|
231
|
+
finally {
|
|
232
|
+
try { await rpc?.close(); receipt.cleanup.cordisRuntimeStopped=true; } catch { receipt.cleanup.cordisRuntimeStopped=false; }
|
|
233
|
+
if (nativeUp) {
|
|
234
|
+
await hcc(['native','close','--peer',peer]).catch(()=>{});
|
|
235
|
+
try { await hcc(['native','down']); receipt.cleanup.nativeRuntimeStopped=!fs.existsSync(path.join(root,'.hello-cc/native/runtime.json')); }
|
|
236
|
+
catch { receipt.cleanup.nativeRuntimeStopped=false; }
|
|
237
|
+
}
|
|
238
|
+
fs.rmSync(env.DSH_HOME,{recursive:true,force:true}); receipt.cleanup.privateProviderStateRemoved=true;
|
|
239
|
+
if (receipt.installedSourceHashes) receipt.currentRuntimeSourcesStillMatch=Object.entries(receipt.installedSourceHashes).every(([file,sha])=>hash(path.join(repo,file))===sha);
|
|
240
|
+
if (output !== directory) {
|
|
241
|
+
const evidence = path.join(output, 'evidence', path.basename(directory)); fs.mkdirSync(evidence, { recursive: true });
|
|
242
|
+
for (const file of fs.readdirSync(directory).filter(file => file.endsWith('.log'))) fs.copyFileSync(path.join(directory, file), path.join(evidence, file));
|
|
243
|
+
receipt.artifacts.commandLogs = evidence;
|
|
244
|
+
}
|
|
245
|
+
receipt.completedAt=new Date().toISOString(); flush();
|
|
246
|
+
console.log(JSON.stringify({receipt:receiptPath,passed:receipt.passed,checks:receipt.checks.length,cleanup:receipt.cleanup}));
|
|
247
|
+
}
|