@ai-workbench/remote-agent 0.8.24 → 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 +9 -5
- package/dist/connect-contract.d.ts +1 -1
- package/dist/employee-shell.d.ts +1 -1
- package/dist/employee-shell.js +45 -12
- package/dist/host-workspace.d.ts +10 -15
- package/dist/host-workspace.js +26 -109
- package/dist/index.js +6 -16
- package/dist/preflight-diagnostic.js +1 -1
- package/dist/presentation.js +1 -0
- package/dist/provider.d.ts +9 -1
- package/dist/provider.js +42 -80
- package/dist/session-source.d.ts +18 -0
- package/dist/session-source.js +90 -0
- package/package.json +7 -5
package/README.md
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
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
|
|
|
5
|
-
文件和命令在所选的本机或主机目录执行,订阅凭据留在主机。插件不改写 DSH
|
|
5
|
+
文件和命令在所选的本机或主机目录执行,订阅凭据留在主机。插件不改写 DSH 的默认模型,不为员工创建同名主机目录,已有主机会话只提取一次原有主机来源并保存。
|
|
6
|
+
|
|
7
|
+
需要远程主机连接组件 0.7.29 或更新的 0.7.x,以及联网助手 0.4.9。主机来源保存于官方 storage.domain 的会话元数据表,每轮仅由连接组件检查一次主机目录,不检查本机占位目录。
|
|
8
|
+
|
|
9
|
+
Claude 本机 Bash 会单独消费员工桥代理的控制诊断,不把它交给模型当作命令输出;真实传输中断仍提示结果未知。命令自身的标准输出、标准错误与退出码照常返回。
|
|
6
10
|
|
|
7
11
|
## 会话行为
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
每轮启动一个新助手进程,按原生会话编号恢复上下文;不接回旧进程或重放上一轮输入。主机会话始终使用创建时保存的主机和目录;工作区之后的选择只影响新会话。
|
|
10
14
|
|
|
11
15
|
断线会结束本轮;操作可能已部分执行,请先检查文件。恢复连接后可在原会话继续。旧进程清理和技术详情记录尽力而为,不阻止下一轮。原生本机会话恢复的兼容性及实际设备结果见本轮交付证据,未验证项不作为已通过。
|
|
12
16
|
|
|
@@ -17,7 +21,7 @@ DSH 启动和打开模型菜单时读取一次主机型号列表。发送时不
|
|
|
17
21
|
- 员工选本机文件夹,就在员工电脑上执行;通道失败不会改在主机运行。
|
|
18
22
|
- 订阅凭据留在主机,不放进命令参数、日志或技术详情。
|
|
19
23
|
- 跟随 DSH 当前权限:完全权限不问、工作区内修改先问、仅可查看只读。需要审批时使用官方卡片。
|
|
20
|
-
-
|
|
24
|
+
- 一条会话对应一个文件来源;主机来源不会随工作区后续选择改变,本机来源继续沿用原生绑定。
|
|
21
25
|
|
|
22
26
|
Codex 员工端只使用随包提供的原生组件,通道等待本次组件的启动标记后才完成开通,不逐通道执行摘要或版本校验。当前员工端支持范围以发布验证记录为准,不让员工手填执行器路径。
|
|
23
27
|
|
|
@@ -40,7 +44,7 @@ Codex 员工端只使用随包提供的原生组件,通道等待本次组件
|
|
|
40
44
|
|
|
41
45
|
技术详情只写官方 profile 下本地私有日志,并由既有技术详情界面展开显示。写入失败不覆盖原始错误、不等待远端投递回执。每个运行环境只记录首次关闭原因;首先观察到的原因不自动等同跨层根因。
|
|
42
46
|
|
|
43
|
-
|
|
47
|
+
工程验证和独立审查见 `docs/evidence/2026-10-25-X-host-source-v2/`;AUTH profile 夹具补齐与最终 main 全套复验见其中的 `auth-profile-return.md`。固定包摘要及真实设备验收状态以项目 X 回执为准。插件不修改 DSH 核心,不添加启动器或自定义 profile。
|
|
44
48
|
|
|
45
49
|
远程助手参数页只显示官方 ConfigForms 当前值,不提供保存、放弃或恢复继承值;修改请使用 DSH 官方 Config“打开配置文件”入口。
|
|
46
50
|
|
|
@@ -20,5 +20,5 @@ export type SessionProjectResolution = HostSessionProjectTarget;
|
|
|
20
20
|
export interface NativeWorkspaceApi {
|
|
21
21
|
getContractInfo(): ContractInfo;
|
|
22
22
|
ensureReady(provider: LoginProvider): Promise<ConnectResult<ConnectTarget>>;
|
|
23
|
-
resolveSessionProject(sessionId: string, use: WorkspaceUse, location
|
|
23
|
+
resolveSessionProject(sessionId: string, use: WorkspaceUse, location: HostWorkspaceResolveContext): Promise<ConnectResult<SessionProjectResolution>>;
|
|
24
24
|
}
|
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();
|
package/dist/host-workspace.d.ts
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
/** Current official membership and explicit host selection; never infer source from a pathname. */
|
|
12
|
-
export declare function hostWorkspaceGuard(get: () => ConnectApi | undefined, api: ConnectApi & NativeWorkspaceApi, connection: ConnectionState, target: ConnectTarget, request: RuntimeRequest, workspace: ((request: RuntimeRequest) => Promise<{
|
|
13
|
-
id: string;
|
|
14
|
-
path: string;
|
|
15
|
-
}>) | undefined, provider: LoginProvider): Promise<HostWorkspaceGuard | undefined>;
|
|
1
|
+
import type { HostWorkspaceResolveContext, LoginProvider, NativeWorkspaceApi } from './connect-contract.ts';
|
|
2
|
+
import type { ConnectApi, RuntimeRequest } from './provider.ts';
|
|
3
|
+
export type SessionSource = Readonly<HostWorkspaceResolveContext> | Readonly<{
|
|
4
|
+
source: 'local';
|
|
5
|
+
}>;
|
|
6
|
+
export declare const absoluteHostPath: (path: unknown) => path is string;
|
|
7
|
+
/** Read only L's public source projection. Old host status/binding/physical fields have no meaning here. */
|
|
8
|
+
export declare function workspaceSessionSource(api: ConnectApi, workspaceId: string, request?: RuntimeRequest): SessionSource | undefined;
|
|
9
|
+
/** One L v2 resolution per user turn. L owns host/path checks and the current SSH target. */
|
|
10
|
+
export declare function resolveHostWorkspace(api: ConnectApi & NativeWorkspaceApi, request: RuntimeRequest, provider: LoginProvider): Promise<import("@ai-workbench/connect/api").HostSessionProjectTarget>;
|
package/dist/host-workspace.js
CHANGED
|
@@ -1,114 +1,31 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { awaitL, sameTarget, unwrap } from "./provider.js";
|
|
3
|
-
import { connectNeedError } from "./connect-need.js";
|
|
1
|
+
import { awaitL, unwrap } from "./provider.js";
|
|
4
2
|
import { NeedError } from "./remote.js";
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
function
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
// Directory contents change timestamps; neither timestamp is an execution identity.
|
|
12
|
-
return JSON.stringify([d.requestedPath, d.path, d.parentPath, d.identity.device, d.identity.inode, d.entryIdentity.device, d.entryIdentity.inode, d.isSymlink, d.permissions.read, d.permissions.write, d.permissions.execute]);
|
|
13
|
-
}
|
|
14
|
-
/** Current official membership and explicit host selection; never infer source from a pathname. */
|
|
15
|
-
export async function hostWorkspaceGuard(get, api, connection, target, request, workspace, provider) {
|
|
16
|
-
if (api.getContractInfo().workspaceLocations !== 1 || typeof api.getWorkspaceLocationState !== 'function')
|
|
17
|
-
return;
|
|
18
|
-
const firstState = api.getWorkspaceLocationState();
|
|
19
|
-
if (!workspace) {
|
|
20
|
-
if (firstState.items.some(row => row.source === 'host'))
|
|
21
|
-
throw new NeedError('DEPENDENCY_REQUIRED', '我需要核对当前会话所属工作区,才能使用所选主机文件夹。', '官方会话与唯一workspaceRegistry关系');
|
|
3
|
+
export const absoluteHostPath = (path) => typeof path === 'string' && path.startsWith('/') && !path.includes('\0');
|
|
4
|
+
const unavailable = () => new NeedError('WORKSPACE_LOCATION_REQUIRED', '我需要这条会话原来选择的文件来源,请在远程主机连接中查看工作区文件来源。', '会话的主机名称和绝对目录');
|
|
5
|
+
/** Read only L's public source projection. Old host status/binding/physical fields have no meaning here. */
|
|
6
|
+
export function workspaceSessionSource(api, workspaceId, request) {
|
|
7
|
+
const rows = api.getWorkspaceLocationState?.().items.filter(row => row.workspaceId === workspaceId) ?? [];
|
|
8
|
+
if (rows.length !== 1)
|
|
22
9
|
return;
|
|
10
|
+
const row = rows[0];
|
|
11
|
+
if (row.source === 'local') {
|
|
12
|
+
if (request?.cwd && request.cwd !== row.localPath)
|
|
13
|
+
throw unavailable();
|
|
14
|
+
return Object.freeze({ source: 'local' });
|
|
23
15
|
}
|
|
24
|
-
|
|
25
|
-
request.signal?.throwIfAborted();
|
|
26
|
-
const state = api.getWorkspaceLocationState(), rows = state.items.filter(row => row.workspaceId === registered.id), selected = rows[0];
|
|
27
|
-
if (rows.length !== 1 || !selected)
|
|
16
|
+
if (row.source !== 'host')
|
|
28
17
|
return;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (!registered.id || !absolute(registered.path) || state.phase !== 'ready' || selected.status !== 'selected' || !selected.bindingId)
|
|
43
|
-
throw invalid();
|
|
44
|
-
const trusted = structuredClone(target);
|
|
45
|
-
if (selected.host && (selected.host.host !== trusted.host || selected.host.username !== trusted.username || selected.host.port !== trusted.port))
|
|
46
|
-
throw invalid();
|
|
47
|
-
const captured = structuredClone(selected), stamp = physical(captured.remote), context = { instanceId: connection.instanceId, epoch: connection.epoch, workspaceId: registered.id, expectedBindingId: selected.bindingId };
|
|
48
|
-
const revoked = new AbortController(), offs = [];
|
|
49
|
-
let runtime, disposed = false;
|
|
50
|
-
const current = () => {
|
|
51
|
-
request.signal?.throwIfAborted();
|
|
52
|
-
revoked.signal.throwIfAborted();
|
|
53
|
-
if (disposed || get() !== api || api.getContractInfo().hostWorkspaceResolution !== 1)
|
|
54
|
-
throw invalid();
|
|
55
|
-
const c = api.getConnectionState(), s = api.getWorkspaceLocationState(), items = s.items.filter(row => row.workspaceId === context.workspaceId), item = items[0];
|
|
56
|
-
if (c.phase !== 'connected' || c.instanceId !== context.instanceId || c.epoch !== context.epoch)
|
|
57
|
-
throw new NeedError('CONNECTION_LOST', '远程主机连接已变化,原派发已停止;请新建会话继续。', '本轮原连接代次');
|
|
58
|
-
if (s.instanceId !== context.instanceId || s.epoch !== context.epoch)
|
|
59
|
-
throw invalid();
|
|
60
|
-
if (s.need)
|
|
61
|
-
throw connectNeedError(s.need);
|
|
62
|
-
if (item?.need)
|
|
63
|
-
throw connectNeedError(item.need);
|
|
64
|
-
if (hostIdentity(item?.host) !== hostIdentity(captured.host))
|
|
65
|
-
throw invalid();
|
|
66
|
-
if (s.phase !== 'ready' || items.length !== 1 || item.bindingId !== context.expectedBindingId || item.workspaceId !== context.workspaceId || item.source !== 'host' || item.status !== 'selected' || item.localPath !== captured.localPath || physical(item.remote) !== stamp)
|
|
67
|
-
throw invalid();
|
|
68
|
-
};
|
|
69
|
-
const revoke = (error) => { if (revoked.signal.aborted)
|
|
70
|
-
return; runtime?.closeDiagnostics?.record({ layer: 'adapter', initiator: 'host-workspace.observer', reason: error instanceof NeedError ? error.code : 'HOST_WORKSPACE_REVOKED', action: 'abort', changes: [{ field: 'host-workspace.valid', before: true, after: false }] }); revoked.abort(error); };
|
|
71
|
-
const changed = () => { if (request.signal?.aborted && !revoked.signal.aborted)
|
|
72
|
-
return; try {
|
|
73
|
-
current();
|
|
74
|
-
}
|
|
75
|
-
catch (error) {
|
|
76
|
-
revoke(error);
|
|
77
|
-
} };
|
|
78
|
-
const dispose = () => { if (disposed)
|
|
79
|
-
return; disposed = true; for (const off of offs.splice(0))
|
|
80
|
-
off(); };
|
|
81
|
-
const membership = async () => { current(); const now = await workspace(request); current(); if (now.id !== registered.id || now.path !== registered.path)
|
|
82
|
-
throw invalid(); };
|
|
83
|
-
try {
|
|
84
|
-
current();
|
|
85
|
-
offs.push(api.onWorkspaceLocationState(changed));
|
|
86
|
-
offs.push(api.onConnectionState(changed));
|
|
87
|
-
current();
|
|
88
|
-
return { revocation: revoked.signal, signal: AbortSignal.any([revoked.signal, ...(request.signal ? [request.signal] : [])]), assertCurrent: current, observeRuntime(value) { runtime = value; current(); }, dispose,
|
|
89
|
-
async resolve() {
|
|
90
|
-
try {
|
|
91
|
-
await membership();
|
|
92
|
-
const value = unwrap(await awaitL(() => api.resolveSessionProject(request.sessionId, { provider: provider, purpose: 'user-turn' }, { ...context }), AbortSignal.any([revoked.signal, ...(request.signal ? [request.signal] : [])])));
|
|
93
|
-
await membership();
|
|
94
|
-
if (!value || !('source' in value) || value.source !== 'host' || value.instanceId !== context.instanceId || value.epoch !== context.epoch || value.sessionId !== request.sessionId || value.workspaceId !== context.workspaceId || value.bindingId !== context.expectedBindingId || !Number.isSafeInteger(value.bindingRevision) || value.bindingRevision < 1 || (!value.target || !value.target.session || !sameTarget(trusted, value.target)) || physical(value.directory) !== stamp || value.project?.path !== value.directory.path || value.project?.name !== (basename(value.directory.path) || '/') || 'notice' in value && value.notice !== undefined)
|
|
95
|
-
throw invalid();
|
|
96
|
-
return value;
|
|
97
|
-
}
|
|
98
|
-
catch (error) {
|
|
99
|
-
if (!request.signal?.aborted) {
|
|
100
|
-
const cause = revoked.signal.reason;
|
|
101
|
-
revoke(error);
|
|
102
|
-
if (cause instanceof NeedError)
|
|
103
|
-
throw cause;
|
|
104
|
-
}
|
|
105
|
-
throw error;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
catch (error) {
|
|
111
|
-
dispose();
|
|
112
|
-
throw error;
|
|
113
|
-
}
|
|
18
|
+
const host = row.host?.host, path = request?.cwd ?? row.remote?.path;
|
|
19
|
+
if (typeof host !== 'string' || !host || !absoluteHostPath(path))
|
|
20
|
+
throw unavailable();
|
|
21
|
+
return Object.freeze({ source: 'host', host, path });
|
|
22
|
+
}
|
|
23
|
+
/** One L v2 resolution per user turn. L owns host/path checks and the current SSH target. */
|
|
24
|
+
export async function resolveHostWorkspace(api, request, provider) {
|
|
25
|
+
const source = request.workspaceSource;
|
|
26
|
+
if (source?.source !== 'host')
|
|
27
|
+
throw unavailable();
|
|
28
|
+
if ((api.getContractInfo().hostWorkspaceResolution ?? 0) < 2)
|
|
29
|
+
throw new NeedError('DEPENDENCY_REQUIRED', '我需要更新远程主机连接组件后再使用主机目录;本轮未执行,原话保留。', 'L >=0.7.29 / hostWorkspaceResolution >=2');
|
|
30
|
+
return unwrap(await awaitL(() => api.resolveSessionProject(request.sessionId, { provider, purpose: 'user-turn' }, { ...source }), request.signal));
|
|
114
31
|
}
|
package/dist/index.js
CHANGED
|
@@ -22,6 +22,7 @@ import { ModelPresentation } from "./model-presentation.js";
|
|
|
22
22
|
import { EMPLOYEE_EXECUTION_NOTICE } from "./employee-presentation.js";
|
|
23
23
|
import { ModelConfigurationError } from "./model-configuration.js";
|
|
24
24
|
import { officialEmployeeWorkspace } from "./employee-workspace.js";
|
|
25
|
+
import { createSessionSources } from "./session-source.js";
|
|
25
26
|
import { resolveEmployeeNative } from "./employee-native.js";
|
|
26
27
|
import { createClaudeLocalProvider } from "./claude-local.js";
|
|
27
28
|
import { readLocalUserTurn } from "./local-user-turn.js";
|
|
@@ -278,7 +279,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
278
279
|
const missingPointer = !!last && !previous;
|
|
279
280
|
live = undefined;
|
|
280
281
|
const configuration = JSON.stringify([config.acpMaxMessageBytes, config.brokerMaxFrameBytes, config.brokerControlMaxBytes, config.employeeShellKillGraceMs]);
|
|
281
|
-
Object.assign(request, { acpMaxMessageBytes: config.acpMaxMessageBytes, resumeSessionId: previous?.sessionId, sessionId: options.sessionId, mode, cwd: previous?.cwd, identity: previous?.identity, bindingId: previous?.bindingId, workspaceId: previous?.workspaceId, agent: this.agent, signal, brokerSettings: Object.freeze({ brokerMaxFrameBytes: config.brokerMaxFrameBytes, brokerControlMaxBytes: config.brokerControlMaxBytes }) });
|
|
282
|
+
Object.assign(request, { acpMaxMessageBytes: config.acpMaxMessageBytes, resumeSessionId: previous?.sessionId, sessionId: options.sessionId, mode, hostSource: previous?.hostSource, cwd: previous?.cwd, identity: previous?.identity, bindingId: previous?.bindingId, workspaceId: previous?.workspaceId, agent: this.agent, signal, brokerSettings: Object.freeze({ brokerMaxFrameBytes: config.brokerMaxFrameBytes, brokerControlMaxBytes: config.brokerControlMaxBytes }) });
|
|
282
283
|
request.previousContextSha256 = previous?.contextSha256;
|
|
283
284
|
const after = options.messages.findLastIndex(message => message.role === 'assistant');
|
|
284
285
|
const tail = options.messages.slice(after + 1);
|
|
@@ -323,13 +324,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
323
324
|
request.signal = modelSignal;
|
|
324
325
|
modelSignal.throwIfAborted();
|
|
325
326
|
}
|
|
326
|
-
if (runtime.
|
|
327
|
-
modelSignal = AbortSignal.any([modelSignal, runtime.contextSignal]);
|
|
328
|
-
options.signal = modelSignal;
|
|
329
|
-
request.signal = modelSignal;
|
|
330
|
-
modelSignal.throwIfAborted();
|
|
331
|
-
}
|
|
332
|
-
if (previous && (previous.identity !== runtime.identity || previous.cwd !== runtime.cwd || previous.bindingId && previous.bindingId !== runtime.binding?.bindingId || previous.workspaceId && previous.workspaceId !== runtime.binding?.workspaceId))
|
|
327
|
+
if (previous && !runtime.hostSource && (previous.identity !== runtime.identity || previous.cwd !== runtime.cwd || previous.bindingId && previous.bindingId !== runtime.binding?.bindingId || previous.workspaceId && previous.workspaceId !== runtime.binding?.workspaceId))
|
|
333
328
|
throw new NeedError('SESSION_CONTEXT_MISMATCH', '原会话的员工主机或工作区已改变,请新建会话', '原会话的可信目标和工作区');
|
|
334
329
|
input = projectHostWorkspaceInput(input, runtime.hostWorkspace);
|
|
335
330
|
if (runtime.hostWorkspace && mode === 'session' && Buffer.byteLength(JSON.stringify({ prompt: input.prompt, systemPrompt: input.systemPrompt }), 'utf8') > config.acpMaxMessageBytes)
|
|
@@ -531,17 +526,11 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
531
526
|
}
|
|
532
527
|
}
|
|
533
528
|
yield* mapper.flush();
|
|
534
|
-
const pointer = { ...(modelConfiguration ? { modelConfiguration } : {}), version: 2, agent: this.agent.id, contextSha256, ...(mode === 'session' ? { usageSnapshot: mapper.usageSnapshot } : {}), ...(options.sessionId ? { dshSessionId: options.sessionId } : {}), identity: runtime.identity, cwd: runtime.cwd, sessionId: id, ...(runtime.binding ? { bindingId: runtime.binding.bindingId, workspaceId: runtime.binding.workspaceId } : {}) };
|
|
529
|
+
const pointer = { ...(runtime.hostSource ? { hostSource: runtime.hostSource } : {}), ...(modelConfiguration ? { modelConfiguration } : {}), version: 2, agent: this.agent.id, contextSha256, ...(mode === 'session' ? { usageSnapshot: mapper.usageSnapshot } : {}), ...(options.sessionId ? { dshSessionId: options.sessionId } : {}), identity: runtime.identity, cwd: runtime.cwd, sessionId: id, ...(runtime.binding ? { bindingId: runtime.binding.bindingId, workspaceId: runtime.binding.workspaceId } : {}) };
|
|
535
530
|
succeeded = true;
|
|
536
531
|
yield { type: 'finish', reason: { kind: stop === 'max_tokens' || stop === 'max_turn_requests' ? 'max-tokens' : 'stop' }, replayState: { response: pointer } };
|
|
537
532
|
}
|
|
538
533
|
catch (error) {
|
|
539
|
-
// ACP cancellation must retain the source revocation that caused it.
|
|
540
|
-
// User cancellation and unrelated signals keep their existing semantics.
|
|
541
|
-
const sourceReason = runtime?.contextSignal?.aborted ? runtime.contextSignal.reason : undefined;
|
|
542
|
-
if (error instanceof NeedError && error.code === 'ABORTED' && sourceReason instanceof NeedError) {
|
|
543
|
-
error = Object.assign(new NeedError(sourceReason.code, sourceReason.message, sourceReason.requires), { ...(sourceReason.connectNeed ? { connectNeed: sourceReason.connectNeed } : {}), ...(sourceReason.officialReason ? { officialReason: sourceReason.officialReason } : {}), cause: new AggregateError([sourceReason, error], 'HOST_SOURCE_REVOKED') });
|
|
544
|
-
}
|
|
545
534
|
turnFailure = error;
|
|
546
535
|
runtime?.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.turn-failure', reason: closeCode(error, signal.aborted ? 'ABORTED' : 'TURN_FAILED'), action: 'failure' });
|
|
547
536
|
recordInformation();
|
|
@@ -614,6 +603,7 @@ export function apply(ctx, live) {
|
|
|
614
603
|
return manager;
|
|
615
604
|
};
|
|
616
605
|
ctx.provide('aiWorkbenchRemoteDiagnostics', { read: (query) => managerForProfile().read(query) });
|
|
606
|
+
const sessionSources = createSessionSources(ctx, get);
|
|
617
607
|
const preflightDiagnostics = createPreflightDiagnostics({ ctx, getApi: get, manager: managerForProfile });
|
|
618
608
|
const diagnostics = new Map();
|
|
619
609
|
const diagnostic = (scope, provider, message) => {
|
|
@@ -666,7 +656,7 @@ export function apply(ctx, live) {
|
|
|
666
656
|
const sessions = services.get('sessions');
|
|
667
657
|
return officialEmployeeWorkspace(request.sessionId, registry, sessions?.get(request.sessionId)?.header);
|
|
668
658
|
};
|
|
669
|
-
const provider = connectProvider(get, { agent, workspace, ...(agent.loginProvider === 'codex' ? { employee: {
|
|
659
|
+
const provider = connectProvider(get, { agent, workspace, source: sessionSources, ...(agent.loginProvider === 'codex' ? { employee: {
|
|
670
660
|
getNetwork: () => ctx.get('aiWorkbenchNetwork'),
|
|
671
661
|
workspace, native: () => resolveEmployeeNative(),
|
|
672
662
|
} } : {}), details, open, local });
|
|
@@ -49,7 +49,7 @@ export function createPreflightDiagnostics(options) {
|
|
|
49
49
|
const rows = state?.items.filter(row => row.workspaceId === workspace.id) ?? [];
|
|
50
50
|
if (state?.phase === 'ready' && state.instanceId === connection?.instanceId && state.epoch === connection.epoch && rows.length === 1) {
|
|
51
51
|
const row = rows[0];
|
|
52
|
-
source = { workspaceId: row.workspaceId, bindingId: row.bindingId, source: row.source === 'local' || row.source === 'host' ? row.source : null, status: row.status };
|
|
52
|
+
source = { workspaceId: row.workspaceId, bindingId: row.bindingId, source: row.source === 'local' || row.source === 'host' ? row.source : null, status: row.source === 'host' ? null : row.status ?? null };
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
}
|
package/dist/presentation.js
CHANGED
|
@@ -12,6 +12,7 @@ export function employeeFailure(code, brand = 'codex') {
|
|
|
12
12
|
PERMISSION_POLICY_CHANGED: '当前权限已变化或无法核对,这次派发已停止;没有自动重发,你的原话仍保留,在途结果请查看技术详情。',
|
|
13
13
|
APPROVAL_STALE: '连接或权限已变化,原审批已失效;没有自动重发,在途结果请查看技术详情。',
|
|
14
14
|
PROJECT_BINDING_INVALID: '文件来源已改,请新建会话继续;原会话历史仍可查看。',
|
|
15
|
+
WORKSPACE_SOURCE_STORAGE_FAILED: '这条会话的文件来源未能保存,本轮工作区准备失败;请恢复后在原会话重试。',
|
|
15
16
|
WORKSPACE_LOCATION_REQUIRED: '我需要你确认这个工作区使用哪一个文件夹,本轮未执行;你的原话仍保留。',
|
|
16
17
|
WORKSPACE_LOCATION_CONFLICT: '工作区来源选择已更新,本次确认不能覆盖新的选择;本轮未执行,你的原话仍保留。',
|
|
17
18
|
LOCAL_USER_TURN_CHANGED: '本次输入已变化,本轮已停止,请确认对话后重新发送。',
|
package/dist/provider.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type SessionSource } from './host-workspace.ts';
|
|
1
2
|
import type { CloseCause, CloseDiagnostics } from './close-diagnostics.ts';
|
|
2
3
|
import type { AgentApp } from '@agentclientprotocol/sdk';
|
|
3
4
|
import type { AgentApp as ExtendedAgentApp } from '@agentclientprotocol/sdk/unstable';
|
|
@@ -27,7 +28,9 @@ export interface Runtime {
|
|
|
27
28
|
identity: string;
|
|
28
29
|
cwd: string;
|
|
29
30
|
hostWorkspace?: Readonly<HostWorkspacePresentation>;
|
|
30
|
-
|
|
31
|
+
hostSource?: Extract<SessionSource, {
|
|
32
|
+
source: 'host';
|
|
33
|
+
}>;
|
|
31
34
|
execution?: EmployeeExecution;
|
|
32
35
|
acpTransport?: {
|
|
33
36
|
writable: WritableStream<Uint8Array>;
|
|
@@ -61,6 +64,10 @@ export interface Runtime {
|
|
|
61
64
|
closeDiagnostics?: CloseDiagnostics;
|
|
62
65
|
}
|
|
63
66
|
export interface RuntimeRequest extends Invocation {
|
|
67
|
+
workspaceSource?: SessionSource;
|
|
68
|
+
hostSource?: Extract<SessionSource, {
|
|
69
|
+
source: 'host';
|
|
70
|
+
}>;
|
|
64
71
|
closeDiagnostics?: CloseDiagnostics;
|
|
65
72
|
previousContextSha256?: string;
|
|
66
73
|
preflightFailureRecorded?: boolean;
|
|
@@ -169,6 +176,7 @@ export declare function connectProvider(get: () => ConnectApi | undefined, optio
|
|
|
169
176
|
id: string;
|
|
170
177
|
path: string;
|
|
171
178
|
}>;
|
|
179
|
+
source?: (request: RuntimeRequest, api: ConnectApi) => Promise<SessionSource | undefined>;
|
|
172
180
|
employee?: EmployeeProviderOptions;
|
|
173
181
|
local?: LocalRuntimeProvider;
|
|
174
182
|
details?: (request: RuntimeRequest, api: ConnectApi, connection: ConnectionState, target: ConnectTarget) => Promise<DetailChannel>;
|
package/dist/provider.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveHostWorkspace, workspaceSessionSource } from "./host-workspace.js";
|
|
2
2
|
import { connectNeedError } from "./connect-need.js";
|
|
3
3
|
import { loginDiagnostic, loginReason } from "./login-diagnostic.js";
|
|
4
4
|
import { NeedError } from "./remote.js";
|
|
@@ -145,30 +145,6 @@ export function sameTarget(a, b) {
|
|
|
145
145
|
}
|
|
146
146
|
export function connectProvider(get, options = {}) {
|
|
147
147
|
const agent = options.agent ?? CODEX_AGENT;
|
|
148
|
-
const hostScopes = new WeakMap();
|
|
149
|
-
const hostMethods = new WeakMap();
|
|
150
|
-
const retainHostScope = (runtime, scope) => {
|
|
151
|
-
hostScopes.get(runtime)?.dispose();
|
|
152
|
-
hostScopes.set(runtime, scope);
|
|
153
|
-
runtime.contextSignal = scope.revocation;
|
|
154
|
-
if (hostMethods.has(runtime))
|
|
155
|
-
return;
|
|
156
|
-
const original = { dispose: runtime.dispose, detach: runtime.detach };
|
|
157
|
-
hostMethods.set(runtime, original);
|
|
158
|
-
const release = () => { hostScopes.get(runtime)?.dispose(); hostScopes.delete(runtime); };
|
|
159
|
-
runtime.dispose = async (cause) => { try {
|
|
160
|
-
await original.dispose?.(cause);
|
|
161
|
-
}
|
|
162
|
-
finally {
|
|
163
|
-
release();
|
|
164
|
-
} };
|
|
165
|
-
runtime.detach = async (cause) => { try {
|
|
166
|
-
await original.detach?.(cause);
|
|
167
|
-
}
|
|
168
|
-
finally {
|
|
169
|
-
release();
|
|
170
|
-
} };
|
|
171
|
-
};
|
|
172
148
|
// Keep the legacy source SDK wrapper's strip-only import graph free of broker runtime syntax.
|
|
173
149
|
// Cache the initialization promise so concurrent sessions share this provider's binding lifetimes.
|
|
174
150
|
let employee;
|
|
@@ -182,7 +158,13 @@ export function connectProvider(get, options = {}) {
|
|
|
182
158
|
requireLoginCoordinator(api);
|
|
183
159
|
let sourceStage = 'input-acceptance';
|
|
184
160
|
try {
|
|
185
|
-
|
|
161
|
+
request.workspaceSource = options.source ? await options.source(request, api) : request.hostSource;
|
|
162
|
+
if (!options.source && !request.workspaceSource && options.workspace && request.sessionId && api.getContractInfo().workspaceLocations === 1) {
|
|
163
|
+
const workspace = await options.workspace(request);
|
|
164
|
+
request.workspaceSource = workspaceSessionSource(api, workspace.id, request);
|
|
165
|
+
}
|
|
166
|
+
const host = request.workspaceSource?.source === 'host';
|
|
167
|
+
if (options.employee && !host) {
|
|
186
168
|
const module = await import("./employee-provider.js");
|
|
187
169
|
request.localContext = await module.inspectEmployeeWorkspace(get, options.employee, agent, api, request);
|
|
188
170
|
}
|
|
@@ -198,15 +180,16 @@ export function connectProvider(get, options = {}) {
|
|
|
198
180
|
const connection = await waitForLogin(api, agent.loginProvider, before, get, request.signal);
|
|
199
181
|
return { target, connection };
|
|
200
182
|
})();
|
|
201
|
-
if (request.identity && request.identity !== identity(target))
|
|
183
|
+
if (request.workspaceSource?.source !== 'host' && request.identity && request.identity !== identity(target))
|
|
202
184
|
throw new NeedError('SESSION_CONTEXT_MISMATCH', '员工主机与原会话不一致,请新建会话', '原员工主机');
|
|
203
185
|
request.localAdmissionCheck?.();
|
|
204
186
|
admissions.set(request, { api, target, connection });
|
|
205
187
|
sourceStage = 'local-admission';
|
|
206
|
-
const locations = api.getContractInfo().workspaceLocations === 1 ? api.getWorkspaceLocationState?.() : undefined;
|
|
207
|
-
if (locations && (locations.phase === 'failed' || locations.phase === 'disposed') && locations.instanceId === connection.instanceId && locations.epoch === connection.epoch && locations.need)
|
|
188
|
+
const locations = !host && api.getContractInfo().workspaceLocations === 1 ? api.getWorkspaceLocationState?.() : undefined;
|
|
189
|
+
if (!host && locations && (locations.phase === 'failed' || locations.phase === 'disposed') && locations.instanceId === connection.instanceId && locations.epoch === connection.epoch && locations.need)
|
|
208
190
|
throw connectNeedError(locations.need);
|
|
209
|
-
|
|
191
|
+
if (!host)
|
|
192
|
+
await options.local?.admit?.(request, api, connection, target);
|
|
210
193
|
}
|
|
211
194
|
catch (error) {
|
|
212
195
|
admissions.delete(request);
|
|
@@ -225,59 +208,38 @@ export function connectProvider(get, options = {}) {
|
|
|
225
208
|
const { api, target, connection } = admitted;
|
|
226
209
|
if (!request.sessionId)
|
|
227
210
|
throw new NeedError('PROJECT_BINDING_REQUIRED', '我需要当前会话和已选择的工作区,请使用原生工作区入口', 'DSH会话与原生工作区');
|
|
228
|
-
const host =
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
return local;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
if (!host && agent.loginProvider === 'claude' && options.local) {
|
|
239
|
-
const runtime = await options.local(request, api, connection, target);
|
|
240
|
-
if (runtime)
|
|
241
|
-
return runtime;
|
|
242
|
-
}
|
|
243
|
-
// A local source must have been handled by its employee provider above.
|
|
244
|
-
// Host execution requires an explicit host location; no name-based resolver.
|
|
245
|
-
if (!host)
|
|
246
|
-
throw new NeedError('WORKSPACE_LOCATION_REQUIRED', '请选择文件夹来源;改变来源后请新建会话。', '已选择的本机或主机文件夹');
|
|
247
|
-
const resolved = await host.resolve();
|
|
248
|
-
const project = resolved.project;
|
|
249
|
-
if (request.cwd && request.cwd !== project.path || request.bindingId && request.bindingId !== resolved.bindingId || request.workspaceId && request.workspaceId !== resolved.workspaceId)
|
|
250
|
-
throw new NeedError(host ? 'PROJECT_BINDING_INVALID' : 'SESSION_CONTEXT_MISMATCH', host ? '文件来源已改,请新建会话继续;原会话历史仍可查看。' : '原会话的工作区绑定已改变,请新建会话', '原工作区绑定');
|
|
251
|
-
const details = await options.details?.(request, api, connection, target);
|
|
252
|
-
try {
|
|
253
|
-
if (!options.open)
|
|
254
|
-
throw new NeedError('RUNTIME_REQUIRED', '我需要主机运行适配器', '主机broker适配');
|
|
255
|
-
const runtime = await options.open(resolved.target, project, { ...request, ...(host ? { signal: host.signal } : {}), agent, bindingId: resolved.bindingId, workspaceId: resolved.workspaceId, bindingRevision: resolved.bindingRevision, detailChannel: details });
|
|
256
|
-
try {
|
|
257
|
-
host?.observeRuntime(runtime);
|
|
258
|
-
}
|
|
259
|
-
catch (error) {
|
|
260
|
-
void runtime.dispose?.().catch(() => console.warn('HOST_CLEANUP_FAILED'));
|
|
261
|
-
throw error;
|
|
262
|
-
}
|
|
263
|
-
if (host)
|
|
264
|
-
retainHostScope(runtime, host);
|
|
265
|
-
runtime.hostWorkspace = host && 'directory' in resolved ? Object.freeze({ cwd: resolved.directory.path }) : undefined;
|
|
266
|
-
// The transport can outlive a turn; presentation belongs only to this invocation.
|
|
267
|
-
runtime.details = details ?? runtime.details;
|
|
268
|
-
runtime.binding = { bindingId: resolved.bindingId, workspaceId: resolved.workspaceId, revision: resolved.bindingRevision };
|
|
269
|
-
runtime.connection = { ...connection };
|
|
270
|
-
runtime.loginRequired = () => offerLogin(api, request, get, agent);
|
|
271
|
-
return runtime;
|
|
272
|
-
}
|
|
273
|
-
catch (error) {
|
|
274
|
-
throw error;
|
|
211
|
+
const host = request.workspaceSource?.source === 'host';
|
|
212
|
+
if (!host && options.employee) {
|
|
213
|
+
employee ??= import("./employee-provider.js").then(module => module.createEmployeeProvider(get, options, agent));
|
|
214
|
+
const local = await (await employee)({ api, target, connection, request });
|
|
215
|
+
if (local) {
|
|
216
|
+
local.loginRequired = () => offerLogin(api, request, get, agent);
|
|
217
|
+
return local;
|
|
275
218
|
}
|
|
276
219
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
220
|
+
if (!host && agent.loginProvider === 'claude' && options.local) {
|
|
221
|
+
const runtime = await options.local(request, api, connection, target);
|
|
222
|
+
if (runtime)
|
|
223
|
+
return runtime;
|
|
280
224
|
}
|
|
225
|
+
// A local source must have been handled by its employee provider above.
|
|
226
|
+
// Host execution requires an explicit host location; no name-based resolver.
|
|
227
|
+
if (!host)
|
|
228
|
+
throw new NeedError('WORKSPACE_LOCATION_REQUIRED', '请选择文件夹来源;改变来源后请新建会话。', '已选择的本机或主机文件夹');
|
|
229
|
+
const resolved = await resolveHostWorkspace(api, request, agent.loginProvider);
|
|
230
|
+
const project = resolved.project;
|
|
231
|
+
const details = await options.details?.(request, api, connection, target);
|
|
232
|
+
if (!options.open)
|
|
233
|
+
throw new NeedError('RUNTIME_REQUIRED', '我需要主机运行适配器', '主机broker适配');
|
|
234
|
+
const runtime = await options.open(resolved.target, project, { ...request, agent, bindingId: undefined, workspaceId: undefined, bindingRevision: undefined, detailChannel: details });
|
|
235
|
+
runtime.hostSource = Object.freeze({ ...request.workspaceSource });
|
|
236
|
+
runtime.hostWorkspace = Object.freeze({ cwd: project.path });
|
|
237
|
+
// The transport can outlive a turn; presentation belongs only to this invocation.
|
|
238
|
+
runtime.details = details ?? runtime.details;
|
|
239
|
+
runtime.binding = undefined;
|
|
240
|
+
runtime.connection = { ...connection };
|
|
241
|
+
runtime.loginRequired = () => offerLogin(api, request, get, agent);
|
|
242
|
+
return runtime;
|
|
281
243
|
};
|
|
282
244
|
resolve.preflight = preflight;
|
|
283
245
|
return resolve;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import { type SessionSource } from './host-workspace.ts';
|
|
3
|
+
import type { ConnectApi, RuntimeRequest } from './provider.ts';
|
|
4
|
+
export declare const sessionSourceSpec: {
|
|
5
|
+
name: string;
|
|
6
|
+
version: number;
|
|
7
|
+
tables: {
|
|
8
|
+
sessions: import("@deepseek-ai/dsh-storage-domain").DomainTableSpec<string, {
|
|
9
|
+
source: "host";
|
|
10
|
+
host: string;
|
|
11
|
+
path: string;
|
|
12
|
+
} | {
|
|
13
|
+
source: "local";
|
|
14
|
+
}>;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
/** Official domain metadata, keyed by native session ID. No L files, local path probes or host identity hashes. */
|
|
18
|
+
export declare function createSessionSources(ctx: Context, getApi: () => ConnectApi | undefined): (request: RuntimeRequest, api: ConnectApi) => Promise<SessionSource | undefined>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { defineDomain, domainTable } from '@deepseek-ai/dsh-storage-domain';
|
|
2
|
+
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { absoluteHostPath, workspaceSessionSource } from "./host-workspace.js";
|
|
5
|
+
import { NeedError } from "./remote.js";
|
|
6
|
+
const sourceSchema = z.discriminatedUnion('source', [
|
|
7
|
+
z.object({ source: z.literal('host'), host: z.string().min(1), path: z.string().refine(absoluteHostPath) }).strict(),
|
|
8
|
+
z.object({ source: z.literal('local') }).strict(),
|
|
9
|
+
]);
|
|
10
|
+
export const sessionSourceSpec = defineDomain({ name: 'ai_workbench_remote_sources', version: 1, tables: { sessions: domainTable(sourceSchema) } });
|
|
11
|
+
const storageNeed = () => new NeedError('WORKSPACE_SOURCE_STORAGE_FAILED', '这条会话的文件来源未能保存,本轮工作区准备失败;请恢复后在原会话重试。', '官方会话来源存储');
|
|
12
|
+
/** Official domain metadata, keyed by native session ID. No L files, local path probes or host identity hashes. */
|
|
13
|
+
export function createSessionSources(ctx, getApi) {
|
|
14
|
+
let domain;
|
|
15
|
+
const pending = new Map();
|
|
16
|
+
const captured = new WeakMap();
|
|
17
|
+
const open = () => {
|
|
18
|
+
if (!domain) {
|
|
19
|
+
const facility = ctx.get('storageDomain');
|
|
20
|
+
if (!facility)
|
|
21
|
+
throw storageNeed();
|
|
22
|
+
domain = facility.open(sessionSourceSpec).catch(error => { domain = undefined; throw error; });
|
|
23
|
+
}
|
|
24
|
+
return domain;
|
|
25
|
+
};
|
|
26
|
+
ctx.effect(() => async () => { if (domain)
|
|
27
|
+
await (await domain).close(); });
|
|
28
|
+
function candidate(session, api, request) {
|
|
29
|
+
if (request?.hostSource)
|
|
30
|
+
return sourceSchema.parse(request.hostSource);
|
|
31
|
+
const registry = ctx.get('workspaceRegistry');
|
|
32
|
+
// Creation can precede workspace membership publication. The official creation cwd identifies its workspace.
|
|
33
|
+
const rows = registry?.list().filter(row => request?.workspaceId ? row.id === request.workspaceId : row.sessionIds.includes(session.id) || row.path === session.header.cwd) ?? [];
|
|
34
|
+
if (rows.length !== 1)
|
|
35
|
+
return;
|
|
36
|
+
return workspaceSessionSource(api, rows[0].id, request);
|
|
37
|
+
}
|
|
38
|
+
function save(session, api, request) {
|
|
39
|
+
const id = session.id, existing = pending.get(id);
|
|
40
|
+
if (existing)
|
|
41
|
+
return existing;
|
|
42
|
+
const work = async () => {
|
|
43
|
+
const table = (await open()).table('sessions'), stored = table.get(id);
|
|
44
|
+
if (stored) {
|
|
45
|
+
const source = Object.freeze({ ...stored });
|
|
46
|
+
captured.set(session, source);
|
|
47
|
+
return source;
|
|
48
|
+
}
|
|
49
|
+
const chosen = captured.get(session) ?? candidate(session, api, request);
|
|
50
|
+
if (!chosen)
|
|
51
|
+
return;
|
|
52
|
+
captured.set(session, chosen);
|
|
53
|
+
await table.put(id, chosen);
|
|
54
|
+
return chosen;
|
|
55
|
+
};
|
|
56
|
+
const result = work().catch(error => { if (error instanceof NeedError)
|
|
57
|
+
throw error; throw Object.assign(storageNeed(), { cause: error }); });
|
|
58
|
+
pending.set(id, result);
|
|
59
|
+
void result.finally(() => pending.delete(id)).catch(() => { });
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
ctx.on('session/created', session => {
|
|
63
|
+
// Restored sessions migrate from their recorded replay/workspace at first turn.
|
|
64
|
+
if (session.firstLiveSeq !== 0)
|
|
65
|
+
return;
|
|
66
|
+
const api = getApi();
|
|
67
|
+
if (!api)
|
|
68
|
+
return;
|
|
69
|
+
try {
|
|
70
|
+
const source = candidate(session, api);
|
|
71
|
+
if (source)
|
|
72
|
+
captured.set(session, source);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
ctx.logger('remote-agent').warn('创建时文件来源暂不可读;发送时将重试');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (!captured.has(session))
|
|
79
|
+
return;
|
|
80
|
+
void save(session, api).catch(() => ctx.logger('remote-agent').warn('会话文件来源暂未保存;发送时将重试'));
|
|
81
|
+
});
|
|
82
|
+
return async (request, api) => {
|
|
83
|
+
if (!request.sessionId || api.getContractInfo?.().workspaceLocations !== 1 && !request.hostSource)
|
|
84
|
+
return;
|
|
85
|
+
const session = ctx.get('sessions')?.get(SessionId(request.sessionId));
|
|
86
|
+
if (!session)
|
|
87
|
+
throw storageNeed();
|
|
88
|
+
return save(session, api, request);
|
|
89
|
+
};
|
|
90
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-workbench/remote-agent",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.26",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"zod": "4.6.7"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@ai-workbench/connect": "file:test/fixtures/ai-workbench-connect-0.7.
|
|
39
|
+
"@ai-workbench/connect": "file:test/fixtures/ai-workbench-connect-0.7.29.tgz",
|
|
40
40
|
"@ai-workbench/network": "file:test/fixtures/ai-workbench-network-0.4.9.tgz",
|
|
41
41
|
"@deepseek-ai/cordis": "4.0.4",
|
|
42
42
|
"@deepseek-ai/cordis-plugin-group": "1.0.4",
|
|
@@ -63,10 +63,11 @@
|
|
|
63
63
|
"react-dom": "18.3.1",
|
|
64
64
|
"typescript": "5.9.3",
|
|
65
65
|
"zustand": "4.4.7",
|
|
66
|
-
"@deepseek-ai/dsh-sandbox-policy": "0.2.0-rc.2"
|
|
66
|
+
"@deepseek-ai/dsh-sandbox-policy": "0.2.0-rc.2",
|
|
67
|
+
"@deepseek-ai/dsh-storage-domain": "0.2.0-rc.2"
|
|
67
68
|
},
|
|
68
69
|
"peerDependencies": {
|
|
69
|
-
"@ai-workbench/connect": "~0.7.
|
|
70
|
+
"@ai-workbench/connect": "~0.7.29",
|
|
70
71
|
"@ai-workbench/network": "~0.4.9",
|
|
71
72
|
"@deepseek-ai/cordis": "4.0.4",
|
|
72
73
|
"@deepseek-ai/dsh-agent": "0.2.0-rc.2",
|
|
@@ -76,7 +77,8 @@
|
|
|
76
77
|
"@deepseek-ai/dsh-typert-protocol": "0.2.0-rc.2",
|
|
77
78
|
"@deepseek-ai/dsh-user-approval": "0.2.0-rc.2",
|
|
78
79
|
"@deepseek-ai/schemastery": "3.18.4",
|
|
79
|
-
"@deepseek-ai/dsh-sandbox-policy": "0.2.0-rc.2"
|
|
80
|
+
"@deepseek-ai/dsh-sandbox-policy": "0.2.0-rc.2",
|
|
81
|
+
"@deepseek-ai/dsh-storage-domain": "0.2.0-rc.2"
|
|
80
82
|
},
|
|
81
83
|
"dsh": {
|
|
82
84
|
"bundle": {
|