@ai-workbench/remote-agent 0.8.2 → 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 +42 -7
- package/dist/acp.d.ts +46 -1
- package/dist/acp.js +296 -36
- package/dist/approval-label.d.ts +6 -0
- package/dist/approval-label.js +57 -0
- package/dist/broker-runtime.d.ts +10 -0
- package/dist/broker-runtime.js +334 -0
- package/dist/broker.d.ts +29 -6
- package/dist/broker.js +146 -142
- 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 +1 -1
- package/dist/conversation-details.js +88 -8
- 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 +31 -14
- package/dist/index.js +335 -42
- package/dist/input.d.ts +15 -0
- package/dist/input.js +65 -24
- package/dist/local-user-turn.d.ts +24 -0
- package/dist/local-user-turn.js +103 -0
- package/dist/model-catalog.d.ts +55 -0
- package/dist/model-catalog.js +254 -0
- package/dist/model-configuration.d.ts +51 -0
- package/dist/model-configuration.js +212 -0
- package/dist/presentation.d.ts +1 -1
- package/dist/presentation.js +50 -2
- package/dist/protocol-boundary.d.ts +13 -0
- package/dist/protocol-boundary.js +49 -0
- package/dist/provider.d.ts +54 -2
- package/dist/provider.js +92 -29
- package/dist/readiness.js +1 -1
- package/dist/recovery-checkpoint.d.ts +6 -0
- package/dist/recovery-checkpoint.js +71 -0
- package/dist/recovery.d.ts +47 -0
- package/dist/recovery.js +61 -0
- package/dist/remote-details.d.ts +10 -0
- package/dist/remote-details.js +14 -4
- package/dist/rpc-namespace.d.ts +3 -0
- package/dist/rpc-namespace.js +50 -0
- package/dist/stage-timing.d.ts +27 -0
- package/dist/stage-timing.js +39 -0
- 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 +30 -12
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { lstat, realpath } from 'node:fs/promises';
|
|
2
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
3
|
+
import { sameTarget, unwrap } from "./provider.js";
|
|
4
|
+
import { NeedError, shellQuote } from "./remote.js";
|
|
5
|
+
const fail = (code = 'EMPLOYEE_CONTEXT_CHANGED') => new NeedError(code, '本机工作区或连接已改变,原工具已停止;没有改在主机执行。', '当前L本地绑定与N/S同代员工端点');
|
|
6
|
+
const uuid = (value) => typeof value === 'string' && /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/.test(value);
|
|
7
|
+
function clean(value) { return typeof value === 'string' && !!value && !/[\x00-\x1f\x7f]/.test(value); }
|
|
8
|
+
export function endpointArguments(target) {
|
|
9
|
+
return ['--settings', target.bridge.settingsPath, '--device', target.endpoint.deviceId, '--if-instance', target.bridge.bridgeInstanceId, '--if-generation', String(target.bridge.generation), '--if-endpoint', target.endpoint.endpointInstanceId, '--if-context', target.endpoint.executionContextId];
|
|
10
|
+
}
|
|
11
|
+
export function endpointCommand(target) { return [target.bridge.command, 'target', ...endpointArguments(target)].map(shellQuote).join(' '); }
|
|
12
|
+
export function verifyEmployeeTarget(target, request, directory, host) {
|
|
13
|
+
if (target?.version !== 1 || !target.local || !target.endpoint || !target.bridge || target.local.instanceId !== request.instanceId || target.local.epoch !== request.epoch || target.local.workspaceId !== request.workspaceId || target.local.bindingId !== request.expectedBindingId || !isDeepStrictEqual(target.local.directory, directory))
|
|
14
|
+
throw fail();
|
|
15
|
+
const e = target.endpoint, b = target.bridge;
|
|
16
|
+
if (!/^[a-f0-9]{64}$/.test(e.deviceId) || ![e.endpointInstanceId, e.executionContextId, b.bridgeInstanceId].every(uuid) || !Number.isSafeInteger(b.generation) || b.generation < 1 || b.protocolVersion !== 1 || b.command !== '/usr/local/aw-host/bin/aw-employee-bridge' || b.settingsPath !== host.session.settingsPath || ![e.remoteUser, e.remoteHome, e.endpointCwd, e.endpointShell].every(clean) || !e.remoteHome.startsWith('/') || !e.endpointCwd.startsWith('/') || !e.endpointShell.startsWith('/') || e.remoteUid !== process.getuid?.() || process.getuid?.() !== process.geteuid?.() || e.remoteUid < 1)
|
|
17
|
+
throw fail('EMPLOYEE_TARGET_INVALID');
|
|
18
|
+
}
|
|
19
|
+
export function verifyBridgeTarget(value, expected) {
|
|
20
|
+
const raw = value;
|
|
21
|
+
if (!raw || raw.type !== 'target' || raw.protocolVersion !== 1 || !raw.endpoint || raw.endpoint.state !== 'online' || !Array.isArray(raw.sshArgs) || !raw.sshArgs.length || raw.sshArgs.some(x => typeof x !== 'string' || x.includes('\0')))
|
|
22
|
+
throw fail('EMPLOYEE_BRIDGE_PROTOCOL_INVALID');
|
|
23
|
+
const endpoint = raw.endpoint;
|
|
24
|
+
for (const [field, value] of Object.entries({ ...expected.endpoint, bridgeInstanceId: expected.bridge.bridgeInstanceId, generation: expected.bridge.generation })) {
|
|
25
|
+
// N's shell/cwd are confirmed by ready; all public identity and execution facts must match.
|
|
26
|
+
if (endpoint[field] !== value)
|
|
27
|
+
throw fail();
|
|
28
|
+
}
|
|
29
|
+
return { sshArgs: raw.sshArgs };
|
|
30
|
+
}
|
|
31
|
+
export async function openEmployeeLease(options, initialSignal) {
|
|
32
|
+
initialSignal?.throwIfAborted();
|
|
33
|
+
const api = options.getConnect(), network = options.getNetwork(), lifetime = new AbortController(), offs = [];
|
|
34
|
+
if (!api || api.getContractInfo().localWorkspaceContext !== 1 || api.getContractInfo().workspaceLocations !== 1 || !api.getLocalWorkspaceContext || !api.getWorkspaceLocationState || !api.onWorkspaceLocationState)
|
|
35
|
+
throw fail('EMPLOYEE_CONTEXT_UNAVAILABLE');
|
|
36
|
+
if (!network || typeof network.prepareEmployeeEndpoint !== 'function' || typeof network.onState !== 'function')
|
|
37
|
+
throw fail('EMPLOYEE_ENDPOINT_UNAVAILABLE');
|
|
38
|
+
const workspace = await options.readWorkspace(), connection = api.getConnectionState(), state = api.getWorkspaceLocationState();
|
|
39
|
+
const items = state.items.filter(x => x.workspaceId === workspace.workspaceId);
|
|
40
|
+
if (connection.phase !== 'connected' || state.phase !== 'ready' || state.instanceId !== connection.instanceId || state.epoch !== connection.epoch || items.length !== 1 || items[0].source !== 'local' || items[0].status !== 'selected' || !items[0].bindingId)
|
|
41
|
+
throw fail();
|
|
42
|
+
const request = { instanceId: connection.instanceId, epoch: connection.epoch, workspaceId: workspace.workspaceId, expectedBindingId: items[0].bindingId };
|
|
43
|
+
let target, closed = false, diagnosticFailure;
|
|
44
|
+
const invalidate = (code) => { if (lifetime.signal.aborted)
|
|
45
|
+
return; try {
|
|
46
|
+
options.detail({ kind: 'session', severity: 'warning', code: 'CLAUDE_EMPLOYEE_REVOKED', summary: '本机旧绑定已停止派发,在途结果按实际确认;不会重放', payload: { reason: code, workspaceId: request.workspaceId, bindingId: request.expectedBindingId } });
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
diagnosticFailure ??= error;
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
lifetime.abort(diagnosticFailure ?? fail(code));
|
|
53
|
+
} };
|
|
54
|
+
const current = () => {
|
|
55
|
+
if (options.getConnect() !== api || options.getNetwork() !== network)
|
|
56
|
+
throw fail();
|
|
57
|
+
const c = api.getConnectionState(), s = api.getWorkspaceLocationState(), matches = s.items.filter(x => x.workspaceId === request.workspaceId);
|
|
58
|
+
if (c.phase !== 'connected' || c.instanceId !== request.instanceId || c.epoch !== request.epoch || s.phase !== 'ready' || s.instanceId !== request.instanceId || s.epoch !== request.epoch || matches.length !== 1 || matches[0].source !== 'local' || matches[0].status !== 'selected' || matches[0].bindingId !== request.expectedBindingId || network.getState().phase !== 'online')
|
|
59
|
+
throw fail();
|
|
60
|
+
lifetime.signal.throwIfAborted();
|
|
61
|
+
};
|
|
62
|
+
const changed = () => { try {
|
|
63
|
+
current();
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
invalidate('EMPLOYEE_CONTEXT_CHANGED');
|
|
67
|
+
} };
|
|
68
|
+
offs.push(api.onConnectionState(changed), api.onWorkspaceLocationState(changed), network.onState(event => { if (event?.kind === 'lost' || network.getState().phase !== 'online')
|
|
69
|
+
invalidate('EMPLOYEE_ENDPOINT_UNAVAILABLE'); }));
|
|
70
|
+
async function validate(callerSignal) {
|
|
71
|
+
if (diagnosticFailure)
|
|
72
|
+
throw diagnosticFailure;
|
|
73
|
+
callerSignal?.throwIfAborted();
|
|
74
|
+
const signal = AbortSignal.any([lifetime.signal, ...callerSignal ? [callerSignal] : []]);
|
|
75
|
+
try {
|
|
76
|
+
current();
|
|
77
|
+
const now = await options.readWorkspace();
|
|
78
|
+
signal.throwIfAborted();
|
|
79
|
+
current();
|
|
80
|
+
const stat = await lstat(workspace.directory.path), canonical = await realpath(workspace.directory.path);
|
|
81
|
+
if (!isDeepStrictEqual(now, workspace) || !stat.isDirectory() || stat.isSymbolicLink() || canonical !== workspace.directory.path || stat.dev !== workspace.directory.device || stat.ino !== workspace.directory.inode)
|
|
82
|
+
throw fail();
|
|
83
|
+
const context = unwrap(await api.getLocalWorkspaceContext(request));
|
|
84
|
+
signal.throwIfAborted();
|
|
85
|
+
current();
|
|
86
|
+
if (context.version !== 1 || context.source !== 'local' || context.instanceId !== request.instanceId || context.epoch !== request.epoch || context.workspaceId !== request.workspaceId || context.bindingId !== request.expectedBindingId || !isDeepStrictEqual(context.directory, workspace.directory))
|
|
87
|
+
throw fail();
|
|
88
|
+
const prepared = await network.prepareEmployeeEndpoint({ ...request }, signal);
|
|
89
|
+
signal.throwIfAborted();
|
|
90
|
+
current();
|
|
91
|
+
if (!prepared.ok)
|
|
92
|
+
throw fail(/^[A-Z][A-Z0-9_]*$/.test(prepared.error.code) ? prepared.error.code : 'EMPLOYEE_ENDPOINT_UNAVAILABLE');
|
|
93
|
+
const next = structuredClone(prepared.value);
|
|
94
|
+
verifyEmployeeTarget(next, request, workspace.directory, options.host);
|
|
95
|
+
if (target && !isDeepStrictEqual(next, target))
|
|
96
|
+
throw fail();
|
|
97
|
+
const host = unwrap(await api.getSshTarget());
|
|
98
|
+
if (!sameTarget(host, options.host))
|
|
99
|
+
throw fail();
|
|
100
|
+
const raw = await options.control(host, endpointCommand(next), signal);
|
|
101
|
+
signal.throwIfAborted();
|
|
102
|
+
current();
|
|
103
|
+
let result;
|
|
104
|
+
try {
|
|
105
|
+
result = JSON.parse(raw);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
throw fail('EMPLOYEE_BRIDGE_PROTOCOL_INVALID');
|
|
109
|
+
}
|
|
110
|
+
verifyBridgeTarget(result, next);
|
|
111
|
+
const final = await lstat(workspace.directory.path);
|
|
112
|
+
signal.throwIfAborted();
|
|
113
|
+
current();
|
|
114
|
+
if (final.dev !== workspace.directory.device || final.ino !== workspace.directory.inode)
|
|
115
|
+
throw fail();
|
|
116
|
+
target ??= next;
|
|
117
|
+
return structuredClone(target);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
if (!callerSignal?.aborted)
|
|
121
|
+
invalidate(error instanceof NeedError ? error.code : 'EMPLOYEE_CONTEXT_CHANGED');
|
|
122
|
+
throw error instanceof NeedError || callerSignal?.aborted ? error : fail();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
await validate(initialSignal);
|
|
127
|
+
return { target: structuredClone(target), request: Object.freeze({ ...request }), signal: lifetime.signal, validate, close: async () => { if (!closed) {
|
|
128
|
+
closed = true;
|
|
129
|
+
lifetime.abort(fail('EMPLOYEE_EXECUTOR_CLOSED'));
|
|
130
|
+
for (const off of offs)
|
|
131
|
+
off();
|
|
132
|
+
} if (diagnosticFailure)
|
|
133
|
+
throw diagnosticFailure; } };
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
for (const off of offs)
|
|
137
|
+
off();
|
|
138
|
+
lifetime.abort();
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ChildProcessWithoutNullStreams } from 'node:child_process';
|
|
2
|
+
import { NeedError } from './remote.ts';
|
|
3
|
+
export type EmployeeMessage = Record<string, unknown>;
|
|
4
|
+
export interface EmployeeExecutionTransport {
|
|
5
|
+
write(message: EmployeeMessage, signal?: AbortSignal): Promise<void>;
|
|
6
|
+
read(signal?: AbortSignal): Promise<EmployeeMessage | null>;
|
|
7
|
+
/** Closing a carrier never proves that an employee command completed. */
|
|
8
|
+
close(): void | Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
export interface EmployeeExecutionOptions {
|
|
11
|
+
bindingId: string;
|
|
12
|
+
acpMaxMessageBytes: number;
|
|
13
|
+
/** Rechecks the current L CAS/directory/account and N/S five-tuple. No cached permit. */
|
|
14
|
+
validate(signal: AbortSignal): Promise<void>;
|
|
15
|
+
/** Once allocation starts, settle only after returning its executor or confirming aborted cleanup. */
|
|
16
|
+
transport(signal: AbortSignal): Promise<EmployeeExecutionTransport>;
|
|
17
|
+
}
|
|
18
|
+
export interface EmployeeExecutionResult {
|
|
19
|
+
version: 1;
|
|
20
|
+
executionId?: string;
|
|
21
|
+
messages?: EmployeeMessage[];
|
|
22
|
+
closed?: boolean;
|
|
23
|
+
result?: 'UNKNOWN';
|
|
24
|
+
}
|
|
25
|
+
/** One immutable workspace binding owns one executor. UNKNOWN is terminal, never replayed. */
|
|
26
|
+
export declare class EmployeeExecution {
|
|
27
|
+
private readonly options;
|
|
28
|
+
private readonly lifetime;
|
|
29
|
+
private executionId?;
|
|
30
|
+
private io?;
|
|
31
|
+
private opening;
|
|
32
|
+
private ended;
|
|
33
|
+
private error?;
|
|
34
|
+
private pendingRead;
|
|
35
|
+
private writeTail;
|
|
36
|
+
private queuedWriteBytes;
|
|
37
|
+
private closing;
|
|
38
|
+
private factoryReceipt;
|
|
39
|
+
constructor(options: EmployeeExecutionOptions);
|
|
40
|
+
private parse;
|
|
41
|
+
private active;
|
|
42
|
+
private gate;
|
|
43
|
+
private stop;
|
|
44
|
+
private closeTransport;
|
|
45
|
+
invalidate(error?: NeedError): void;
|
|
46
|
+
private cleanup;
|
|
47
|
+
dispose(): Promise<void>;
|
|
48
|
+
handle(params: unknown, signal?: AbortSignal): Promise<EmployeeExecutionResult>;
|
|
49
|
+
private open;
|
|
50
|
+
private read;
|
|
51
|
+
}
|
|
52
|
+
/** NDJSON carrier with byte bounds and backpressure derived solely from acpMaxMessageBytes. */
|
|
53
|
+
export declare function createEmployeeProcessTransport(child: ChildProcessWithoutNullStreams, acpMaxMessageBytes: number, stderrCodes?: readonly string[]): EmployeeExecutionTransport;
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { NeedError } from "./remote.js";
|
|
3
|
+
const object = (value) => !!value && typeof value === 'object' && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
4
|
+
const need = (code) => new NeedError(code, code === 'ABORTED' ? '员工执行等待已取消,命令结果需另行确认' : '员工执行通道无法继续(' + code + '),未重放请求', '当前员工工作区绑定、N/S端点与实际命令状态');
|
|
5
|
+
const failure = (error) => error instanceof NeedError ? error : need('EMPLOYEE_EXEC_UNKNOWN');
|
|
6
|
+
function capacity(value) { if (!Number.isSafeInteger(value) || value < 1)
|
|
7
|
+
throw new NeedError('SETTINGS_REQUIRED', '我需要有效的 acpMaxMessageBytes', '管理员确认 acpMaxMessageBytes'); }
|
|
8
|
+
function encoded(message, maxBytes) {
|
|
9
|
+
if (!object(message))
|
|
10
|
+
throw need('EMPLOYEE_EXEC_PROTOCOL_INVALID');
|
|
11
|
+
let json;
|
|
12
|
+
try {
|
|
13
|
+
json = JSON.stringify(message);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
throw need('EMPLOYEE_EXEC_PROTOCOL_INVALID');
|
|
17
|
+
}
|
|
18
|
+
if (Buffer.byteLength(json) > maxBytes)
|
|
19
|
+
throw need('ACP_MESSAGE_TOO_LARGE');
|
|
20
|
+
// Freeze this operation's wire value before any async gate can yield.
|
|
21
|
+
const value = JSON.parse(json);
|
|
22
|
+
if (!object(value))
|
|
23
|
+
throw need('EMPLOYEE_EXEC_PROTOCOL_INVALID');
|
|
24
|
+
return { message: value, line: Buffer.from(json + '\n') };
|
|
25
|
+
}
|
|
26
|
+
function cancelled(signal) { if (signal?.aborted)
|
|
27
|
+
throw need('ABORTED'); }
|
|
28
|
+
function waiting(operation, signal) {
|
|
29
|
+
if (signal.aborted) {
|
|
30
|
+
void operation.catch(() => { });
|
|
31
|
+
return Promise.reject(need('ABORTED'));
|
|
32
|
+
}
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const abort = () => { signal.removeEventListener('abort', abort); reject(need('ABORTED')); };
|
|
35
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
36
|
+
void operation.then(value => { signal.removeEventListener('abort', abort); resolve(value); }, error => { signal.removeEventListener('abort', abort); reject(error); });
|
|
37
|
+
if (signal.aborted)
|
|
38
|
+
abort();
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/** One immutable workspace binding owns one executor. UNKNOWN is terminal, never replayed. */
|
|
42
|
+
export class EmployeeExecution {
|
|
43
|
+
options;
|
|
44
|
+
lifetime = new AbortController();
|
|
45
|
+
executionId;
|
|
46
|
+
io;
|
|
47
|
+
opening = false;
|
|
48
|
+
ended = false;
|
|
49
|
+
error;
|
|
50
|
+
pendingRead = false;
|
|
51
|
+
writeTail = Promise.resolve();
|
|
52
|
+
queuedWriteBytes = 0;
|
|
53
|
+
closing = Promise.resolve();
|
|
54
|
+
factoryReceipt = Promise.resolve();
|
|
55
|
+
constructor(options) {
|
|
56
|
+
capacity(options.acpMaxMessageBytes);
|
|
57
|
+
if (typeof options.bindingId !== 'string' || !options.bindingId)
|
|
58
|
+
throw need('EMPLOYEE_EXEC_IDENTITY_MISMATCH');
|
|
59
|
+
this.options = options;
|
|
60
|
+
}
|
|
61
|
+
parse(params) {
|
|
62
|
+
if (!object(params) || Object.keys(params).some(key => !['version', 'bindingId', 'operation', 'executionId', 'message'].includes(key)) || params.version !== 1 || typeof params.bindingId !== 'string' || !['open', 'write', 'read', 'close'].includes(params.operation))
|
|
63
|
+
throw need('EMPLOYEE_EXEC_PROTOCOL_INVALID');
|
|
64
|
+
if (params.bindingId !== this.options.bindingId)
|
|
65
|
+
throw need('EMPLOYEE_EXEC_IDENTITY_MISMATCH');
|
|
66
|
+
if (params.operation === 'open') {
|
|
67
|
+
if (Object.hasOwn(params, 'executionId') || Object.hasOwn(params, 'message'))
|
|
68
|
+
throw need('EMPLOYEE_EXEC_PROTOCOL_INVALID');
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
if (typeof params.executionId !== 'string' || !params.executionId)
|
|
72
|
+
throw need('EMPLOYEE_EXEC_PROTOCOL_INVALID');
|
|
73
|
+
if (params.executionId !== this.executionId)
|
|
74
|
+
throw need('EMPLOYEE_EXEC_IDENTITY_MISMATCH');
|
|
75
|
+
if (params.operation === 'write' ? !object(params.message) : Object.hasOwn(params, 'message'))
|
|
76
|
+
throw need('EMPLOYEE_EXEC_PROTOCOL_INVALID');
|
|
77
|
+
}
|
|
78
|
+
return params;
|
|
79
|
+
}
|
|
80
|
+
active() { if (this.ended)
|
|
81
|
+
throw this.error ?? need('EMPLOYEE_EXEC_CLOSED'); }
|
|
82
|
+
async gate(signal) { this.active(); cancelled(signal); await waiting(this.options.validate(signal), signal); this.active(); cancelled(signal); }
|
|
83
|
+
stop(error) {
|
|
84
|
+
if (this.ended)
|
|
85
|
+
return;
|
|
86
|
+
this.ended = true;
|
|
87
|
+
this.error = error;
|
|
88
|
+
this.lifetime.abort();
|
|
89
|
+
const io = this.io;
|
|
90
|
+
this.io = undefined;
|
|
91
|
+
if (io)
|
|
92
|
+
this.closeTransport(io);
|
|
93
|
+
}
|
|
94
|
+
closeTransport(io) {
|
|
95
|
+
let closing;
|
|
96
|
+
try {
|
|
97
|
+
closing = Promise.resolve(io.close());
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
closing = Promise.reject(need('EMPLOYEE_EXEC_UNKNOWN'));
|
|
101
|
+
}
|
|
102
|
+
this.closing = Promise.all([this.closing, closing]).then(() => { }, () => { const unknown = need('EMPLOYEE_EXEC_UNKNOWN'); this.error ??= unknown; throw unknown; });
|
|
103
|
+
// Revocation starts cleanup without a caller waiting yet; retain rejection for close/dispose.
|
|
104
|
+
void this.closing.catch(() => { });
|
|
105
|
+
}
|
|
106
|
+
invalidate(error = need('EMPLOYEE_CONTEXT_CHANGED')) { this.stop(error); }
|
|
107
|
+
async cleanup() { await this.factoryReceipt; await this.closing; }
|
|
108
|
+
async dispose() { this.stop(); await this.cleanup(); }
|
|
109
|
+
async handle(params, signal) {
|
|
110
|
+
const request = this.parse(params);
|
|
111
|
+
cancelled(signal);
|
|
112
|
+
if (request.operation === 'close') {
|
|
113
|
+
this.stop();
|
|
114
|
+
await this.cleanup();
|
|
115
|
+
return { version: 1, closed: true, result: 'UNKNOWN' };
|
|
116
|
+
}
|
|
117
|
+
this.active();
|
|
118
|
+
const current = signal ? AbortSignal.any([signal, this.lifetime.signal]) : this.lifetime.signal;
|
|
119
|
+
if (request.operation === 'open')
|
|
120
|
+
return this.open(current);
|
|
121
|
+
if (request.operation === 'read')
|
|
122
|
+
return this.read(current);
|
|
123
|
+
const value = encoded(request.message, this.options.acpMaxMessageBytes);
|
|
124
|
+
const size = value.line.byteLength;
|
|
125
|
+
if (this.queuedWriteBytes + size > this.options.acpMaxMessageBytes) {
|
|
126
|
+
const error = need('ACP_MESSAGE_TOO_LARGE');
|
|
127
|
+
this.stop(error);
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
this.queuedWriteBytes += size;
|
|
131
|
+
const operation = this.writeTail.then(async () => {
|
|
132
|
+
try {
|
|
133
|
+
await this.gate(current);
|
|
134
|
+
const io = this.io;
|
|
135
|
+
if (!io)
|
|
136
|
+
throw need('EMPLOYEE_EXEC_CLOSED');
|
|
137
|
+
await waiting(io.write(value.message, current), current);
|
|
138
|
+
await this.gate(current);
|
|
139
|
+
return { version: 1 };
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
const known = this.error ?? failure(error);
|
|
143
|
+
this.stop(known);
|
|
144
|
+
throw known;
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
this.queuedWriteBytes -= size;
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
this.writeTail = operation.catch(() => { });
|
|
151
|
+
return operation;
|
|
152
|
+
}
|
|
153
|
+
async open(signal) {
|
|
154
|
+
if (this.opening || this.executionId)
|
|
155
|
+
throw need('EMPLOYEE_EXEC_ALREADY_OPEN');
|
|
156
|
+
this.opening = true;
|
|
157
|
+
try {
|
|
158
|
+
await this.gate(signal);
|
|
159
|
+
// A factory may finish after revoke/abort. Its late executor belongs to this binding and must close.
|
|
160
|
+
const created = Promise.resolve().then(() => { cancelled(signal); return this.options.transport(signal); }).then(async (io) => { if (this.ended || signal.aborted) {
|
|
161
|
+
this.closeTransport(io);
|
|
162
|
+
await this.closing;
|
|
163
|
+
throw this.error ?? need('ABORTED');
|
|
164
|
+
} this.io = io; return io; });
|
|
165
|
+
// Open cancellation may return immediately; the runtime still owns the full allocation receipt.
|
|
166
|
+
this.factoryReceipt = created.then(() => { }, error => { if (failure(error).code === 'EMPLOYEE_EXEC_UNKNOWN') {
|
|
167
|
+
const unknown = need('EMPLOYEE_EXEC_UNKNOWN');
|
|
168
|
+
this.error ??= unknown;
|
|
169
|
+
throw unknown;
|
|
170
|
+
} });
|
|
171
|
+
void this.factoryReceipt.catch(() => { });
|
|
172
|
+
await waiting(created, signal);
|
|
173
|
+
await this.gate(signal);
|
|
174
|
+
this.executionId = randomUUID();
|
|
175
|
+
return { version: 1, executionId: this.executionId };
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
const known = this.error ?? failure(error);
|
|
179
|
+
this.stop(known);
|
|
180
|
+
throw known;
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
this.opening = false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async read(signal) {
|
|
187
|
+
if (this.pendingRead)
|
|
188
|
+
throw need('EMPLOYEE_EXEC_READ_BUSY');
|
|
189
|
+
this.pendingRead = true;
|
|
190
|
+
let consumed = false;
|
|
191
|
+
try {
|
|
192
|
+
await this.gate(signal);
|
|
193
|
+
const io = this.io;
|
|
194
|
+
if (!io)
|
|
195
|
+
throw need('EMPLOYEE_EXEC_CLOSED');
|
|
196
|
+
const message = await waiting(io.read(signal).then(message => { consumed = message !== null; return message; }), signal);
|
|
197
|
+
if (this.ended) {
|
|
198
|
+
if (this.error)
|
|
199
|
+
throw this.error;
|
|
200
|
+
return { version: 1, messages: [], closed: true };
|
|
201
|
+
}
|
|
202
|
+
await this.gate(signal);
|
|
203
|
+
if (message === null) {
|
|
204
|
+
this.stop();
|
|
205
|
+
return { version: 1, messages: [], closed: true };
|
|
206
|
+
}
|
|
207
|
+
const value = encoded(message, this.options.acpMaxMessageBytes);
|
|
208
|
+
if (Buffer.byteLength(JSON.stringify({ version: 1, messages: [value.message], closed: false })) > this.options.acpMaxMessageBytes)
|
|
209
|
+
throw need('ACP_MESSAGE_TOO_LARGE');
|
|
210
|
+
return { version: 1, messages: [value.message], closed: false };
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
if (this.error)
|
|
214
|
+
throw this.error;
|
|
215
|
+
if (signal.aborted && !this.ended) {
|
|
216
|
+
if (consumed) {
|
|
217
|
+
const unknown = need('EMPLOYEE_EXEC_UNKNOWN');
|
|
218
|
+
this.stop(unknown);
|
|
219
|
+
throw unknown;
|
|
220
|
+
}
|
|
221
|
+
throw need('ABORTED');
|
|
222
|
+
}
|
|
223
|
+
if (this.ended && !this.error)
|
|
224
|
+
return { version: 1, messages: [], closed: true };
|
|
225
|
+
const known = failure(error);
|
|
226
|
+
this.stop(known);
|
|
227
|
+
throw known;
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
this.pendingRead = false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/** NDJSON carrier with byte bounds and backpressure derived solely from acpMaxMessageBytes. */
|
|
235
|
+
export function createEmployeeProcessTransport(child, acpMaxMessageBytes, stderrCodes = []) {
|
|
236
|
+
capacity(acpMaxMessageBytes);
|
|
237
|
+
let partial = Buffer.alloc(0), queuedBytes = 0, writingBytes = 0, closed = false, reason;
|
|
238
|
+
let processClosed = false, terminationRequested = false;
|
|
239
|
+
const receipt = Promise.withResolvers();
|
|
240
|
+
void receipt.promise.catch(() => { });
|
|
241
|
+
let sawOutput = false, diagnosticCode, stderrPosition = 0;
|
|
242
|
+
const knownCodes = stderrCodes.filter(code => /^[A-Z][A-Z0-9_]*$/.test(code));
|
|
243
|
+
let stderrCandidates = knownCodes;
|
|
244
|
+
const queue = [];
|
|
245
|
+
let pending;
|
|
246
|
+
const writes = new Set();
|
|
247
|
+
const settle = () => { if (!pending)
|
|
248
|
+
return; const reader = pending; pending = undefined; reader.clear(); reason ? reader.reject(reason) : reader.resolve(null); };
|
|
249
|
+
const terminate = () => {
|
|
250
|
+
if (processClosed || terminationRequested)
|
|
251
|
+
return;
|
|
252
|
+
terminationRequested = true;
|
|
253
|
+
try {
|
|
254
|
+
child.stdin.end();
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
receipt.reject(need('EMPLOYEE_EXEC_UNKNOWN'));
|
|
258
|
+
}
|
|
259
|
+
// A paused output pipe must drain before Node can deliver the actual child close event.
|
|
260
|
+
child.stdout.resume();
|
|
261
|
+
child.stderr.resume();
|
|
262
|
+
try {
|
|
263
|
+
if (!child.kill('SIGTERM') && !processClosed)
|
|
264
|
+
receipt.reject(need('EMPLOYEE_EXEC_UNKNOWN'));
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
receipt.reject(need('EMPLOYEE_EXEC_UNKNOWN'));
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
const finish = (error, kill = false) => {
|
|
271
|
+
if (!closed) {
|
|
272
|
+
closed = true;
|
|
273
|
+
reason = error;
|
|
274
|
+
partial = Buffer.alloc(0);
|
|
275
|
+
if (error) {
|
|
276
|
+
queue.length = 0;
|
|
277
|
+
queuedBytes = 0;
|
|
278
|
+
}
|
|
279
|
+
settle();
|
|
280
|
+
for (const reject of writes)
|
|
281
|
+
reject(error ?? need('EMPLOYEE_EXEC_UNKNOWN'));
|
|
282
|
+
writes.clear();
|
|
283
|
+
}
|
|
284
|
+
if (kill)
|
|
285
|
+
terminate();
|
|
286
|
+
};
|
|
287
|
+
const output = (message, bytes) => {
|
|
288
|
+
if (pending) {
|
|
289
|
+
const reader = pending;
|
|
290
|
+
pending = undefined;
|
|
291
|
+
reader.clear();
|
|
292
|
+
reader.resolve(message);
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
if (queuedBytes + bytes > acpMaxMessageBytes)
|
|
296
|
+
throw need('ACP_MESSAGE_TOO_LARGE');
|
|
297
|
+
queue.push({ message, bytes });
|
|
298
|
+
queuedBytes += bytes;
|
|
299
|
+
child.stdout.pause();
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
child.stdout.on('data', (chunk) => {
|
|
303
|
+
if (closed)
|
|
304
|
+
return;
|
|
305
|
+
sawOutput = true;
|
|
306
|
+
diagnosticCode = undefined;
|
|
307
|
+
try {
|
|
308
|
+
let start = 0;
|
|
309
|
+
while (start < chunk.length) {
|
|
310
|
+
const end = chunk.indexOf(10, start), last = end < 0 ? chunk.length : end;
|
|
311
|
+
const length = partial.length + last - start;
|
|
312
|
+
if (length > acpMaxMessageBytes || queuedBytes + length > acpMaxMessageBytes)
|
|
313
|
+
throw need('ACP_MESSAGE_TOO_LARGE');
|
|
314
|
+
if (end < 0) {
|
|
315
|
+
partial = Buffer.concat([partial, chunk.subarray(start)]);
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
const line = partial.length ? Buffer.concat([partial, chunk.subarray(start, end)]) : chunk.subarray(start, end);
|
|
319
|
+
partial = Buffer.alloc(0);
|
|
320
|
+
start = end + 1;
|
|
321
|
+
let value;
|
|
322
|
+
try {
|
|
323
|
+
value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(line));
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
throw need('EMPLOYEE_EXEC_PROTOCOL_INVALID');
|
|
327
|
+
}
|
|
328
|
+
if (!object(value))
|
|
329
|
+
throw need('EMPLOYEE_EXEC_PROTOCOL_INVALID');
|
|
330
|
+
output(value, line.byteLength);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
finish(failure(error), true);
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
// Match fixed startup codes byte by byte. No arbitrary diagnostic bytes are retained.
|
|
338
|
+
child.stderr.on('data', (chunk) => {
|
|
339
|
+
if (sawOutput || closed || !knownCodes.length)
|
|
340
|
+
return;
|
|
341
|
+
for (const byte of chunk) {
|
|
342
|
+
if (byte === 10) {
|
|
343
|
+
const match = stderrCandidates.find(code => code.length === stderrPosition);
|
|
344
|
+
if (match)
|
|
345
|
+
diagnosticCode = match;
|
|
346
|
+
stderrPosition = 0;
|
|
347
|
+
stderrCandidates = knownCodes;
|
|
348
|
+
}
|
|
349
|
+
else if (stderrCandidates.length) {
|
|
350
|
+
stderrCandidates = stderrCandidates.filter(code => code.charCodeAt(stderrPosition) === byte);
|
|
351
|
+
stderrPosition++;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
child.stderr.resume();
|
|
356
|
+
child.stdin.on('error', () => finish(need(diagnosticCode ?? 'EMPLOYEE_EXEC_UNKNOWN'), true));
|
|
357
|
+
child.once('error', () => finish(need('EMPLOYEE_EXEC_UNKNOWN'), true));
|
|
358
|
+
child.once('close', (code, signal) => { processClosed = true; finish(partial.length ? need('EMPLOYEE_EXEC_PROTOCOL_INVALID') : code === 0 && !signal ? undefined : need(diagnosticCode ?? 'EMPLOYEE_EXEC_UNKNOWN')); receipt.resolve(); });
|
|
359
|
+
return {
|
|
360
|
+
write: async (message, signal) => {
|
|
361
|
+
cancelled(signal);
|
|
362
|
+
if (closed)
|
|
363
|
+
throw reason ?? need('EMPLOYEE_EXEC_UNKNOWN');
|
|
364
|
+
const value = encoded(message, acpMaxMessageBytes);
|
|
365
|
+
if (writingBytes + value.line.byteLength > acpMaxMessageBytes) {
|
|
366
|
+
const error = need('ACP_MESSAGE_TOO_LARGE');
|
|
367
|
+
finish(error, true);
|
|
368
|
+
throw error;
|
|
369
|
+
}
|
|
370
|
+
writingBytes += value.line.byteLength;
|
|
371
|
+
await new Promise((resolve, reject) => {
|
|
372
|
+
let done = false;
|
|
373
|
+
const cleanup = () => { if (done)
|
|
374
|
+
return false; done = true; writingBytes -= value.line.byteLength; writes.delete(fail); signal?.removeEventListener('abort', abort); return true; };
|
|
375
|
+
const fail = (error) => { if (cleanup())
|
|
376
|
+
reject(error); };
|
|
377
|
+
const abort = () => { const error = need('EMPLOYEE_EXEC_UNKNOWN'); finish(error, true); fail(error); };
|
|
378
|
+
writes.add(fail);
|
|
379
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
380
|
+
try {
|
|
381
|
+
child.stdin.write(value.line, error => { if (error) {
|
|
382
|
+
finish(need('EMPLOYEE_EXEC_UNKNOWN'), true);
|
|
383
|
+
fail(need('EMPLOYEE_EXEC_UNKNOWN'));
|
|
384
|
+
}
|
|
385
|
+
else if (cleanup())
|
|
386
|
+
resolve(); });
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
finish(need('EMPLOYEE_EXEC_UNKNOWN'), true);
|
|
390
|
+
fail(need('EMPLOYEE_EXEC_UNKNOWN'));
|
|
391
|
+
}
|
|
392
|
+
if (signal?.aborted)
|
|
393
|
+
abort();
|
|
394
|
+
});
|
|
395
|
+
},
|
|
396
|
+
read: async (signal) => {
|
|
397
|
+
cancelled(signal);
|
|
398
|
+
if (reason)
|
|
399
|
+
throw reason;
|
|
400
|
+
if (pending)
|
|
401
|
+
throw need('EMPLOYEE_EXEC_READ_BUSY');
|
|
402
|
+
if (queue.length) {
|
|
403
|
+
const item = queue.shift();
|
|
404
|
+
queuedBytes -= item.bytes;
|
|
405
|
+
if (!queue.length && !closed)
|
|
406
|
+
child.stdout.resume();
|
|
407
|
+
return item.message;
|
|
408
|
+
}
|
|
409
|
+
if (closed)
|
|
410
|
+
return null;
|
|
411
|
+
return new Promise((resolve, reject) => {
|
|
412
|
+
const abort = () => { if (pending?.resolve !== resolve)
|
|
413
|
+
return; pending.clear(); pending = undefined; reject(need('ABORTED')); };
|
|
414
|
+
pending = { resolve, reject, clear: () => signal?.removeEventListener('abort', abort) };
|
|
415
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
416
|
+
child.stdout.resume();
|
|
417
|
+
if (signal?.aborted)
|
|
418
|
+
abort();
|
|
419
|
+
});
|
|
420
|
+
},
|
|
421
|
+
close: () => { finish(undefined, true); queue.length = 0; queuedBytes = 0; return receipt.promise; }
|
|
422
|
+
};
|
|
423
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { EmployeeEndpointTarget, NetworkApi } from '@ai-workbench/network/api';
|
|
2
|
+
export interface EmployeeLocationSource {
|
|
3
|
+
getWorkspaceLocationState(): {
|
|
4
|
+
instanceId: string;
|
|
5
|
+
epoch: number;
|
|
6
|
+
phase: string;
|
|
7
|
+
items: {
|
|
8
|
+
workspaceId: string;
|
|
9
|
+
bindingId: string | null;
|
|
10
|
+
source: string | null;
|
|
11
|
+
status: string;
|
|
12
|
+
}[];
|
|
13
|
+
};
|
|
14
|
+
getLocalWorkspaceContext(request: {
|
|
15
|
+
instanceId: string;
|
|
16
|
+
epoch: number;
|
|
17
|
+
workspaceId: string;
|
|
18
|
+
expectedBindingId: string;
|
|
19
|
+
}): Promise<{
|
|
20
|
+
ok: true;
|
|
21
|
+
value: {
|
|
22
|
+
version: 1;
|
|
23
|
+
instanceId: string;
|
|
24
|
+
epoch: number;
|
|
25
|
+
workspaceId: string;
|
|
26
|
+
bindingId: string;
|
|
27
|
+
source: 'local';
|
|
28
|
+
directory: {
|
|
29
|
+
path: string;
|
|
30
|
+
device: number;
|
|
31
|
+
inode: number;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
} | {
|
|
35
|
+
ok: false;
|
|
36
|
+
error: {
|
|
37
|
+
code: string;
|
|
38
|
+
message: string;
|
|
39
|
+
requires: string;
|
|
40
|
+
};
|
|
41
|
+
}>;
|
|
42
|
+
}
|
|
43
|
+
export type EmployeeEndpointSource = Pick<NetworkApi, 'prepareEmployeeEndpoint'>;
|
|
44
|
+
export interface EmployeeLocationOptions {
|
|
45
|
+
api: EmployeeLocationSource;
|
|
46
|
+
network: EmployeeEndpointSource;
|
|
47
|
+
workspace: {
|
|
48
|
+
id: string;
|
|
49
|
+
path: string;
|
|
50
|
+
};
|
|
51
|
+
connection: {
|
|
52
|
+
instanceId: string;
|
|
53
|
+
epoch: number;
|
|
54
|
+
};
|
|
55
|
+
expectedLocal?: EmployeeEndpointTarget['local'];
|
|
56
|
+
settingsPath: string;
|
|
57
|
+
signal?: AbortSignal;
|
|
58
|
+
}
|
|
59
|
+
/** One admission binds source/canonical directory and real N/S execution identity. No host-path substitute. */
|
|
60
|
+
export declare function admitEmployeeLocation(options: EmployeeLocationOptions): Promise<{
|
|
61
|
+
cwd: string;
|
|
62
|
+
bindingId: string;
|
|
63
|
+
workspaceId: string;
|
|
64
|
+
endpoint: EmployeeEndpointTarget;
|
|
65
|
+
validate: (signal: AbortSignal) => Promise<void>;
|
|
66
|
+
}>;
|