@ai-workbench/remote-agent 0.8.5 → 0.8.10
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/README.md +33 -8
- package/dist/acp.d.ts +22 -2
- package/dist/acp.js +143 -25
- package/dist/approval-label.d.ts +1 -1
- package/dist/approval-label.js +15 -3
- package/dist/broker-runtime.js +40 -8
- package/dist/broker.js +5 -1
- package/dist/claude-local.d.ts +37 -0
- package/dist/claude-local.js +207 -0
- package/dist/client.js +71 -2
- package/dist/conversation-details.d.ts +0 -1
- package/dist/conversation-details.js +37 -7
- package/dist/employee-endpoint.d.ts +91 -0
- package/dist/employee-endpoint.js +141 -0
- package/dist/employee-exec.d.ts +53 -0
- package/dist/employee-exec.js +423 -0
- package/dist/employee-location.d.ts +66 -0
- package/dist/employee-location.js +51 -0
- package/dist/employee-mcp.d.ts +42 -0
- package/dist/employee-mcp.js +181 -0
- package/dist/employee-native.d.ts +18 -0
- package/dist/employee-native.js +38 -0
- package/dist/employee-provider.d.ts +42 -0
- package/dist/employee-provider.js +326 -0
- package/dist/employee-relay.d.ts +18 -0
- package/dist/employee-relay.js +49 -0
- package/dist/employee-shell.d.ts +26 -0
- package/dist/employee-shell.js +89 -0
- package/dist/employee-ssh.d.ts +22 -0
- package/dist/employee-ssh.js +123 -0
- package/dist/employee-tools.d.ts +117 -0
- package/dist/employee-tools.js +292 -0
- package/dist/employee-workspace.d.ts +26 -0
- package/dist/employee-workspace.js +61 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +148 -28
- package/dist/input.d.ts +11 -0
- package/dist/input.js +61 -23
- package/dist/local-user-turn.d.ts +24 -0
- package/dist/local-user-turn.js +103 -0
- package/dist/presentation.d.ts +1 -1
- package/dist/presentation.js +46 -2
- package/dist/provider.d.ts +36 -2
- package/dist/provider.js +33 -0
- package/dist/readiness.js +1 -1
- package/dist/recovery.d.ts +14 -1
- package/dist/recovery.js +11 -2
- package/dist/remote-details.d.ts +7 -0
- package/dist/remote-details.js +11 -1
- package/helpers/ripgrep/LICENSE-vscode +13 -0
- package/helpers/ripgrep/PROVENANCE.md +9 -0
- package/helpers/ripgrep/darwin-arm64/rg +0 -0
- package/helpers/ripgrep/darwin-x64/rg +0 -0
- package/helpers/ripgrep/manifest.json +56 -0
- package/helpers/ripgrep/notices/COPYING-ripgrep +3 -0
- package/helpers/ripgrep/notices/DEPENDENCIES.json +946 -0
- package/helpers/ripgrep/notices/LICENCE-PCRE2.md +103 -0
- package/helpers/ripgrep/notices/LICENSE-MIT-ripgrep +21 -0
- package/helpers/ripgrep/notices/UNLICENSE-ripgrep +24 -0
- package/helpers/ripgrep/notices/aho-corasick-1.1.3/COPYING +3 -0
- package/helpers/ripgrep/notices/aho-corasick-1.1.3/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/aho-corasick-1.1.3/UNLICENSE +24 -0
- package/helpers/ripgrep/notices/anyhow-1.0.100/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/anyhow-1.0.100/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/arbitrary-1.4.2/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/arbitrary-1.4.2/LICENSE-MIT +27 -0
- package/helpers/ripgrep/notices/bstr-1.12.0/COPYING +8 -0
- package/helpers/ripgrep/notices/bstr-1.12.0/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/bstr-1.12.0/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/cc-1.2.41/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/cc-1.2.41/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/cfg-if-1.0.4/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/cfg-if-1.0.4/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/crossbeam-channel-0.5.15/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/crossbeam-channel-0.5.15/LICENSE-MIT +27 -0
- package/helpers/ripgrep/notices/crossbeam-channel-0.5.15/LICENSE-THIRD-PARTY +593 -0
- package/helpers/ripgrep/notices/crossbeam-deque-0.8.6/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/crossbeam-deque-0.8.6/LICENSE-MIT +27 -0
- package/helpers/ripgrep/notices/crossbeam-epoch-0.9.18/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/crossbeam-epoch-0.9.18/LICENSE-MIT +27 -0
- package/helpers/ripgrep/notices/crossbeam-utils-0.8.21/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/crossbeam-utils-0.8.21/LICENSE-MIT +27 -0
- package/helpers/ripgrep/notices/derive_arbitrary-1.4.2/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/derive_arbitrary-1.4.2/LICENSE-MIT +27 -0
- package/helpers/ripgrep/notices/encoding_rs-0.8.35/COPYRIGHT +17 -0
- package/helpers/ripgrep/notices/encoding_rs-0.8.35/LICENSE-APACHE +202 -0
- package/helpers/ripgrep/notices/encoding_rs-0.8.35/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/encoding_rs-0.8.35/LICENSE-WHATWG +26 -0
- package/helpers/ripgrep/notices/encoding_rs_io-0.1.7/COPYING +8 -0
- package/helpers/ripgrep/notices/encoding_rs_io-0.1.7/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/encoding_rs_io-0.1.7/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/find-msvc-tools-0.1.4/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/find-msvc-tools-0.1.4/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/getrandom-0.3.4/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/getrandom-0.3.4/LICENSE-MIT +26 -0
- package/helpers/ripgrep/notices/glob-0.3.3/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/glob-0.3.3/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/itoa-1.0.15/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/itoa-1.0.15/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/jobserver-0.1.34/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/jobserver-0.1.34/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/lexopt-0.3.1/LICENSE +19 -0
- package/helpers/ripgrep/notices/libc-0.2.177/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/libc-0.2.177/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/log-0.4.28/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/log-0.4.28/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/memchr-2.7.6/COPYING +3 -0
- package/helpers/ripgrep/notices/memchr-2.7.6/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/memchr-2.7.6/UNLICENSE +24 -0
- package/helpers/ripgrep/notices/memmap2-0.9.8/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/memmap2-0.9.8/LICENSE-MIT +26 -0
- package/helpers/ripgrep/notices/msvc_spectre_libs-0.1.3/LICENSE +21 -0
- package/helpers/ripgrep/notices/pcre2-0.2.11/COPYING +3 -0
- package/helpers/ripgrep/notices/pcre2-0.2.11/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/pcre2-0.2.11/UNLICENSE +24 -0
- package/helpers/ripgrep/notices/pcre2-sys-0.2.10/COPYING +3 -0
- package/helpers/ripgrep/notices/pcre2-sys-0.2.10/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/pcre2-sys-0.2.10/UNLICENSE +24 -0
- package/helpers/ripgrep/notices/pkg-config-0.3.32/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/pkg-config-0.3.32/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/proc-macro2-1.0.101/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/proc-macro2-1.0.101/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/quote-1.0.41/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/quote-1.0.41/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/r-efi-5.3.0/AUTHORS +74 -0
- package/helpers/ripgrep/notices/regex-1.12.2/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/regex-1.12.2/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/regex-automata-0.4.13/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/regex-automata-0.4.13/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/regex-syntax-0.8.8/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/regex-syntax-0.8.8/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/regex-syntax-0.8.8/src/unicode_tables/LICENSE-UNICODE +57 -0
- package/helpers/ripgrep/notices/ryu-1.0.20/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/ryu-1.0.20/LICENSE-BOOST +23 -0
- package/helpers/ripgrep/notices/same-file-1.0.6/COPYING +3 -0
- package/helpers/ripgrep/notices/same-file-1.0.6/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/same-file-1.0.6/UNLICENSE +24 -0
- package/helpers/ripgrep/notices/serde-1.0.228/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/serde-1.0.228/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/serde_core-1.0.228/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/serde_core-1.0.228/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/serde_derive-1.0.228/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/serde_derive-1.0.228/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/serde_json-1.0.145/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/serde_json-1.0.145/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/shlex-1.3.0/LICENSE-APACHE +13 -0
- package/helpers/ripgrep/notices/shlex-1.3.0/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/syn-2.0.106/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/syn-2.0.106/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/termcolor-1.4.1/COPYING +3 -0
- package/helpers/ripgrep/notices/termcolor-1.4.1/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/termcolor-1.4.1/UNLICENSE +24 -0
- package/helpers/ripgrep/notices/textwrap-0.16.2/LICENSE +21 -0
- package/helpers/ripgrep/notices/tikv-jemalloc-sys-0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/tikv-jemalloc-sys-0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/tikv-jemalloc-sys-0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7/jemalloc/COPYING +27 -0
- package/helpers/ripgrep/notices/tikv-jemallocator-0.6.0/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/tikv-jemallocator-0.6.0/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/unicode-ident-1.0.19/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/unicode-ident-1.0.19/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/unicode-ident-1.0.19/LICENSE-UNICODE +39 -0
- package/helpers/ripgrep/notices/walkdir-2.5.0/COPYING +3 -0
- package/helpers/ripgrep/notices/walkdir-2.5.0/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/walkdir-2.5.0/UNLICENSE +24 -0
- package/helpers/ripgrep/notices/wasip2-1.0.1+wasi-0.2.4/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/wasip2-1.0.1+wasi-0.2.4/LICENSE-Apache-2.0_WITH_LLVM-exception +220 -0
- package/helpers/ripgrep/notices/wasip2-1.0.1+wasi-0.2.4/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/winapi-util-0.1.11/COPYING +3 -0
- package/helpers/ripgrep/notices/winapi-util-0.1.11/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/winapi-util-0.1.11/UNLICENSE +24 -0
- package/helpers/ripgrep/notices/windows-link-0.2.1/license-apache-2.0 +201 -0
- package/helpers/ripgrep/notices/windows-link-0.2.1/license-mit +21 -0
- package/helpers/ripgrep/notices/windows-sys-0.61.2/license-apache-2.0 +201 -0
- package/helpers/ripgrep/notices/windows-sys-0.61.2/license-mit +21 -0
- package/helpers/ripgrep/notices/wit-bindgen-0.46.0/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/wit-bindgen-0.46.0/LICENSE-Apache-2.0_WITH_LLVM-exception +220 -0
- package/helpers/ripgrep/notices/wit-bindgen-0.46.0/LICENSE-MIT +23 -0
- package/locale/en.json +1 -1
- package/locale/zh.json +2 -2
- package/package.json +29 -11
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { randomUUID, createHash } from 'node:crypto';
|
|
3
|
+
import { readFile, writeFile, mkdir, glob, lstat, access, realpath, stat } from 'node:fs/promises';
|
|
4
|
+
import { constants } from 'node:fs';
|
|
5
|
+
import { resolve, dirname, isAbsolute, basename, join } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { NeedError } from "./remote.js";
|
|
8
|
+
export const EMPLOYEE_TOOL_NAMES = ['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash'];
|
|
9
|
+
const failure = (code) => new NeedError(code, '本机工具未完成(' + code + '),没有改到主机执行。', '本机实际执行能力;技术详情保留当前结果');
|
|
10
|
+
function object(value) { if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
11
|
+
throw failure('EMPLOYEE_ARGUMENT_INVALID'); return value; }
|
|
12
|
+
function string(args, key, required = true) {
|
|
13
|
+
const v = args[key];
|
|
14
|
+
if (v === undefined && !required)
|
|
15
|
+
return;
|
|
16
|
+
if (typeof v !== 'string' || v.includes('\0') || (required && !v))
|
|
17
|
+
throw failure('EMPLOYEE_ARGUMENT_INVALID');
|
|
18
|
+
return v;
|
|
19
|
+
}
|
|
20
|
+
function integer(args, key, min) { const v = args[key]; if (v === undefined)
|
|
21
|
+
return; if (!Number.isSafeInteger(v) || v < min)
|
|
22
|
+
throw failure('EMPLOYEE_ARGUMENT_INVALID'); return v; }
|
|
23
|
+
function boolean(args, key) { const v = args[key]; if (v === undefined)
|
|
24
|
+
return; if (typeof v !== 'boolean')
|
|
25
|
+
throw failure('EMPLOYEE_ARGUMENT_INVALID'); return v; }
|
|
26
|
+
function fields(args, allowed) { if (Object.keys(args).some(k => !allowed.includes(k)))
|
|
27
|
+
throw failure('EMPLOYEE_ARGUMENT_UNSUPPORTED'); }
|
|
28
|
+
function text(value) { return { content: [{ type: 'text', text: value }] }; }
|
|
29
|
+
// Shared across this plugin's sessions: protect the entire read/modify/write, not just writeFile.
|
|
30
|
+
const fileQueues = new Map();
|
|
31
|
+
async function serialized(key, work) {
|
|
32
|
+
const previous = fileQueues.get(key) ?? Promise.resolve();
|
|
33
|
+
let release = () => { };
|
|
34
|
+
const next = new Promise(yes => { release = yes; });
|
|
35
|
+
fileQueues.set(key, next);
|
|
36
|
+
await previous;
|
|
37
|
+
try {
|
|
38
|
+
return await work();
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
release();
|
|
42
|
+
if (fileQueues.get(key) === next)
|
|
43
|
+
fileQueues.delete(key);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function canonicalFile(path) {
|
|
47
|
+
try {
|
|
48
|
+
return await realpath(path);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (error.code !== 'ENOENT')
|
|
52
|
+
throw error;
|
|
53
|
+
const parent = dirname(path);
|
|
54
|
+
if (parent === path)
|
|
55
|
+
throw error;
|
|
56
|
+
return join(await canonicalFile(parent), basename(path));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function mutate(path, signal, work) {
|
|
60
|
+
return serialized('path:' + await canonicalFile(path), async () => {
|
|
61
|
+
signal.throwIfAborted();
|
|
62
|
+
let inode;
|
|
63
|
+
try {
|
|
64
|
+
const info = await stat(path);
|
|
65
|
+
inode = `inode:${info.dev}:${info.ino}`;
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (error.code !== 'ENOENT')
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
const perform = () => { signal.throwIfAborted(); return work(); };
|
|
72
|
+
return inode ? serialized(inode, perform) : perform();
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
async function bundledRipgrep() {
|
|
76
|
+
if (process.platform !== 'darwin' || !['arm64', 'x64'].includes(process.arch))
|
|
77
|
+
throw failure('EMPLOYEE_PLATFORM_UNSUPPORTED');
|
|
78
|
+
const root = new URL('./helpers/ripgrep/', import.meta.resolve('@ai-workbench/remote-agent/package.json'));
|
|
79
|
+
const manifest = JSON.parse(await readFile(new URL('manifest.json', root), 'utf8'));
|
|
80
|
+
const arch = 'darwin-' + process.arch, path = fileURLToPath(new URL(arch + '/rg', root)), entry = manifest.files[arch];
|
|
81
|
+
const info = await lstat(path);
|
|
82
|
+
if (!entry || !info.isFile() || info.isSymbolicLink() || info.size !== entry.bytes || createHash('sha256').update(await readFile(path)).digest('hex') !== entry.sha256)
|
|
83
|
+
throw failure('EMPLOYEE_SEARCH_COMPONENT_INVALID');
|
|
84
|
+
await access(path, constants.X_OK);
|
|
85
|
+
return path;
|
|
86
|
+
}
|
|
87
|
+
/** Executed only in the employee DSH process; never creates a host fallback. */
|
|
88
|
+
export function createEmployeeTools(options) {
|
|
89
|
+
if (!isAbsolute(options.cwd) || !isAbsolute(options.shell) || !Number.isSafeInteger(options.uid) || options.uid !== process.getuid?.() || !Number.isSafeInteger(options.maxBytes) || options.maxBytes < 1 || !Number.isSafeInteger(options.killGraceMs) || options.killGraceMs < 0)
|
|
90
|
+
throw failure('EMPLOYEE_CONTEXT_INVALID');
|
|
91
|
+
const lifetime = new AbortController(), children = new Set(), calls = new Set();
|
|
92
|
+
const path = (value) => resolve(options.cwd, value);
|
|
93
|
+
const capacity = (value) => { if (Buffer.byteLength(value) > options.maxBytes)
|
|
94
|
+
throw failure('EMPLOYEE_OUTPUT_TOO_LARGE'); };
|
|
95
|
+
async function run(command, args, signal, detail, timeout) {
|
|
96
|
+
signal.throwIfAborted();
|
|
97
|
+
const operation = new Promise((yes, no) => {
|
|
98
|
+
const child = spawn(command, args, { cwd: options.cwd, env: process.env, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
99
|
+
child.stdin.end();
|
|
100
|
+
let bytes = 0, stdout = [], stderr = [], failed, killTimer, timeoutTimer;
|
|
101
|
+
const kill = (which) => { if (child.exitCode === null && child.signalCode === null)
|
|
102
|
+
child.kill(which); };
|
|
103
|
+
const stop = (code) => { if (failed)
|
|
104
|
+
return; failed = failure(code); kill('SIGTERM'); killTimer = setTimeout(() => kill('SIGKILL'), options.killGraceMs); killTimer.unref(); };
|
|
105
|
+
const aborted = () => stop('EMPLOYEE_CANCELLED_UNKNOWN');
|
|
106
|
+
const collect = (dest, chunk) => { bytes += chunk.length; if (bytes > options.maxBytes) {
|
|
107
|
+
stop('EMPLOYEE_OUTPUT_TOO_LARGE_UNKNOWN');
|
|
108
|
+
return;
|
|
109
|
+
} dest.push(chunk); };
|
|
110
|
+
child.stdout.on('data', (b) => collect(stdout, b));
|
|
111
|
+
child.stderr.on('data', (b) => collect(stderr, b));
|
|
112
|
+
child.on('spawn', () => { try {
|
|
113
|
+
detail({ kind: 'session', severity: 'info', code: 'CLAUDE_EMPLOYEE_PROCESS', summary: '本机工具子进程', payload: { childPid: child.pid ?? null, uid: options.uid, cwd: options.cwd } });
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
stop('CONVERSATION_DETAILS_FAILED');
|
|
117
|
+
} if (signal.aborted)
|
|
118
|
+
aborted(); });
|
|
119
|
+
child.on('error', e => { failed ??= failure(e.code === 'ENOENT' ? 'EMPLOYEE_COMMAND_UNAVAILABLE' : 'EMPLOYEE_PROCESS_FAILED'); });
|
|
120
|
+
child.on('close', (exitCode, exitSignal) => { signal.removeEventListener('abort', aborted); if (killTimer)
|
|
121
|
+
clearTimeout(killTimer); if (timeoutTimer)
|
|
122
|
+
clearTimeout(timeoutTimer); failed ? no(failed) : yes({ stdout: Buffer.concat(stdout).toString('utf8'), stderr: Buffer.concat(stderr).toString('utf8'), exitCode, signal: exitSignal }); });
|
|
123
|
+
signal.addEventListener('abort', aborted, { once: true });
|
|
124
|
+
if (signal.aborted)
|
|
125
|
+
aborted();
|
|
126
|
+
if (timeout !== undefined) {
|
|
127
|
+
timeoutTimer = setTimeout(() => stop('EMPLOYEE_TIMEOUT_UNKNOWN'), timeout);
|
|
128
|
+
timeoutTimer.unref();
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
children.add(operation);
|
|
132
|
+
try {
|
|
133
|
+
return await operation;
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
children.delete(operation);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async function execute(name, args, signal, detail) {
|
|
140
|
+
const check = () => signal.throwIfAborted();
|
|
141
|
+
if (name === 'Read') {
|
|
142
|
+
fields(args, ['file_path', 'offset', 'limit', 'pages']);
|
|
143
|
+
if (args.pages !== undefined)
|
|
144
|
+
throw failure('EMPLOYEE_READ_PDF_UNSUPPORTED');
|
|
145
|
+
const target = path(string(args, 'file_path')), offset = integer(args, 'offset', 1) ?? 1, limit = integer(args, 'limit', 1);
|
|
146
|
+
const info = await lstat(target);
|
|
147
|
+
if (info.size > options.maxBytes)
|
|
148
|
+
throw failure('EMPLOYEE_OUTPUT_TOO_LARGE');
|
|
149
|
+
const bytes = await readFile(target, { signal });
|
|
150
|
+
check();
|
|
151
|
+
capacity(bytes);
|
|
152
|
+
const magic = bytes.subarray(0, 16), mime = magic[0] === 137 && magic.subarray(1, 4).toString() === 'PNG' ? 'image/png' : magic[0] === 255 && magic[1] === 216 ? 'image/jpeg' : magic.subarray(0, 4).toString() === 'RIFF' && magic.subarray(8, 12).toString() === 'WEBP' ? 'image/webp' : magic.subarray(0, 3).toString() === 'GIF' ? 'image/gif' : undefined;
|
|
153
|
+
if (mime) {
|
|
154
|
+
if (args.offset !== undefined || args.limit !== undefined)
|
|
155
|
+
throw failure('EMPLOYEE_IMAGE_RANGE_UNSUPPORTED');
|
|
156
|
+
return { content: [{ type: 'image', mimeType: mime, data: bytes.toString('base64') }] };
|
|
157
|
+
}
|
|
158
|
+
let contents;
|
|
159
|
+
try {
|
|
160
|
+
contents = new TextDecoder('utf8', { fatal: true }).decode(bytes);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
throw failure('EMPLOYEE_READ_BINARY_UNSUPPORTED');
|
|
164
|
+
}
|
|
165
|
+
if (contents.includes('\0') || contents.startsWith('%PDF-'))
|
|
166
|
+
throw failure('EMPLOYEE_READ_BINARY_UNSUPPORTED');
|
|
167
|
+
const lines = contents.split('\n'), selected = lines.slice(offset - 1, limit === undefined ? undefined : offset - 1 + limit);
|
|
168
|
+
return text(selected.map((line, i) => String(offset + i) + '\t' + line).join('\n'));
|
|
169
|
+
}
|
|
170
|
+
if (name === 'Write') {
|
|
171
|
+
fields(args, ['file_path', 'content']);
|
|
172
|
+
const target = path(string(args, 'file_path')), content = string(args, 'content', false);
|
|
173
|
+
if (content === undefined)
|
|
174
|
+
throw failure('EMPLOYEE_ARGUMENT_INVALID');
|
|
175
|
+
capacity(content);
|
|
176
|
+
await mutate(target, signal, async () => { check(); await mkdir(dirname(target), { recursive: true }); check(); await writeFile(target, content, { signal }); });
|
|
177
|
+
return text('文件已写入:' + target);
|
|
178
|
+
}
|
|
179
|
+
if (name === 'Edit') {
|
|
180
|
+
fields(args, ['file_path', 'old_string', 'new_string', 'replace_all']);
|
|
181
|
+
const target = path(string(args, 'file_path')), old = string(args, 'old_string', false), replacement = string(args, 'new_string', false), all = boolean(args, 'replace_all') ?? false;
|
|
182
|
+
if (old === undefined || replacement === undefined || !old)
|
|
183
|
+
throw failure('EMPLOYEE_EDIT_EMPTY_MATCH_UNSUPPORTED');
|
|
184
|
+
return mutate(target, signal, async () => {
|
|
185
|
+
const bytes = await readFile(target, { signal });
|
|
186
|
+
capacity(bytes);
|
|
187
|
+
let content;
|
|
188
|
+
try {
|
|
189
|
+
content = new TextDecoder('utf8', { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
throw failure('EMPLOYEE_READ_BINARY_UNSUPPORTED');
|
|
193
|
+
}
|
|
194
|
+
const matches = content.split(old).length - 1;
|
|
195
|
+
if (!matches)
|
|
196
|
+
throw failure('EMPLOYEE_EDIT_NOT_FOUND');
|
|
197
|
+
if (matches > 1 && !all)
|
|
198
|
+
throw failure('EMPLOYEE_EDIT_AMBIGUOUS');
|
|
199
|
+
const next = all ? content.replaceAll(old, () => replacement) : content.replace(old, () => replacement);
|
|
200
|
+
capacity(next);
|
|
201
|
+
check();
|
|
202
|
+
await writeFile(target, next, { signal });
|
|
203
|
+
return text('文件已修改:' + target);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
if (name === 'Glob') {
|
|
207
|
+
fields(args, ['pattern', 'path']);
|
|
208
|
+
const pattern = string(args, 'pattern'), cwd = path(string(args, 'path', false) ?? '.'), found = [];
|
|
209
|
+
for await (const item of glob(pattern, { cwd })) {
|
|
210
|
+
check();
|
|
211
|
+
found.push(resolve(cwd, item));
|
|
212
|
+
capacity(JSON.stringify(found));
|
|
213
|
+
}
|
|
214
|
+
return text(JSON.stringify(found));
|
|
215
|
+
}
|
|
216
|
+
if (name === 'Grep') {
|
|
217
|
+
fields(args, ['pattern', 'path', 'glob', 'output_mode', '-B', '-A', '-C', '-n', '-i', 'type', 'head_limit', 'offset', 'multiline']);
|
|
218
|
+
const pattern = string(args, 'pattern'), mode = string(args, 'output_mode', false) ?? 'files_with_matches', argv = ['--hidden', '--pcre2', '--color', 'never'];
|
|
219
|
+
if (mode === 'files_with_matches')
|
|
220
|
+
argv.push('--files-with-matches');
|
|
221
|
+
else if (mode === 'count')
|
|
222
|
+
argv.push('--count');
|
|
223
|
+
else if (mode !== 'content')
|
|
224
|
+
throw failure('EMPLOYEE_ARGUMENT_UNSUPPORTED');
|
|
225
|
+
for (const flag of ['-B', '-A', '-C']) {
|
|
226
|
+
const value = integer(args, flag, 0);
|
|
227
|
+
if (value !== undefined)
|
|
228
|
+
argv.push(flag, String(value));
|
|
229
|
+
}
|
|
230
|
+
if (boolean(args, '-n'))
|
|
231
|
+
argv.push('--line-number');
|
|
232
|
+
if (boolean(args, '-i'))
|
|
233
|
+
argv.push('--ignore-case');
|
|
234
|
+
if (boolean(args, 'multiline'))
|
|
235
|
+
argv.push('--multiline', '--multiline-dotall');
|
|
236
|
+
for (const [key, flag] of [['glob', '--glob'], ['type', '--type']]) {
|
|
237
|
+
const v = string(args, key, false);
|
|
238
|
+
if (v !== undefined)
|
|
239
|
+
argv.push(flag, v);
|
|
240
|
+
}
|
|
241
|
+
argv.push('--', pattern, path(string(args, 'path', false) ?? '.'));
|
|
242
|
+
const result = await run(options.rgPath ?? await bundledRipgrep(), argv, signal, detail);
|
|
243
|
+
if (result.exitCode !== 0 && result.exitCode !== 1)
|
|
244
|
+
return { isError: true, content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
245
|
+
const offset = integer(args, 'offset', 0) ?? 0, limit = integer(args, 'head_limit', 0), lines = result.stdout.split('\n');
|
|
246
|
+
return text(lines.slice(offset, limit === undefined || limit === 0 ? undefined : offset + limit).join('\n'));
|
|
247
|
+
}
|
|
248
|
+
fields(args, ['command', 'timeout', 'description', 'run_in_background', 'dangerouslyDisableSandbox']);
|
|
249
|
+
if (boolean(args, 'run_in_background'))
|
|
250
|
+
throw failure('EMPLOYEE_BACKGROUND_UNSUPPORTED');
|
|
251
|
+
boolean(args, 'dangerouslyDisableSandbox');
|
|
252
|
+
string(args, 'description', false);
|
|
253
|
+
const timeout = integer(args, 'timeout', 1);
|
|
254
|
+
if (timeout !== undefined && timeout > 2147483647)
|
|
255
|
+
throw failure('EMPLOYEE_ARGUMENT_INVALID');
|
|
256
|
+
const command = string(args, 'command');
|
|
257
|
+
if (!options.runShell)
|
|
258
|
+
throw failure('EMPLOYEE_SHELL_ENDPOINT_UNAVAILABLE');
|
|
259
|
+
const result = await options.runShell({ command, signal, ...timeout === undefined ? {} : { timeout } });
|
|
260
|
+
return { ...text(JSON.stringify(result)), ...(result.exitCode !== 0 || result.signal ? { isError: true } : {}) };
|
|
261
|
+
}
|
|
262
|
+
async function call(name, input, requestSignal, observer = options.detail) {
|
|
263
|
+
const signal = AbortSignal.any([lifetime.signal, ...requestSignal ? [requestSignal] : []]);
|
|
264
|
+
signal.throwIfAborted();
|
|
265
|
+
const executionId = randomUUID();
|
|
266
|
+
let submitted = false;
|
|
267
|
+
const detail = (status, code) => observer({ kind: 'session', severity: status === 'completed' ? 'info' : 'warning', code: 'CLAUDE_EMPLOYEE_OPERATION', summary: 'Claude本机工具执行位置与结果', payload: { executionId, operation: name, status, ...code ? { reason: code } : {}, cwd: options.cwd, uid: options.uid, pid: process.pid } });
|
|
268
|
+
try {
|
|
269
|
+
if (!EMPLOYEE_TOOL_NAMES.includes(name))
|
|
270
|
+
throw failure('EMPLOYEE_TOOL_UNSUPPORTED');
|
|
271
|
+
await options.validate(signal);
|
|
272
|
+
signal.throwIfAborted();
|
|
273
|
+
submitted = true;
|
|
274
|
+
detail('started');
|
|
275
|
+
const result = await execute(name, object(input), signal, observer);
|
|
276
|
+
detail(result.isError ? 'failed' : 'completed');
|
|
277
|
+
await options.validate(signal);
|
|
278
|
+
signal.throwIfAborted();
|
|
279
|
+
capacity(JSON.stringify(result));
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
const code = error instanceof NeedError ? error.code : signal.aborted ? 'EMPLOYEE_CANCELLED_UNKNOWN' : typeof error?.code === 'string' ? 'EMPLOYEE_IO_' + error.code : 'EMPLOYEE_OPERATION_FAILED';
|
|
284
|
+
detail(submitted && signal.aborted ? 'unknown' : 'failed', code);
|
|
285
|
+
return { isError: true, content: [{ type: 'text', text: code + ':本机操作未完成;没有在主机代跑。' }] };
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
call(name, input, requestSignal, observer) { const operation = call(name, input, requestSignal, observer); calls.add(operation); void operation.then(() => calls.delete(operation), () => calls.delete(operation)); return operation; },
|
|
290
|
+
async close() { lifetime.abort(); await Promise.allSettled([...calls, ...children]); },
|
|
291
|
+
};
|
|
292
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { EmployeeWorkspace } from './employee-endpoint.ts';
|
|
2
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
3
|
+
interface Services {
|
|
4
|
+
get(name: string): unknown;
|
|
5
|
+
inject: Context['inject'];
|
|
6
|
+
}
|
|
7
|
+
/** Session headers do not have workspaceId. Resolve only the official registry relationship. */
|
|
8
|
+
export declare function readEmployeeWorkspace(ctx: Services, sessionId: string): Promise<EmployeeWorkspace>;
|
|
9
|
+
interface RegisteredWorkspace {
|
|
10
|
+
id: string;
|
|
11
|
+
path: string;
|
|
12
|
+
sessionIds: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
interface OfficialRegistry {
|
|
15
|
+
list(): readonly RegisteredWorkspace[];
|
|
16
|
+
get(id: string): RegisteredWorkspace | undefined;
|
|
17
|
+
}
|
|
18
|
+
/** Same official membership/header boundary as L. No basename or first-workspace inference. */
|
|
19
|
+
export declare function officialEmployeeWorkspace(sessionId: string, registry: OfficialRegistry | undefined, header: {
|
|
20
|
+
id: string;
|
|
21
|
+
cwd?: string;
|
|
22
|
+
} | undefined): {
|
|
23
|
+
id: string;
|
|
24
|
+
path: string;
|
|
25
|
+
};
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { lstat, realpath } from 'node:fs/promises';
|
|
2
|
+
import { isAbsolute } from 'node:path';
|
|
3
|
+
import { NeedError } from "./remote.js";
|
|
4
|
+
const invalid = () => new NeedError('EMPLOYEE_WORKSPACE_INVALID', '无法核对当前会话的本机文件夹,本轮未执行。', '官方工作区唯一会话关系与当前原生cwd');
|
|
5
|
+
/** Session headers do not have workspaceId. Resolve only the official registry relationship. */
|
|
6
|
+
export async function readEmployeeWorkspace(ctx, sessionId) {
|
|
7
|
+
if (typeof ctx.inject !== 'function')
|
|
8
|
+
throw invalid();
|
|
9
|
+
let active = false, seen = false, scoped;
|
|
10
|
+
const injection = ctx.inject(['workspaceRegistry', 'sessions'], current => { if (seen)
|
|
11
|
+
return; seen = true; active = true; scoped = current; return () => { active = false; }; });
|
|
12
|
+
try {
|
|
13
|
+
await injection;
|
|
14
|
+
if (!active || !scoped)
|
|
15
|
+
throw invalid();
|
|
16
|
+
return await readWorkspace(scoped, sessionId, () => { if (!active)
|
|
17
|
+
throw invalid(); });
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
await injection.dispose();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function readWorkspace(ctx, sessionId, fence) {
|
|
24
|
+
fence();
|
|
25
|
+
const registry = ctx.get('workspaceRegistry'), sessions = ctx.get('sessions');
|
|
26
|
+
if (!registry || typeof registry.list !== 'function' || typeof registry.get !== 'function' || !sessions || typeof sessions.get !== 'function' || !sessionId)
|
|
27
|
+
throw invalid();
|
|
28
|
+
const live = sessions.get(sessionId);
|
|
29
|
+
const current = () => {
|
|
30
|
+
fence();
|
|
31
|
+
const currentRegistry = ctx.get('workspaceRegistry');
|
|
32
|
+
// Service proxies are caller-scoped; injection owns the lifecycle fence.
|
|
33
|
+
if (!currentRegistry || ctx.get('sessions')?.get(sessionId) !== live)
|
|
34
|
+
throw invalid();
|
|
35
|
+
const matches = currentRegistry.list().filter(row => Array.isArray(row.sessionIds) && row.sessionIds.includes(sessionId));
|
|
36
|
+
if (matches.length !== 1)
|
|
37
|
+
throw invalid();
|
|
38
|
+
const row = matches[0], entry = currentRegistry.get(row.id), header = sessions.get(sessionId)?.header;
|
|
39
|
+
if (!entry || row.id !== entry.id || row.path !== entry.path || !entry.sessionIds.includes(sessionId) || !isAbsolute(row.path) || !header || header.id !== sessionId || !isAbsolute(header.cwd))
|
|
40
|
+
throw invalid();
|
|
41
|
+
return { workspaceId: row.id, path: row.path, cwd: header.cwd };
|
|
42
|
+
};
|
|
43
|
+
const first = current(), path = await realpath(first.path), cwd = await realpath(first.cwd), info = await lstat(path);
|
|
44
|
+
const last = current();
|
|
45
|
+
if (JSON.stringify(last) !== JSON.stringify(first) || path !== cwd || !info.isDirectory() || info.isSymbolicLink())
|
|
46
|
+
throw invalid();
|
|
47
|
+
return { workspaceId: first.workspaceId, directory: { path, device: info.dev, inode: info.ino } };
|
|
48
|
+
}
|
|
49
|
+
/** Same official membership/header boundary as L. No basename or first-workspace inference. */
|
|
50
|
+
export function officialEmployeeWorkspace(sessionId, registry, header) {
|
|
51
|
+
const fail = () => new NeedError('EMPLOYEE_WORKSPACE_UNAVAILABLE', '无法确认这次会话的本机工作区,本轮没有执行;请从原生工作区打开会话。', '官方workspaceRegistry成员关系与会话header.cwd');
|
|
52
|
+
if (!sessionId || !registry || !header || header.id !== sessionId || typeof header.cwd !== 'string')
|
|
53
|
+
throw fail();
|
|
54
|
+
const matches = registry.list().filter(w => Array.isArray(w.sessionIds) && w.sessionIds.includes(sessionId));
|
|
55
|
+
if (matches.length !== 1)
|
|
56
|
+
throw fail();
|
|
57
|
+
const item = matches[0], current = registry.get(item.id);
|
|
58
|
+
if (!current || current.id !== item.id || current.path !== item.path || !current.sessionIds.includes(sessionId) || !item.path.startsWith('/') || item.path.includes('\0') || header.cwd !== item.path)
|
|
59
|
+
throw fail();
|
|
60
|
+
return { id: item.id, path: item.path };
|
|
61
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export declare const DEFAULT_CONFIG: Readonly<{
|
|
|
18
18
|
readonly brokerControlMaxBytes: 1048576;
|
|
19
19
|
readonly oneShotSandbox: "workspace-write";
|
|
20
20
|
readonly oneShotApproval: "never";
|
|
21
|
+
readonly employeeShellKillGraceMs: 1000;
|
|
21
22
|
}>;
|
|
22
23
|
export declare const Config: Schema<Schemastery.ObjectS<NoInfer<{
|
|
23
24
|
mode: Schema<"session" | "one-shot", "session" | "one-shot", "volatile-defined">;
|
|
@@ -26,6 +27,7 @@ export declare const Config: Schema<Schemastery.ObjectS<NoInfer<{
|
|
|
26
27
|
brokerControlMaxBytes: Schema<number, number, "volatile-defined">;
|
|
27
28
|
oneShotSandbox: Schema<"read-only" | "workspace-write" | "danger-full-access", "read-only" | "workspace-write" | "danger-full-access", "volatile-defined">;
|
|
28
29
|
oneShotApproval: Schema<"never", "never", "volatile-defined">;
|
|
30
|
+
employeeShellKillGraceMs: Schema<number, number, "volatile-defined">;
|
|
29
31
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
30
32
|
mode: Schema<"session" | "one-shot", "session" | "one-shot", "volatile-defined">;
|
|
31
33
|
acpMaxMessageBytes: Schema<number, number, "volatile-defined">;
|
|
@@ -33,6 +35,7 @@ export declare const Config: Schema<Schemastery.ObjectS<NoInfer<{
|
|
|
33
35
|
brokerControlMaxBytes: Schema<number, number, "volatile-defined">;
|
|
34
36
|
oneShotSandbox: Schema<"read-only" | "workspace-write" | "danger-full-access", "read-only" | "workspace-write" | "danger-full-access", "volatile-defined">;
|
|
35
37
|
oneShotApproval: Schema<"never", "never", "volatile-defined">;
|
|
38
|
+
employeeShellKillGraceMs: Schema<number, number, "volatile-defined">;
|
|
36
39
|
}>>, "plain">;
|
|
37
40
|
export interface PluginConfig {
|
|
38
41
|
mode?: 'session' | 'one-shot';
|
|
@@ -41,6 +44,7 @@ export interface PluginConfig {
|
|
|
41
44
|
brokerControlMaxBytes?: number;
|
|
42
45
|
oneShotSandbox?: 'read-only' | 'workspace-write' | 'danger-full-access';
|
|
43
46
|
oneShotApproval?: 'never';
|
|
47
|
+
employeeShellKillGraceMs?: number;
|
|
44
48
|
}
|
|
45
49
|
type LiveConfig = {
|
|
46
50
|
[K in keyof PluginConfig]: PluginConfig[K] | {
|
|
@@ -81,6 +85,7 @@ export declare class CodexRemoteAdapter extends LlmAdapter {
|
|
|
81
85
|
catalog: HostModel;
|
|
82
86
|
}>;
|
|
83
87
|
invalidateModelState(): Promise<void>;
|
|
88
|
+
invalidateEmployeeState(): Promise<void>;
|
|
84
89
|
providerRetryPolicy(): {
|
|
85
90
|
mode: "normal";
|
|
86
91
|
maxRetries: number;
|
|
@@ -112,6 +117,7 @@ export declare class RemoteAgentsAdapter extends LlmAdapter {
|
|
|
112
117
|
catalog: HostModel;
|
|
113
118
|
}>;
|
|
114
119
|
invalidateModelState(): Promise<void>;
|
|
120
|
+
invalidateEmployeeState(): Promise<void>;
|
|
115
121
|
providerRetryPolicy(): {
|
|
116
122
|
mode: "normal";
|
|
117
123
|
maxRetries: number;
|