@logicseek/hello-cc 1.1.0-rc.4 → 1.1.0-rc.6
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 +62 -0
- package/docs/README.md +2 -0
- package/docs/README.zh-CN.md +2 -0
- package/docs/app-bridge.md +110 -0
- package/docs/app-bridge.zh-CN.md +80 -0
- package/docs/dsh.md +1 -1
- package/docs/dsh.zh-CN.md +1 -1
- package/lib/cli/app.mjs +3 -0
- package/lib/cli/commands/app-bridge.mjs +89 -0
- package/lib/cli/commands/hook.mjs +21 -2
- package/lib/coordination-state.mjs +5 -2
- package/lib/core/coordination/messages.mjs +7 -2
- package/lib/integrations/claude-app-collaboration.mjs +222 -0
- package/lib/integrations/claude-app.mjs +289 -0
- package/lib/integrations/claude-mod/register.mjs +141 -0
- package/lib/integrations/codex-app-probe.mjs +213 -0
- package/lib/integrations/dsh-collaboration.mjs +19 -2
- package/lib/integrations/dsh-cordis.d.ts +2 -0
- package/lib/integrations/dsh-cordis.mjs +26 -33
- package/lib/integrations/dsh-inbox.mjs +251 -0
- package/lib/ui/help.mjs +1 -0
- package/package.json +1 -1
- package/scripts/dsh-inbox-acceptance.mjs +209 -0
- package/scripts/helpers/sqlite-observer.mjs +9 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// Exercises the production Cordis plugin in the pinned official Agent runtime.
|
|
2
|
+
// ACP only creates/disposes test Agents: no session/prompt is ever submitted.
|
|
3
|
+
// All model responses come from a deterministic localhost fixture.
|
|
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 http from 'node:http';
|
|
9
|
+
import { createRequire } from 'node:module';
|
|
10
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
11
|
+
import { spawnSync } from 'node:child_process';
|
|
12
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
13
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
14
|
+
import { checkDshRuntime } from '../lib/integrations/dsh-cordis.mjs';
|
|
15
|
+
import { JsonRpcProcess } from '../lib/integrations/native/jsonrpc.mjs';
|
|
16
|
+
import { readAcceptanceRows } from './helpers/sqlite-observer.mjs';
|
|
17
|
+
|
|
18
|
+
const args = process.argv.slice(2);
|
|
19
|
+
if (args.includes('--help')) {
|
|
20
|
+
console.log('Usage: node scripts/dsh-inbox-acceptance.mjs --dsh-install DIRECTORY [--output FILE]');
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
const options = {};
|
|
24
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
25
|
+
assert.ok(['--dsh-install', '--output'].includes(args[i]) && args[i + 1] && !args[i + 1].startsWith('--'), 'Invalid acceptance arguments');
|
|
26
|
+
assert.equal(options[args[i]], undefined, 'Duplicate acceptance option');
|
|
27
|
+
options[args[i]] = args[i + 1];
|
|
28
|
+
}
|
|
29
|
+
assert.ok(options['--dsh-install'], 'An existing official DSH installation is required');
|
|
30
|
+
const requireOfficial = createRequire(path.join(path.resolve(options['--dsh-install']), 'package.json'));
|
|
31
|
+
const officialBinary = requireOfficial.resolve('@deepseek-ai/dsh/lib/bin.js');
|
|
32
|
+
checkDshRuntime(officialBinary);
|
|
33
|
+
const repo = fileURLToPath(new URL('..', import.meta.url));
|
|
34
|
+
const sandbox = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'hcc-dsh-inbox-')));
|
|
35
|
+
const home = path.join(sandbox, 'home');
|
|
36
|
+
const roots = ['project A', 'project B', 'denied'].map(name => path.join(sandbox, name));
|
|
37
|
+
for (const directory of [home, ...roots]) fs.mkdirSync(directory, { mode: 0o700 });
|
|
38
|
+
const env = { PATH: path.dirname(process.execPath) + path.delimiter + (process.env.PATH || '/usr/bin:/bin'),
|
|
39
|
+
HOME: home, DSH_HOME: path.join(home, 'dsh'), TMPDIR: sandbox, LANG: 'C.UTF-8', SHELL: '/bin/sh',
|
|
40
|
+
DSH_TELEMETRY_DISABLED: '1', DEEPSEEK_API_KEY: 'local-fixture-no-credential' };
|
|
41
|
+
const output = path.resolve(options['--output'] || path.join(sandbox, 'receipt.json'));
|
|
42
|
+
const marker = 'IDLE_' + randomUUID().replaceAll('-', '');
|
|
43
|
+
const traceFile = path.join(sandbox, 'events.jsonl');
|
|
44
|
+
const hash = file => createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
45
|
+
const sources = ['lib/integrations/dsh-cordis.mjs', 'lib/integrations/dsh-inbox.mjs',
|
|
46
|
+
'lib/integrations/dsh-collaboration.mjs', 'lib/core/coordination/messages.mjs',
|
|
47
|
+
'lib/coordination-state.mjs', 'scripts/dsh-inbox-acceptance.mjs', 'scripts/helpers/sqlite-observer.mjs'];
|
|
48
|
+
const receipt = { schemaVersion: 1, startedAt: new Date().toISOString(), baseline: '0.2.0-rc.2',
|
|
49
|
+
mode: 'Official Cordis Agent runtime; deterministic local model; isolated sessions; no Desktop UI',
|
|
50
|
+
realModelCalls: false, explicitPromptRequests: 0, permissions: [], checks: [], completed: false,
|
|
51
|
+
sourceHashes: Object.fromEntries(sources.map(name => [name, hash(path.join(repo, name))])) };
|
|
52
|
+
const databases = new Map(), modelRequests = [], notices = [];
|
|
53
|
+
let rpc, server, modelError;
|
|
54
|
+
const pass = name => receipt.checks.push({ name, passed: true });
|
|
55
|
+
function childNode(argv, root) {
|
|
56
|
+
const result = spawnSync(process.execPath, argv, { cwd: root, env, encoding: 'utf8', timeout: 15000 });
|
|
57
|
+
assert.equal(result.status, 0, result.stderr.slice(-2000));
|
|
58
|
+
return result.stdout.trim();
|
|
59
|
+
}
|
|
60
|
+
function hcc(root, argv) {
|
|
61
|
+
const value = JSON.parse(childNode([path.join(repo, 'bin/hcc.mjs'), '--root', root, '--json', ...argv], root));
|
|
62
|
+
return value.data ?? value;
|
|
63
|
+
}
|
|
64
|
+
function rows(root, sql, ...values) {
|
|
65
|
+
if (!databases.has(root)) {
|
|
66
|
+
const module = pathToFileURL(path.join(repo, 'lib/runtime/paths.mjs')).href;
|
|
67
|
+
databases.set(root, childNode(['--input-type=module', '-e',
|
|
68
|
+
`import { projectDbPath } from ${JSON.stringify(module)}; console.log(projectDbPath(process.argv[1]));`, root], root));
|
|
69
|
+
}
|
|
70
|
+
return readAcceptanceRows(databases.get(root), sql, ...values);
|
|
71
|
+
}
|
|
72
|
+
const events = () => fs.existsSync(traceFile)
|
|
73
|
+
? fs.readFileSync(traceFile, 'utf8').trim().split('\n').filter(Boolean).map(line => JSON.parse(line)) : [];
|
|
74
|
+
async function until(check, description) {
|
|
75
|
+
const deadline = Date.now() + 15000;
|
|
76
|
+
while (Date.now() < deadline) {
|
|
77
|
+
if (modelError) throw modelError;
|
|
78
|
+
if (check()) return;
|
|
79
|
+
await delay(30);
|
|
80
|
+
}
|
|
81
|
+
throw new Error('Timed out: ' + description);
|
|
82
|
+
}
|
|
83
|
+
function stream(res, model, block) {
|
|
84
|
+
res.writeHead(200, { 'content-type': 'text/event-stream' });
|
|
85
|
+
const emit = (type, data) => res.write(`event: ${type}\ndata: ${JSON.stringify({ type, ...data })}\n\n`);
|
|
86
|
+
emit('message_start', { message: { id: 'msg-' + randomUUID(), type: 'message', role: 'assistant', model,
|
|
87
|
+
content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: 1, output_tokens: 0 } } });
|
|
88
|
+
emit('content_block_start', { index: 0, content_block: block.type === 'tool_use' ? { ...block, input: {} } : block });
|
|
89
|
+
if (block.type === 'tool_use') emit('content_block_delta', { index: 0, delta: { type: 'input_json_delta', partial_json: JSON.stringify(block.input) } });
|
|
90
|
+
emit('content_block_stop', { index: 0 });
|
|
91
|
+
emit('message_delta', { delta: { stop_reason: block.type === 'tool_use' ? 'tool_use' : 'end_turn', stop_sequence: null }, usage: { output_tokens: 1 } });
|
|
92
|
+
emit('message_stop', {}); res.end();
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const policy = path.join(sandbox, 'acceptance-policy.mjs');
|
|
96
|
+
fs.writeFileSync(policy, `import fs from 'node:fs';
|
|
97
|
+
export const inject = ['agents', 'sessions'];
|
|
98
|
+
export function apply(ctx) {
|
|
99
|
+
const record = value => fs.appendFileSync(${JSON.stringify(traceFile)}, JSON.stringify(value) + '\\n', {mode:0o600});
|
|
100
|
+
ctx.on('agent/pre-step', async ({agent}, next) => {
|
|
101
|
+
if (agent.session.header.cwd === ${JSON.stringify(roots[2])}) {
|
|
102
|
+
record({type:'admission-rejected', sessionId:agent.session.header.id});
|
|
103
|
+
return {kind:'reject'};
|
|
104
|
+
}
|
|
105
|
+
return next();
|
|
106
|
+
});
|
|
107
|
+
ctx.on('agent/status', ({agent,status}) => record({type:'status',sessionId:agent.session.header.id,status}));
|
|
108
|
+
ctx.on('session/event', (session,event) => {
|
|
109
|
+
if (event.type === 'user/message') record({type:event.type,sessionId:session.header.id,id:event.data.id,source:event.data.source?.kind});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
`, { mode: 0o600 });
|
|
113
|
+
const patch = path.join(sandbox, 'cordis.patch.yml');
|
|
114
|
+
fs.writeFileSync(patch, '- insert:\n - id: hcc-inbox-acceptance\n name: ' +
|
|
115
|
+
JSON.stringify(path.join(repo, 'lib/integrations/dsh-cordis.mjs')) +
|
|
116
|
+
'\n config:\n inboxPollMs: 100\n - id: hcc-inbox-policy\n name: ' + JSON.stringify(policy) + '\n', { mode: 0o600 });
|
|
117
|
+
server = http.createServer(async (request, response) => {
|
|
118
|
+
try {
|
|
119
|
+
assert.equal(request.method, 'POST');
|
|
120
|
+
let length = 0; const chunks = [];
|
|
121
|
+
for await (const chunk of request) { length += chunk.length; assert.ok(length < 2 * 1024 * 1024); chunks.push(chunk); }
|
|
122
|
+
const body = JSON.parse(Buffer.concat(chunks).toString());
|
|
123
|
+
modelRequests.push(body);
|
|
124
|
+
assert.ok(modelRequests.length <= 2, 'No duplicate or unrequested model round');
|
|
125
|
+
assert.ok(JSON.stringify(body.messages).includes(marker), 'The model sees the exact inbox marker');
|
|
126
|
+
if (modelRequests.length === 1) {
|
|
127
|
+
assert.ok(body.tools.some(tool => tool.name === 'hcc_message_send'));
|
|
128
|
+
stream(response, body.model, { type: 'tool_use', id: 'reply-once', name: 'hcc_message_send',
|
|
129
|
+
input: { to: 'inbox-coordinator', body: 'REPLY_' + marker, kind: 'note' } });
|
|
130
|
+
} else {
|
|
131
|
+
assert.ok(body.messages.some(message => message.content?.some(block => block.type === 'tool_result' && block.tool_use_id === 'reply-once')));
|
|
132
|
+
stream(response, body.model, { type: 'text', text: 'DONE_' + marker });
|
|
133
|
+
}
|
|
134
|
+
} catch (error) {
|
|
135
|
+
modelError = error;
|
|
136
|
+
response.writeHead(500); response.end(JSON.stringify({ error: { type: 'api_error', message: 'Acceptance assertion failed' } }));
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
140
|
+
env.DEEPSEEK_BASE_URL = `http://127.0.0.1:${server.address().port}`;
|
|
141
|
+
rpc = new JsonRpcProcess({ binary: process.execPath,
|
|
142
|
+
args: [officialBinary, '--profile', 'acp', '--patch', patch], cwd: roots[0], env, timeoutMs: 30000,
|
|
143
|
+
onNotification(method, params) { if (method === 'session/update') notices.push(params); },
|
|
144
|
+
onRequest(method, params) {
|
|
145
|
+
assert.equal(method, 'session/request_permission');
|
|
146
|
+
// Answer only the exact test-owned reply through the official approval
|
|
147
|
+
// path. All other requests are declined; no global policy is relaxed.
|
|
148
|
+
const input = params.toolCall?.rawInput;
|
|
149
|
+
const exactReply = params.toolCall?.title === 'hcc_message_send' &&
|
|
150
|
+
input?.to === 'inbox-coordinator' && input.body === 'REPLY_' + marker;
|
|
151
|
+
receipt.permissions.push({ tool: params.toolCall?.title, exactReply });
|
|
152
|
+
const option = params.options?.find(item => item.kind === (exactReply ? 'allow_once' : 'reject_once'));
|
|
153
|
+
return option ? { outcome: { outcome: 'selected', optionId: option.optionId } } : { outcome: { outcome: 'cancelled' } };
|
|
154
|
+
} });
|
|
155
|
+
await rpc.start();
|
|
156
|
+
const initialized = await rpc.request('initialize', { protocolVersion: 1, clientCapabilities: {},
|
|
157
|
+
clientInfo: { name: 'hcc-inbox-acceptance', version: '1' } });
|
|
158
|
+
assert.equal(initialized.protocolVersion, 1);
|
|
159
|
+
const sessions = [];
|
|
160
|
+
for (const root of roots) sessions.push(await rpc.request('session/new', { cwd: root, mcpServers: [] }));
|
|
161
|
+
const peers = roots.map((root, i) => rows(root, 'SELECT peer FROM peer_bindings WHERE provider_session_id=?', sessions[i].sessionId)[0].peer);
|
|
162
|
+
await delay(400);
|
|
163
|
+
assert.equal(modelRequests.length, 0);
|
|
164
|
+
pass('idle Agents with empty inboxes do not call the model');
|
|
165
|
+
hcc(roots[0], ['register', '--peer', 'inbox-coordinator', '--kind', 'shell']);
|
|
166
|
+
const sent = hcc(roots[0], ['msg', 'send', '--from', 'inbox-coordinator', '--to', peers[0], '--body', marker]);
|
|
167
|
+
await until(() => rows(roots[0], 'SELECT id FROM messages WHERE sender=? AND body=?', peers[0], 'REPLY_' + marker).length === 1, 'inbox reply');
|
|
168
|
+
await until(() => modelRequests.length === 2 && rows(roots[0], 'SELECT status FROM peers WHERE id=?', peers[0])[0].status === 'idle', 'Agent settles');
|
|
169
|
+
assert.equal(rows(roots[0], 'SELECT * FROM message_reads WHERE peer=? AND message_id=?', peers[0], sent.id).length, 1);
|
|
170
|
+
const committed = events().filter(event => event.type === 'user/message' && event.sessionId === sessions[0].sessionId);
|
|
171
|
+
assert.ok(committed.length >= 1);
|
|
172
|
+
assert.ok(committed.some(event => event.source === 'hello-cc'));
|
|
173
|
+
assert.ok(committed.every(event => event.source !== 'user'), 'No user prompt started the Agent');
|
|
174
|
+
assert.ok(notices.some(event => event.sessionId === sessions[0].sessionId), 'Official client receives original-session events');
|
|
175
|
+
pass('bus message wakes original idle Agent, commits context, ACKs and replies without session/prompt');
|
|
176
|
+
assert.equal(rows(roots[1], 'SELECT * FROM messages').length, 0);
|
|
177
|
+
assert.equal(events().filter(event => event.type === 'user/message' && event.sessionId === sessions[1].sessionId).length, 0);
|
|
178
|
+
pass('a sibling Agent in another project receives no message or model turn');
|
|
179
|
+
hcc(roots[2], ['register', '--peer', 'inbox-coordinator', '--kind', 'shell']);
|
|
180
|
+
const denied = hcc(roots[2], ['msg', 'send', '--from', 'inbox-coordinator', '--to', peers[2], '--body', 'DENIED_' + marker]);
|
|
181
|
+
await until(() => events().some(event => event.type === 'admission-rejected'), 'rejected admission');
|
|
182
|
+
await delay(500);
|
|
183
|
+
assert.equal(rows(roots[2], 'SELECT * FROM message_reads WHERE peer=? AND message_id=?', peers[2], denied.id).length, 0);
|
|
184
|
+
assert.equal(events().filter(event => event.type === 'admission-rejected').length, 1, 'Rejected input is not automatically retried');
|
|
185
|
+
assert.equal(modelRequests.length, 2);
|
|
186
|
+
pass('downstream admission rejection leaves the message unread without a wake loop');
|
|
187
|
+
for (const session of sessions) await rpc.request('session/close', { sessionId: session.sessionId });
|
|
188
|
+
hcc(roots[0], ['msg', 'send', '--from', 'inbox-coordinator', '--to', peers[0], '--body', 'AFTER_CLOSE_' + marker]);
|
|
189
|
+
await delay(400);
|
|
190
|
+
assert.equal(modelRequests.length, 2);
|
|
191
|
+
assert.equal(rows(roots[0], 'SELECT status FROM peers WHERE id=?', peers[0])[0].status, 'exited');
|
|
192
|
+
pass('disposed Agents stay exited and are not woken by later inbox messages');
|
|
193
|
+
receipt.completed = true;
|
|
194
|
+
} catch (error) {
|
|
195
|
+
receipt.error = { name: error.name, message: error.message }; process.exitCode = 1;
|
|
196
|
+
} finally {
|
|
197
|
+
receipt.cleanup = {};
|
|
198
|
+
try { await rpc?.close(); receipt.cleanup.ownedRuntimeStopped = true; }
|
|
199
|
+
catch { receipt.cleanup.ownedRuntimeStopped = false; receipt.completed = false; process.exitCode = 1; }
|
|
200
|
+
if (server) { server.closeAllConnections(); await new Promise(resolve => server.close(resolve)); }
|
|
201
|
+
receipt.cleanup.localModelStopped = !server?.listening;
|
|
202
|
+
receipt.modelRequests = modelRequests.length;
|
|
203
|
+
receipt.finishedAt = new Date().toISOString();
|
|
204
|
+
receipt.changedSources = sources.filter(name => receipt.sourceHashes[name] !== hash(path.join(repo, name)));
|
|
205
|
+
if (receipt.changedSources.length) { receipt.completed = false; process.exitCode = 1; }
|
|
206
|
+
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
207
|
+
fs.writeFileSync(output, JSON.stringify(receipt, null, 2) + '\n', { mode: 0o600 });
|
|
208
|
+
console.log(JSON.stringify({ receipt: output, ...receipt }, null, 2));
|
|
209
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
|
|
3
|
+
// Acceptance observers race the real runtime's short-lived SQLite connections.
|
|
4
|
+
// Match its bounded busy timeout without retrying tasks or hiding query errors.
|
|
5
|
+
export function readAcceptanceRows(database, sql, ...values) {
|
|
6
|
+
const db = new DatabaseSync(database, { readOnly: true, timeout: 5000 });
|
|
7
|
+
try { return db.prepare(sql).all(...values); }
|
|
8
|
+
finally { db.close(); }
|
|
9
|
+
}
|