@ai-workbench/remote-agent 0.8.25 → 0.8.26
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 +3 -1
- package/dist/employee-shell.d.ts +1 -1
- package/dist/employee-shell.js +45 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# AI Workbench 远程助手 · @ai-workbench/remote-agent@0.8.
|
|
1
|
+
# AI Workbench 远程助手 · @ai-workbench/remote-agent@0.8.26 · 插件 → 添加插件
|
|
2
2
|
|
|
3
3
|
通过官方 DSH 插件入口使用 Claude(远程主机)与 Codex(远程主机)。登录和工作区来源由“远程主机连接”组件提供;员工在原生工作区和模型菜单中选择文件夹与型号。
|
|
4
4
|
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
需要远程主机连接组件 0.7.29 或更新的 0.7.x,以及联网助手 0.4.9。主机来源保存于官方 storage.domain 的会话元数据表,每轮仅由连接组件检查一次主机目录,不检查本机占位目录。
|
|
8
8
|
|
|
9
|
+
Claude 本机 Bash 会单独消费员工桥代理的控制诊断,不把它交给模型当作命令输出;真实传输中断仍提示结果未知。命令自身的标准输出、标准错误与退出码照常返回。
|
|
10
|
+
|
|
9
11
|
## 会话行为
|
|
10
12
|
|
|
11
13
|
每轮启动一个新助手进程,按原生会话编号恢复上下文;不接回旧进程或重放上一轮输入。主机会话始终使用创建时保存的主机和目录;工作区之后的选择只影响新会话。
|
package/dist/employee-shell.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { CloseDiagnostics } from './close-diagnostics.ts';
|
|
2
2
|
import type { ChildProcessWithoutNullStreams } from 'node:child_process';
|
|
3
3
|
import type { EmployeeEndpointTarget } from './employee-endpoint.ts';
|
|
4
4
|
export interface EmployeeShellResult {
|
package/dist/employee-shell.js
CHANGED
|
@@ -1,19 +1,35 @@
|
|
|
1
|
-
import { closeCode } from "./close-diagnostics.js";
|
|
2
1
|
import { randomUUID } from 'node:crypto';
|
|
3
2
|
import { EMPLOYEE_PYTHON_ERRORS } from "./employee-relay.js";
|
|
4
3
|
import { NeedError } from "./remote.js";
|
|
5
4
|
const SHELL_PYTHON = String.raw `
|
|
6
|
-
import sys,json,subprocess,shlex
|
|
5
|
+
import sys,json,subprocess,shlex,tempfile,signal,re
|
|
7
6
|
def fail(code):
|
|
8
7
|
sys.stderr.write(json.dumps({"code":code})+"\n");sys.exit(254)
|
|
9
8
|
try:
|
|
10
|
-
x=json.loads(sys.argv[1]);e=x["endpoint"];args=x["sshArgs"];cwd=x["cwd"]
|
|
9
|
+
x=json.loads(sys.argv[1]);e=x["endpoint"];args=list(x["sshArgs"]);cwd=x["cwd"]
|
|
10
|
+
proxy=[i for i,arg in enumerate(args) if arg.startswith("ProxyCommand=")]
|
|
11
|
+
if len(proxy)!=1:fail("EMPLOYEE_BRIDGE_PROTOCOL_INVALID")
|
|
11
12
|
# User source is stdin, never argv. The sentinel preserves trailing newlines.
|
|
12
13
|
wrapper="cd -- "+shlex.quote(cwd)+" || exit 254; "+"printf '%s\\n' "+shlex.quote('AW_EMPLOYEE_SHELL_STARTED '+x['executionId'])+" >&2; "+'aw_x_source=$(/bin/cat; printf x); aw_x_source='+chr(36)+'{aw_x_source%?}; eval "'+chr(36)+'aw_x_source"'
|
|
13
14
|
command=shlex.join([e["endpointShell"],"-c",wrapper])
|
|
14
15
|
sys.stderr.write("AW_EMPLOYEE_SHELL_READY "+x["executionId"]+"\n");sys.stderr.flush()
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
# OpenSSH inherits ProxyCommand fd2 into its combined stderr. Separate that
|
|
17
|
+
# producer before launch; never inspect/filter the employee command's bytes.
|
|
18
|
+
def interrupted(signum,frame):
|
|
19
|
+
raise SystemExit(255)
|
|
20
|
+
for sig in [signal.SIGTERM,signal.SIGHUP,signal.SIGINT]:signal.signal(sig,interrupted)
|
|
21
|
+
with tempfile.NamedTemporaryFile(prefix="aw-x-bridge-") as diagnostic:
|
|
22
|
+
# S already escaped its own percent tokens. Only escape the added path.
|
|
23
|
+
i=proxy[0];args[i]+=" 2>"+shlex.quote(diagnostic.name.replace("%","%%"))
|
|
24
|
+
p=subprocess.Popen(["/usr/bin/ssh","-T"]+args+[command],stdin=sys.stdin,stdout=sys.stdout,stderr=sys.stderr)
|
|
25
|
+
code=p.wait()
|
|
26
|
+
diagnostic.seek(0)
|
|
27
|
+
# S's raw CLI emits at most one fixed error line, never arbitrary diagnostics.
|
|
28
|
+
raw=diagnostic.read(x["maxBytes"]+1)
|
|
29
|
+
match=re.fullmatch(rb"BRIDGE_ERROR ([A-Z][A-Z0-9_]{0,63})\n",raw)
|
|
30
|
+
bridge_code=match.group(1).decode("ascii") if match else None
|
|
31
|
+
sys.stderr.write("\nAW_EMPLOYEE_BRIDGE_RESULT "+x["executionId"]+" "+json.dumps(bridge_code)+"\n");sys.stderr.flush()
|
|
32
|
+
sys.exit(code if code>=0 else 255)
|
|
17
33
|
` + EMPLOYEE_PYTHON_ERRORS;
|
|
18
34
|
const unknown = (code) => new NeedError(code, '本机命令结果未知;没有重放,也未改在主机执行。', '员工端真实退出回执');
|
|
19
35
|
/** Use only the target verified at this round lease opening. Cancellation cannot prove remote exit. */
|
|
@@ -22,11 +38,10 @@ export function createEmployeeShell(options) {
|
|
|
22
38
|
signal.throwIfAborted();
|
|
23
39
|
const diagnostics = options.closeDiagnostics?.child();
|
|
24
40
|
const executionId = randomUUID(), target = options.target;
|
|
25
|
-
const child = await options.attach({ command: '/usr/bin/python3', args: ['-c', SHELL_PYTHON, JSON.stringify({ endpoint: target.endpoint, sshArgs: options.sshArgs, cwd: target.local.directory.path, executionId })] });
|
|
41
|
+
const child = await options.attach({ command: '/usr/bin/python3', args: ['-c', SHELL_PYTHON, JSON.stringify({ endpoint: target.endpoint, sshArgs: options.sshArgs, cwd: target.local.directory.path, executionId, maxBytes: options.maxBytes })] });
|
|
26
42
|
return new Promise((yes, no) => {
|
|
27
43
|
let settled = false, bytes = 0, stdout = [], stderr = [], reason, grace, timer;
|
|
28
|
-
const finish = (error, code) => {
|
|
29
|
-
diagnostics?.record({ layer: 'executor', initiator: 'shell.finish', reason: closeCode(error, 'SHELL_FINISHED'), action: 'close', exitCode: code ?? null, changes: [{ field: 'shell.settled', before: settled, after: true }] });
|
|
44
|
+
const finish = (error, code, exitSignal) => {
|
|
30
45
|
if (settled)
|
|
31
46
|
return;
|
|
32
47
|
settled = true;
|
|
@@ -39,6 +54,7 @@ export function createEmployeeShell(options) {
|
|
|
39
54
|
child.stdout.destroy();
|
|
40
55
|
child.stderr.destroy();
|
|
41
56
|
if (error) {
|
|
57
|
+
diagnostics?.record({ layer: 'executor', initiator: 'shell.finish', reason: error.code, action: 'failure', exitCode: code ?? null });
|
|
42
58
|
no(error);
|
|
43
59
|
return;
|
|
44
60
|
}
|
|
@@ -51,6 +67,23 @@ export function createEmployeeShell(options) {
|
|
|
51
67
|
no(unknown('EMPLOYEE_OUTPUT_BINARY_UNSUPPORTED'));
|
|
52
68
|
return;
|
|
53
69
|
}
|
|
70
|
+
const trailer = '\nAW_EMPLOYEE_BRIDGE_RESULT ' + executionId + ' ', end = err.lastIndexOf(trailer);
|
|
71
|
+
let bridgeCode;
|
|
72
|
+
if (end >= 0) {
|
|
73
|
+
try {
|
|
74
|
+
const value = JSON.parse(err.slice(end + trailer.length));
|
|
75
|
+
if (typeof value === 'string' && /^[A-Z][A-Z0-9_]{0,63}$/.test(value))
|
|
76
|
+
bridgeCode = value;
|
|
77
|
+
else if (value !== null)
|
|
78
|
+
throw Error();
|
|
79
|
+
err = err.slice(0, end);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
no(unknown('EMPLOYEE_EXIT_UNKNOWN'));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
diagnostics?.record({ layer: 'relay', initiator: 'shell.process-close', reason: 'PROCESS_EXIT', action: 'exit', exitCode: code ?? null, signal: exitSignal ?? null, ...bridgeCode ? { remoteReason: bridgeCode } : {} });
|
|
54
87
|
const header = 'AW_EMPLOYEE_SHELL_READY ' + executionId + '\n', started = 'AW_EMPLOYEE_SHELL_STARTED ' + executionId + '\n';
|
|
55
88
|
const start = err.indexOf(started, header.length);
|
|
56
89
|
if (!err.startsWith(header) || start < header.length || start > header.length && err[start - 1] !== '\n') {
|
|
@@ -62,11 +95,11 @@ export function createEmployeeShell(options) {
|
|
|
62
95
|
source = parsed.code;
|
|
63
96
|
}
|
|
64
97
|
catch { /* Never expose arbitrary transport diagnostics. */ }
|
|
65
|
-
no(unknown(source ?? 'EMPLOYEE_EXIT_UNKNOWN'));
|
|
98
|
+
no(unknown(bridgeCode ?? source ?? 'EMPLOYEE_EXIT_UNKNOWN'));
|
|
66
99
|
return;
|
|
67
100
|
}
|
|
68
|
-
if (code === null || code === undefined || code === 255) {
|
|
69
|
-
no(unknown('EMPLOYEE_EXIT_UNKNOWN'));
|
|
101
|
+
if (code === null || code === undefined || code === 255 || end < 0) {
|
|
102
|
+
no(unknown(bridgeCode ?? 'EMPLOYEE_EXIT_UNKNOWN'));
|
|
70
103
|
return;
|
|
71
104
|
}
|
|
72
105
|
yes({ stdout: out, stderr: err.slice(header.length, start) + err.slice(start + started.length), stderrSource: 'combined-ssh-and-command', exitCode: code, signal: null });
|
|
@@ -85,7 +118,7 @@ export function createEmployeeShell(options) {
|
|
|
85
118
|
child.stderr.on('data', (b) => collect(stderr, b));
|
|
86
119
|
child.stdin.on('error', () => stop('EMPLOYEE_INPUT_UNKNOWN'));
|
|
87
120
|
child.once('error', () => { diagnostics?.record({ layer: 'relay', initiator: 'shell.process-error', reason: 'EMPLOYEE_TRANSPORT_UNKNOWN', action: 'failure' }); finish(unknown('EMPLOYEE_TRANSPORT_UNKNOWN')); });
|
|
88
|
-
child.once('close', (code, exitSignal) =>
|
|
121
|
+
child.once('close', (code, exitSignal) => finish(reason, code, exitSignal));
|
|
89
122
|
signal.addEventListener('abort', abort, { once: true });
|
|
90
123
|
if (signal.aborted)
|
|
91
124
|
abort();
|