@ai-workbench/remote-agent 0.8.19 → 0.8.23
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 +27 -134
- package/dist/acp.d.ts +23 -16
- package/dist/acp.js +209 -226
- package/dist/agents.d.ts +0 -1
- package/dist/agents.js +2 -2
- package/dist/broker-runtime.d.ts +3 -5
- package/dist/broker-runtime.js +45 -342
- package/dist/broker.d.ts +6 -31
- package/dist/broker.js +42 -113
- package/dist/catalog-ssh.d.ts +2 -31
- package/dist/catalog-ssh.js +9 -39
- package/dist/claude-local.d.ts +1 -1
- package/dist/claude-local.js +41 -105
- package/dist/client.js +34 -126
- package/dist/close-diagnostics.d.ts +6 -20
- package/dist/close-diagnostics.js +13 -57
- package/dist/connect-contract.d.ts +4 -8
- package/dist/conversation-details.d.ts +0 -29
- package/dist/conversation-details.js +8 -99
- package/dist/employee-endpoint.d.ts +1 -1
- package/dist/employee-endpoint.js +21 -19
- package/dist/employee-exec.d.ts +4 -3
- package/dist/employee-exec.js +25 -13
- package/dist/employee-info.d.ts +7 -1
- package/dist/employee-info.js +24 -3
- package/dist/employee-location.js +3 -4
- package/dist/employee-mcp.js +44 -69
- package/dist/employee-native.d.ts +1 -3
- package/dist/employee-native.js +6 -27
- package/dist/employee-presentation.d.ts +0 -5
- package/dist/employee-presentation.js +0 -15
- package/dist/employee-provider.d.ts +3 -11
- package/dist/employee-provider.js +69 -205
- package/dist/employee-reasons.js +1 -5
- package/dist/employee-shell.d.ts +2 -1
- package/dist/employee-shell.js +10 -13
- package/dist/employee-ssh.d.ts +1 -3
- package/dist/employee-ssh.js +13 -16
- package/dist/employee-tools.d.ts +5 -5
- package/dist/failure-evidence.d.ts +2 -10
- package/dist/failure-evidence.js +12 -61
- package/dist/host-workspace.d.ts +4 -2
- package/dist/host-workspace.js +6 -2
- package/dist/index.d.ts +13 -43
- package/dist/index.js +192 -484
- package/dist/input.js +2 -3
- package/dist/local-user-turn.js +5 -6
- package/dist/mapping.d.ts +5 -3
- package/dist/mapping.js +31 -36
- package/dist/model-catalog.d.ts +2 -41
- package/dist/model-catalog.js +24 -282
- package/dist/model-configuration.d.ts +0 -3
- package/dist/model-configuration.js +8 -47
- package/dist/model-presentation.d.ts +1 -0
- package/dist/model-presentation.js +2 -2
- package/dist/permissions.d.ts +15 -0
- package/dist/permissions.js +30 -0
- package/dist/preflight-diagnostic.js +11 -22
- package/dist/preflight-payload.d.ts +1 -1
- package/dist/preflight-payload.js +2 -2
- package/dist/presentation.js +21 -41
- package/dist/provider.d.ts +12 -34
- package/dist/provider.js +17 -92
- package/dist/readiness.js +1 -7
- package/dist/remote-details.d.ts +41 -17
- package/dist/remote-details.js +84 -381
- package/dist/workspace-source.d.ts +1 -25
- package/dist/workspace-source.js +1 -138
- package/package.json +11 -12
- package/dist/catalog-diagnostics.d.ts +0 -16
- package/dist/catalog-diagnostics.js +0 -108
- package/dist/host-layout.d.ts +0 -8
- package/dist/host-layout.js +0 -9
- package/dist/job.d.ts +0 -17
- package/dist/job.js +0 -57
- package/dist/model-defaults.d.ts +0 -29
- package/dist/model-defaults.js +0 -62
- package/dist/oneshot.d.ts +0 -13
- package/dist/oneshot.js +0 -78
- package/dist/protocol-boundary.d.ts +0 -13
- package/dist/protocol-boundary.js +0 -49
- package/dist/provider-locale.d.ts +0 -19
- package/dist/provider-locale.js +0 -73
- package/dist/recovery-checkpoint.d.ts +0 -6
- package/dist/recovery-checkpoint.js +0 -71
- package/dist/recovery.d.ts +0 -51
- package/dist/recovery.js +0 -61
- package/dist/sdk-ssh-wrapper.d.ts +0 -2
- package/dist/sdk-ssh-wrapper.js +0 -34
- package/dist/stage-timing.d.ts +0 -31
- package/dist/stage-timing.js +0 -61
- package/dist/workspace-notice.d.ts +0 -47
- package/dist/workspace-notice.js +0 -273
- package/dist/workspace-source-confirmation.d.ts +0 -4
- package/dist/workspace-source-confirmation.js +0 -12
|
@@ -7,22 +7,22 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
7
7
|
return path;
|
|
8
8
|
};
|
|
9
9
|
import { connectNeedError } from "./connect-need.js";
|
|
10
|
-
import {
|
|
11
|
-
import { CloseDiagnostics,
|
|
10
|
+
import { EMPLOYEE_EXECUTION_NOTICE } from "./employee-presentation.js";
|
|
11
|
+
import { CloseDiagnostics, withFirstFailureEvidence, closeCode } from "./close-diagnostics.js";
|
|
12
12
|
import { createHash } from 'node:crypto';
|
|
13
13
|
import { admitEmployeeLocation } from "./employee-location.js";
|
|
14
14
|
import { EmployeeExecution } from "./employee-exec.js";
|
|
15
15
|
import { createEmployeeSshTransport } from "./employee-ssh.js";
|
|
16
16
|
import { sshIo } from "./broker.js";
|
|
17
17
|
import { NeedError } from "./remote.js";
|
|
18
|
-
const fail = (code) => new NeedError(code,
|
|
18
|
+
const fail = (code) => new NeedError(code, '本机工作区的执行连接无法继续,本轮没有改用主机执行;恢复连接后可在原会话继续。', '当前 L 工作区绑定、N/S 员工执行端点与支持本机执行的会话适配器');
|
|
19
19
|
const errorCode = (error) => error instanceof NeedError ? error.code : 'EMPLOYEE_EXEC_UNKNOWN';
|
|
20
20
|
const asNeed = (error) => error instanceof NeedError ? error : fail('EMPLOYEE_EXEC_UNKNOWN');
|
|
21
21
|
const digest = (value) => createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
22
22
|
const absolute = (value) => typeof value === 'string' && value.startsWith('/') && !value.includes('\0');
|
|
23
|
-
const localApiCurrent = (api) => { const c = api.getContractInfo?.(); return c?.workspaceLocations === 1 && c.localWorkspaceContext === 1 &&
|
|
24
|
-
/**
|
|
25
|
-
export async function
|
|
23
|
+
const localApiCurrent = (api) => { const c = api.getContractInfo?.(); return c?.workspaceLocations === 1 && c.localWorkspaceContext === 1 && typeof api.getWorkspaceLocationState === 'function' && typeof api.getLocalWorkspaceContext === 'function'; };
|
|
24
|
+
/** Inspect the current local workspace once at turn start, before host readiness. */
|
|
25
|
+
export async function inspectEmployeeWorkspace(get, employee, agent, api, request) {
|
|
26
26
|
const info = api.getContractInfo?.();
|
|
27
27
|
if (info?.workspaceLocations !== 1)
|
|
28
28
|
return;
|
|
@@ -46,7 +46,7 @@ export async function acceptEmployeeUserTurn(get, employee, agent, api, request)
|
|
|
46
46
|
throw fail('EMPLOYEE_CONTEXT_CHANGED');
|
|
47
47
|
if (row.source !== 'local')
|
|
48
48
|
return;
|
|
49
|
-
if (agent.id !== 'codex-remote' || agent.loginProvider !== 'codex' || state.phase !== 'ready' || row.status !== 'selected' || !row.bindingId || info.localWorkspaceContext !== 1 ||
|
|
49
|
+
if (agent.id !== 'codex-remote' || agent.loginProvider !== 'codex' || state.phase !== 'ready' || row.status !== 'selected' || !row.bindingId || info.localWorkspaceContext !== 1 || typeof api.getLocalWorkspaceContext !== 'function')
|
|
50
50
|
throw fail('DEPENDENCY_REQUIRED');
|
|
51
51
|
const connection = api.getConnectionState();
|
|
52
52
|
const stable = () => { sourceStable(); const now = api.getConnectionState(), location = api.getWorkspaceLocationState(), matches = location.items.filter(x => x.workspaceId === workspace.id); if (get() !== api || now.phase !== 'connected' || now.instanceId !== connection.instanceId || now.epoch !== connection.epoch || location.phase !== 'ready' || location.instanceId !== connection.instanceId || location.epoch !== connection.epoch || matches.length !== 1 || matches[0].source !== 'local' || matches[0].status !== 'selected' || matches[0].bindingId !== row.bindingId)
|
|
@@ -59,40 +59,11 @@ export async function acceptEmployeeUserTurn(get, employee, agent, api, request)
|
|
|
59
59
|
const c = context.value, d = { ...c.directory };
|
|
60
60
|
if (c.version !== 1 || c.source !== 'local' || c.instanceId !== connection.instanceId || c.epoch !== connection.epoch || c.workspaceId !== workspace.id || c.bindingId !== row.bindingId || !absolute(d?.path) || d.path !== workspace.path || ![d.device, d.inode].every(n => Number.isSafeInteger(n) && n >= 0))
|
|
61
61
|
throw fail('EMPLOYEE_CONTEXT_CHANGED');
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
throw connectNeedError(response.error);
|
|
68
|
-
stable();
|
|
69
|
-
const a = response.value;
|
|
70
|
-
if (!a || a.version !== 1 || typeof a.acceptanceId !== 'string' || !a.acceptanceId.trim() || a.instanceId !== connection.instanceId || a.epoch !== connection.epoch || a.sessionId !== request.sessionId || a.provider !== agent.loginProvider || a.workspaceId !== workspace.id || a.bindingId !== row.bindingId || a.source !== 'local' || a.directory?.path !== d.path || a.directory?.device !== d.device || a.directory?.inode !== d.inode || a.userTurn?.turn !== expected.turn || a.userTurn?.userMessageId !== expected.userMessageId || ![a.userTurn?.startSequence, a.userTurn?.userMessageSequence].every(n => Number.isSafeInteger(n) && n >= 0) || a.userTurn.userMessageSequence <= a.userTurn.startSequence)
|
|
71
|
-
throw fail('LOCAL_USER_TURN_ACCEPTANCE_INVALID');
|
|
72
|
-
const accepted = Object.freeze({ ...a, directory: Object.freeze({ ...a.directory }), userTurn: Object.freeze({ ...a.userTurn }) });
|
|
73
|
-
request.localAdmissionCheck = () => { sourceStable(); const c = api.getConnectionState(); if (c.phase !== 'connected' || c.instanceId !== accepted.instanceId || c.epoch !== accepted.epoch)
|
|
74
|
-
throw fail('EMPLOYEE_CONTEXT_CHANGED'); const turn = employee.currentUserTurn(request); if (turn.turn !== accepted.userTurn.turn || turn.userMessageId !== accepted.userTurn.userMessageId)
|
|
75
|
-
throw new NeedError('LOCAL_USER_TURN_CHANGED', '本次输入已变化,本轮已停止。', '当前官方真实员工输入'); };
|
|
76
|
-
return accepted;
|
|
77
|
-
}
|
|
78
|
-
/** Recheck the admitted read-only facts after readiness awaits, before the catalog command. */
|
|
79
|
-
export async function validateEmployeeUserTurn(get, employee, api, request, connection) {
|
|
80
|
-
const a = request.localAcceptance;
|
|
81
|
-
if (!a)
|
|
82
|
-
return;
|
|
83
|
-
const stable = () => { if (get() !== api || !localApiCurrent(api))
|
|
84
|
-
throw fail('EMPLOYEE_CONTEXT_CHANGED'); const c = api.getConnectionState(), state = api.getWorkspaceLocationState(), rows = state.items.filter(x => x.workspaceId === a.workspaceId); if (c.phase !== 'connected' || c.instanceId !== a.instanceId || c.epoch !== a.epoch || connection.instanceId !== a.instanceId || connection.epoch !== a.epoch || state.phase !== 'ready' || state.instanceId !== a.instanceId || state.epoch !== a.epoch || rows.length !== 1 || rows[0].source !== 'local' || rows[0].status !== 'selected' || rows[0].bindingId !== a.bindingId)
|
|
85
|
-
throw fail('EMPLOYEE_CONTEXT_CHANGED'); request.signal?.throwIfAborted(); };
|
|
86
|
-
stable();
|
|
87
|
-
const c = await api.getLocalWorkspaceContext({ instanceId: a.instanceId, epoch: a.epoch, workspaceId: a.workspaceId, expectedBindingId: a.bindingId });
|
|
88
|
-
stable();
|
|
89
|
-
if (!c.ok)
|
|
90
|
-
throw connectNeedError(c.error);
|
|
91
|
-
if (c.value.version !== 1 || c.value.source !== 'local' || c.value.instanceId !== a.instanceId || c.value.epoch !== a.epoch || c.value.workspaceId !== a.workspaceId || c.value.bindingId !== a.bindingId || c.value.directory.path !== a.directory.path || c.value.directory.device !== a.directory.device || c.value.directory.inode !== a.directory.inode)
|
|
92
|
-
throw fail('EMPLOYEE_CONTEXT_CHANGED');
|
|
93
|
-
const turn = employee.currentUserTurn(request);
|
|
94
|
-
if (turn.turn !== a.userTurn.turn || turn.userMessageId !== a.userTurn.userMessageId)
|
|
95
|
-
throw new NeedError('LOCAL_USER_TURN_CHANGED', '本次输入已变化,本轮已停止。', '当前官方真实员工输入');
|
|
62
|
+
// L0.7.27 provides a read-only context, not a per-turn context ledger.
|
|
63
|
+
const contextSnapshot = Object.freeze({ ...c, directory: Object.freeze(d) });
|
|
64
|
+
request.localAdmissionCheck = () => { sourceStable(); const now = api.getConnectionState(); if (now.phase !== 'connected' || now.instanceId !== c.instanceId || now.epoch !== c.epoch)
|
|
65
|
+
throw fail('EMPLOYEE_CONTEXT_CHANGED'); };
|
|
66
|
+
return contextSnapshot;
|
|
96
67
|
}
|
|
97
68
|
/** Source admission is separate from L's legacy host project resolver. */
|
|
98
69
|
export function createEmployeeProvider(get, options, agent) {
|
|
@@ -100,15 +71,11 @@ export function createEmployeeProvider(get, options, agent) {
|
|
|
100
71
|
return async (input) => {
|
|
101
72
|
const { api, target, connection, request } = input, info = api.getContractInfo?.(), sessionId = request.sessionId;
|
|
102
73
|
let prior = live.get(sessionId);
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
prior =
|
|
108
|
-
}
|
|
109
|
-
if (prior && (prior.current.api !== api || info?.workspaceLocations !== 1 || info?.localWorkspaceContext !== 1)) {
|
|
110
|
-
await prior.dispose(fail('EMPLOYEE_CONTEXT_CHANGED'), { layer: 'endpoint', initiator: 'provider.scope-change', reason: 'EMPLOYEE_CONTEXT_CHANGED', action: 'dispose' });
|
|
111
|
-
throw fail('EMPLOYEE_CONTEXT_CHANGED');
|
|
74
|
+
// Each round owns a fresh employee channel. Old cleanup never admits or blocks it.
|
|
75
|
+
if (prior) {
|
|
76
|
+
live.delete(sessionId);
|
|
77
|
+
void prior.dispose(undefined, { layer: 'runtime', initiator: 'provider.next-round', reason: 'NEXT_ROUND', action: 'dispose' }).catch(() => console.warn('EMPLOYEE_CLEANUP_FAILED'));
|
|
78
|
+
prior = undefined;
|
|
112
79
|
}
|
|
113
80
|
if (info?.workspaceLocations !== 1 && info?.localWorkspaceContext !== 1)
|
|
114
81
|
return;
|
|
@@ -116,50 +83,56 @@ export function createEmployeeProvider(get, options, agent) {
|
|
|
116
83
|
throw fail('DEPENDENCY_REQUIRED');
|
|
117
84
|
const employee = options.employee;
|
|
118
85
|
const workspace = await employee.workspace(request);
|
|
119
|
-
await input.stable(request.signal);
|
|
120
86
|
if (!workspace.id || !absolute(workspace.path))
|
|
121
87
|
throw fail('EMPLOYEE_CONTEXT_CHANGED');
|
|
122
88
|
const state = api.getWorkspaceLocationState(), rows = state.items.filter(row => row.workspaceId === workspace.id), row = rows[0];
|
|
123
|
-
if (state.phase !== 'ready' || state.instanceId !== connection.instanceId || state.epoch !== connection.epoch || rows.length !== 1 || row?.status !== 'selected' || !row.bindingId || !['local', 'host'
|
|
89
|
+
if (state.phase !== 'ready' || state.instanceId !== connection.instanceId || state.epoch !== connection.epoch || rows.length !== 1 || row?.status !== 'selected' || !row.bindingId || !['local', 'host'].includes(row.source ?? ''))
|
|
124
90
|
throw fail('EMPLOYEE_CONTEXT_CHANGED');
|
|
125
91
|
if (row.source !== 'local') {
|
|
126
|
-
if (
|
|
127
|
-
await prior.dispose(fail('EMPLOYEE_CONTEXT_CHANGED'), { layer: 'endpoint', initiator: 'provider.scope-change', reason: 'EMPLOYEE_CONTEXT_CHANGED', action: 'dispose' });
|
|
128
|
-
if (request.localAcceptance)
|
|
92
|
+
if (request.localContext)
|
|
129
93
|
throw fail('EMPLOYEE_CONTEXT_CHANGED');
|
|
130
94
|
return;
|
|
131
95
|
}
|
|
132
96
|
let details, closeDiagnostics;
|
|
97
|
+
let entry, candidate, opening;
|
|
98
|
+
const disposers = [], lifetime = new AbortController();
|
|
99
|
+
const roundSignal = AbortSignal.any([lifetime.signal, ...request.signal ? [request.signal] : []]);
|
|
133
100
|
try {
|
|
134
|
-
const
|
|
135
|
-
if (!
|
|
101
|
+
const context = request.localContext;
|
|
102
|
+
if (!context || context.instanceId !== connection.instanceId || context.epoch !== connection.epoch || context.workspaceId !== workspace.id || context.bindingId !== row.bindingId)
|
|
136
103
|
throw fail('LOCAL_USER_TURN_UNCONFIRMED');
|
|
104
|
+
if (typeof api.onWorkspaceLocationState !== 'function' || typeof api.onConnectionState !== 'function')
|
|
105
|
+
throw fail('DEPENDENCY_REQUIRED');
|
|
106
|
+
const network = employee.getNetwork();
|
|
107
|
+
if (!network || typeof network.prepareEmployeeEndpoint !== 'function' || typeof network.onState !== 'function')
|
|
108
|
+
throw fail('DEPENDENCY_REQUIRED');
|
|
109
|
+
if (network.getState().phase !== 'online')
|
|
110
|
+
throw fail('EMPLOYEE_ENDPOINT_UNAVAILABLE');
|
|
111
|
+
const revoke = (reason, initiator, changes) => { lifetime.abort(reason); if (candidate)
|
|
112
|
+
void candidate.dispose(reason, { layer: 'endpoint', initiator, reason: reason.code, action: 'invalidate', changes }).catch(() => { }); };
|
|
113
|
+
disposers.push(api.onWorkspaceLocationState(next => {
|
|
114
|
+
const matches = next.items.filter(item => item.workspaceId === context.workspaceId);
|
|
115
|
+
if (get() !== api || next.phase !== 'ready' || next.instanceId !== connection.instanceId || next.epoch !== connection.epoch || matches.length !== 1 || matches[0].bindingId !== context.bindingId || matches[0].source !== 'local' || matches[0].status !== 'selected')
|
|
116
|
+
revoke(fail('EMPLOYEE_CONTEXT_CHANGED'), 'endpoint.workspace-change', [{ field: 'workspace.binding-id', before: context.bindingId, after: matches[0]?.bindingId ?? null }, { field: 'workspace.source', before: 'local', after: matches[0]?.source ?? null }, { field: 'workspace.status', before: 'selected', after: matches[0]?.status ?? null }, { field: 'workspace.match-count', before: 1, after: matches.length }]);
|
|
117
|
+
}), api.onConnectionState(next => { if (get() !== api || next.phase !== 'connected' || next.instanceId !== connection.instanceId || next.epoch !== connection.epoch)
|
|
118
|
+
revoke(fail('CONNECTION_LOST'), 'endpoint.connection-change', [{ field: 'connection.instance-id', before: connection.instanceId, after: next.instanceId }, { field: 'connection.epoch', before: connection.epoch, after: next.epoch }, { field: 'connection.phase', before: 'connected', after: next.phase }]); }), network.onState(event => { if (event.kind === 'lost' || employee.getNetwork() !== network)
|
|
119
|
+
revoke(fail('CONNECTION_LOST'), 'endpoint.network-loss', [{ field: 'network.phase', before: null, after: event.state.phase }]); }));
|
|
137
120
|
details = await options.details?.(request, api, connection, target);
|
|
138
|
-
if (details)
|
|
139
|
-
request.timing?.bind(detail => details.record(detail));
|
|
140
121
|
const sink = details;
|
|
141
|
-
closeDiagnostics =
|
|
122
|
+
closeDiagnostics = new CloseDiagnostics(event => sink?.record(event));
|
|
142
123
|
closeDiagnostics.bind(event => sink?.record(event));
|
|
143
124
|
request.closeDiagnostics = closeDiagnostics;
|
|
144
|
-
const unsupported =
|
|
125
|
+
const unsupported = agent.loginProvider !== 'codex';
|
|
145
126
|
if (unsupported)
|
|
146
127
|
throw fail('EMPLOYEE_OPERATION_UNSUPPORTED');
|
|
147
128
|
if (!Number.isSafeInteger(request.acpMaxMessageBytes) || request.acpMaxMessageBytes < 1)
|
|
148
129
|
throw fail('SETTINGS_REQUIRED');
|
|
149
|
-
|
|
150
|
-
throw fail('DEPENDENCY_REQUIRED');
|
|
151
|
-
const network = employee.getNetwork();
|
|
152
|
-
if (!network || typeof network.prepareEmployeeEndpoint !== 'function' || typeof network.onState !== 'function')
|
|
153
|
-
throw fail('DEPENDENCY_REQUIRED');
|
|
154
|
-
if (prior && prior.network !== network) {
|
|
155
|
-
await prior.dispose(fail('EMPLOYEE_CONTEXT_CHANGED'), { layer: 'endpoint', initiator: 'provider.scope-change', reason: 'EMPLOYEE_CONTEXT_CHANGED', action: 'dispose' });
|
|
156
|
-
throw fail('EMPLOYEE_CONTEXT_CHANGED');
|
|
157
|
-
}
|
|
158
|
-
const admitted = await admitEmployeeLocation({ api: api, network, workspace, connection, expectedLocal: { instanceId: acceptance.instanceId, epoch: acceptance.epoch, workspaceId: acceptance.workspaceId, bindingId: acceptance.bindingId, directory: { ...acceptance.directory } }, settingsPath: target.session.settingsPath, signal: request.signal });
|
|
130
|
+
const admitted = await admitEmployeeLocation({ api: api, network, workspace, connection, expectedLocal: { instanceId: context.instanceId, epoch: context.epoch, workspaceId: context.workspaceId, bindingId: context.bindingId, directory: { ...context.directory } }, settingsPath: target.session.settingsPath, signal: roundSignal });
|
|
159
131
|
if (request.cwd && request.cwd !== admitted.cwd || request.bindingId && request.bindingId !== admitted.bindingId || request.workspaceId && request.workspaceId !== admitted.workspaceId)
|
|
160
132
|
throw fail('SESSION_CONTEXT_MISMATCH');
|
|
161
133
|
const native = await employee.native();
|
|
162
|
-
|
|
134
|
+
roundSignal.throwIfAborted();
|
|
135
|
+
if (!absolute(native.path))
|
|
163
136
|
throw fail('EMPLOYEE_BINARY_REQUIRED');
|
|
164
137
|
// Only executable identity belongs in this scope. Diagnostic metadata and object key
|
|
165
138
|
// insertion order cannot revoke a live binding; every public identity field stays bound.
|
|
@@ -171,33 +144,9 @@ export function createEmployeeProvider(get, options, agent) {
|
|
|
171
144
|
local: { instanceId: local.instanceId, epoch: local.epoch, workspaceId: local.workspaceId, bindingId: local.bindingId, directory: { path: local.directory.path, device: local.directory.device, inode: local.directory.inode } },
|
|
172
145
|
endpoint: { deviceId: account.deviceId, endpointInstanceId: account.endpointInstanceId, executionContextId: account.executionContextId, remoteUser: account.remoteUser, remoteUid: account.remoteUid, remoteHome: account.remoteHome, endpointCwd: account.endpointCwd, endpointShell: account.endpointShell },
|
|
173
146
|
bridge: { protocolVersion: bridge.protocolVersion, command: bridge.command, settingsPath: bridge.settingsPath, bridgeInstanceId: bridge.bridgeInstanceId, generation: bridge.generation } },
|
|
174
|
-
native: { path: native.path
|
|
147
|
+
native: { path: native.path },
|
|
175
148
|
});
|
|
176
|
-
|
|
177
|
-
await prior.dispose(fail('EMPLOYEE_CONTEXT_CHANGED'), { layer: 'endpoint', initiator: 'provider.scope-change', reason: 'EMPLOYEE_CONTEXT_CHANGED', action: 'dispose' });
|
|
178
|
-
let entry = prior && prior.scope === scope && !prior.ended ? prior : undefined;
|
|
179
|
-
let candidate = entry;
|
|
180
|
-
// Admission still verifies the real directory and N generation. Per-RPC gates below
|
|
181
|
-
// reuse the subscribed connection/location/network facts, without another SSH.
|
|
182
|
-
await admitted.validate(request.signal ?? new AbortController().signal);
|
|
183
|
-
details?.record({ kind: 'session', severity: 'info', code: 'EMPLOYEE_EXECUTION_VALIDATION_POLICY', summary: '本机执行核验不可关闭:L接纳核对实际目录与N端点;同代执行通道仅在open时核对一次主机target与固定执行器。之后每条消息只核对内存中的连接、工作区绑定和已接纳输入,撤销沿现有连接/位置/N网络事件及执行SSH关闭处理;不重复SSH,不改用主机执行。' });
|
|
184
|
-
if (entry)
|
|
185
|
-
entry.current = input;
|
|
186
|
-
const base = async (signal) => {
|
|
187
|
-
signal.throwIfAborted();
|
|
188
|
-
if (get() !== api || employee.getNetwork() !== network)
|
|
189
|
-
throw fail('EMPLOYEE_CONTEXT_CHANGED');
|
|
190
|
-
if (candidate?.ended)
|
|
191
|
-
throw candidate.reason ?? fail('EMPLOYEE_EXEC_CLOSED');
|
|
192
|
-
const current = candidate?.current ?? input;
|
|
193
|
-
current.request.localAdmissionCheck?.();
|
|
194
|
-
await current.stable(signal);
|
|
195
|
-
if (get() !== api || employee.getNetwork() !== network)
|
|
196
|
-
throw fail('EMPLOYEE_CONTEXT_CHANGED');
|
|
197
|
-
current.request.localAdmissionCheck?.();
|
|
198
|
-
await current.stable(signal);
|
|
199
|
-
signal.throwIfAborted();
|
|
200
|
-
};
|
|
149
|
+
details?.record({ kind: 'session', severity: 'info', code: 'EMPLOYEE_EXECUTION_VALIDATION_POLICY', summary: '每轮开始核验本机所选目录、当前网络及端点,并重新prepare一次;本轮执行通道open时核验主机目标。之后不逐消息、审批或原生RPC重复核验。已有连接、位置、网络事件和通道关闭只终止本轮,不回退主机,下一轮重新核验。' });
|
|
201
150
|
let argv = employee.argv;
|
|
202
151
|
if (!argv && !employee.io) {
|
|
203
152
|
const name = '@ai-workbench/connect';
|
|
@@ -214,98 +163,33 @@ export function createEmployeeProvider(get, options, agent) {
|
|
|
214
163
|
void candidate.dispose(fail('EMPLOYEE_EXEC_UNKNOWN'), { layer: 'runtime', initiator: 'provider.child-close', reason: 'EMPLOYEE_EXEC_UNKNOWN', action: 'dispose' }).catch(() => { }); });
|
|
215
164
|
return child;
|
|
216
165
|
} };
|
|
217
|
-
const ssh = { closeDiagnostics, target, endpoint: admitted.endpoint, employeeBinary: native.path,
|
|
218
|
-
const validate = async (signal) => { try {
|
|
219
|
-
await base(signal);
|
|
220
|
-
}
|
|
221
|
-
catch (error) {
|
|
222
|
-
if (candidate && errorCode(error) !== 'ABORTED')
|
|
223
|
-
void candidate.dispose(asNeed(error), { layer: 'endpoint', initiator: 'provider.validation-failure', reason: closeCode(error), action: 'invalidate' }).catch(cleanup => closeDiagnostics.record({ layer: 'runtime', initiator: 'provider.validation-cleanup', reason: closeCode(cleanup, 'REMOTE_CLEANUP_UNCONFIRMED'), action: 'failure' }));
|
|
224
|
-
throw error;
|
|
225
|
-
} };
|
|
226
|
-
await validate(request.signal ?? new AbortController().signal);
|
|
166
|
+
const ssh = { closeDiagnostics, target, endpoint: admitted.endpoint, employeeBinary: native.path, acpMaxMessageBytes: request.acpMaxMessageBytes, argv: argv ?? (() => []), io: scopedIo };
|
|
227
167
|
if (!options.open)
|
|
228
168
|
throw fail('RUNTIME_REQUIRED');
|
|
229
169
|
// Host cwd is only the protected agent bootstrap/config directory. Native tools use admitted.cwd.
|
|
230
|
-
const host = await options.open(target, { name: 'employee', path: target.projectRoot }, { ...request, agent, employeeScope: scope, bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, bindingRevision: state.revision, closeDiagnostics, detailChannel: details,
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}, resumeSessionId: undefined, recoveryCheckpointId: undefined, verifyRecovery: undefined });
|
|
238
|
-
if (entry) {
|
|
239
|
-
entry.current = input;
|
|
240
|
-
entry.details = details;
|
|
241
|
-
try {
|
|
242
|
-
await validate(request.signal ?? new AbortController().signal);
|
|
243
|
-
}
|
|
244
|
-
catch (error) {
|
|
245
|
-
await entry.dispose(asNeed(error), { layer: 'endpoint', initiator: 'provider.final-validation', reason: closeCode(error), action: 'dispose' });
|
|
246
|
-
throw error;
|
|
247
|
-
}
|
|
248
|
-
entry.runtime.details = details;
|
|
249
|
-
entry.runtime.connection = { ...connection };
|
|
250
|
-
entry.runtime.binding = { bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, revision: state.revision };
|
|
251
|
-
entry.runtime.restartNotice = host.restartNotice;
|
|
252
|
-
entry.runtime.restartInputIds = host.restartInputIds;
|
|
253
|
-
entry.runtime.freshSessionConfirmed = host.freshSessionConfirmed;
|
|
254
|
-
return entry.runtime;
|
|
255
|
-
}
|
|
256
|
-
const disposers = [];
|
|
257
|
-
const execution = new EmployeeExecution({ closeDiagnostics, bindingId: admitted.bindingId, acpMaxMessageBytes: request.acpMaxMessageBytes, validate, transport: signal => createEmployeeSshTransport(ssh, signal) });
|
|
258
|
-
const runtime = { ...host, closeDiagnostics, cwd: admitted.cwd, details, connection: { ...connection }, binding: { bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, revision: state.revision }, saveCheckpoint: undefined, recoveredSession: undefined, employeeExecutionMeta: { version: 1, bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, cwd: admitted.cwd, acpMaxMessageBytes: request.acpMaxMessageBytes, httpMcpPolicy: request.employeeHttpMcpPolicy ?? 'reject' }, employeePresentation: CODEX_EMPLOYEE_SCOPE_NOTICE };
|
|
170
|
+
const host = opening = await options.open(target, { name: 'employee', path: target.projectRoot }, { ...request, signal: roundSignal, agent, employeeScope: scope, bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, bindingRevision: state.revision, closeDiagnostics, detailChannel: details, resumeSessionId: request.resumeSessionId });
|
|
171
|
+
roundSignal.throwIfAborted();
|
|
172
|
+
// The old per-RPC identity check belongs once at the end of this round's opening.
|
|
173
|
+
if (get() !== api || employee.getNetwork() !== network)
|
|
174
|
+
throw fail('EMPLOYEE_CONTEXT_CHANGED');
|
|
175
|
+
const execution = new EmployeeExecution({ closeDiagnostics, bindingId: admitted.bindingId, acpMaxMessageBytes: request.acpMaxMessageBytes, transport: signal => createEmployeeSshTransport(ssh, signal) });
|
|
176
|
+
const runtime = { ...host, closeDiagnostics, cwd: admitted.cwd, details, connection: { ...connection }, binding: { bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, revision: state.revision }, employeeExecutionMeta: { version: 1, bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, cwd: admitted.cwd, acpMaxMessageBytes: request.acpMaxMessageBytes }, employeePresentation: EMPLOYEE_EXECUTION_NOTICE };
|
|
259
177
|
runtime.employeeRemoteCloseReason = () => execution.remoteClose?.reason;
|
|
260
178
|
entry = { scope, runtime, current: input, details, execution, network, ended: false, dispose: (reason, cause = { layer: 'runtime', initiator: 'provider.dispose', reason: reason?.code ?? 'EMPLOYEE_EXEC_CLOSED', action: 'dispose' }) => {
|
|
261
179
|
closeDiagnostics.record({ ...cause, changes: cause.changes ?? [{ field: 'provider.ended', before: entry.ended, after: true }] });
|
|
262
|
-
if (entry.
|
|
263
|
-
return
|
|
180
|
+
if (entry.ended)
|
|
181
|
+
return Promise.resolve();
|
|
264
182
|
entry.ended = true;
|
|
265
183
|
entry.reason = reason;
|
|
184
|
+
if (live.get(sessionId) === entry)
|
|
185
|
+
live.delete(sessionId);
|
|
266
186
|
execution.invalidate(reason ?? fail('EMPLOYEE_EXEC_CLOSED'), { layer: 'executor', initiator: 'provider.invalidate-executor', reason: reason?.code ?? 'EMPLOYEE_EXEC_CLOSED', action: 'invalidate' });
|
|
267
187
|
for (const off of disposers)
|
|
268
188
|
off();
|
|
269
189
|
host.broker?.fail(reason ?? fail('EMPLOYEE_EXEC_CLOSED'));
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
await execution.dispose({ layer: 'executor', initiator: 'provider.cleanup-executor', reason: 'RUNTIME_CLEANUP', action: 'dispose' });
|
|
274
|
-
}
|
|
275
|
-
catch (error) {
|
|
276
|
-
cleanup.push(...closeErrorCodes(error, 'EXECUTOR_CLEANUP_FAILED'));
|
|
277
|
-
}
|
|
278
|
-
try {
|
|
279
|
-
await host.dispose?.({ layer: 'broker', initiator: 'provider.cleanup-host', reason: 'RUNTIME_CLEANUP', action: 'dispose' });
|
|
280
|
-
}
|
|
281
|
-
catch (error) {
|
|
282
|
-
cleanup.push(...closeErrorCodes(error, 'HOST_CLEANUP_FAILED'));
|
|
283
|
-
}
|
|
284
|
-
const firstReason = String(closeDiagnostics.first?.reason ?? reason?.code ?? 'EMPLOYEE_EXEC_CLOSED');
|
|
285
|
-
try {
|
|
286
|
-
entry.details?.record({ kind: 'need', severity: 'warning', code: cleanup.length ? 'REMOTE_CLEANUP_UNCONFIRMED' : 'EMPLOYEE_EXEC_UNKNOWN', summary: '员工执行通道已关闭,命令结果 UNKNOWN', payload: { bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, result: 'UNKNOWN', reason: reason?.code ?? 'EMPLOYEE_EXEC_CLOSED', firstReason, cleanup: cleanup.length ? 'unconfirmed' : 'closed', cleanupCodes: cleanup } });
|
|
287
|
-
}
|
|
288
|
-
catch (error) {
|
|
289
|
-
reporting.push(closeCode(error, 'CLOSE_RECORD_FAILED'));
|
|
290
|
-
}
|
|
291
|
-
try {
|
|
292
|
-
await entry.details?.flush();
|
|
293
|
-
}
|
|
294
|
-
catch (error) {
|
|
295
|
-
reporting.push(...closeErrorCodes(error, 'CLOSE_FLUSH_FAILED'));
|
|
296
|
-
}
|
|
297
|
-
try {
|
|
298
|
-
closeDiagnostics.assertRecorded();
|
|
299
|
-
}
|
|
300
|
-
catch (error) {
|
|
301
|
-
reporting.push(closeDiagnostics.recordingFailureCode ?? closeCode(error, 'CLOSE_RECORD_FAILED'));
|
|
302
|
-
}
|
|
303
|
-
if (cleanup.length || reporting.length)
|
|
304
|
-
throw Object.assign(new NeedError(reporting.length ? 'CONVERSATION_DETAILS_FAILED' : 'REMOTE_CLEANUP_UNCONFIRMED', '执行连接已关闭;首次原因 ' + firstReason + ',清理或诊断保存未确认。', '首次关闭 ' + firstReason + ';清理 ' + (cleanup.join(',') || 'closed') + ';保存 ' + (reporting.join(',') || 'recorded')), { cause: new AggregateError([firstReason, ...cleanup, ...reporting].map(code => new NeedError(code, '关闭收尾代码 ' + code, '技术详情中的固定代码')), 'EMPLOYEE_CLOSE_AND_DIAGNOSTIC_FAILURES') });
|
|
305
|
-
if (live.get(sessionId) === entry)
|
|
306
|
-
live.delete(sessionId);
|
|
307
|
-
})();
|
|
308
|
-
return entry.closing;
|
|
190
|
+
void Promise.allSettled([execution.dispose({ layer: 'executor', initiator: 'provider.cleanup-executor', reason: 'RUNTIME_CLEANUP', action: 'dispose' }), host.dispose?.({ layer: 'broker', initiator: 'provider.cleanup-host', reason: 'RUNTIME_CLEANUP', action: 'dispose' })]).then(results => { if (results.some(result => result.status === 'rejected'))
|
|
191
|
+
console.warn('[remote-agent] 员工执行通道的旧资源将由主机回收'); });
|
|
192
|
+
return Promise.resolve();
|
|
309
193
|
} };
|
|
310
194
|
candidate = entry;
|
|
311
195
|
runtime.employeeExecution = async (params, signal) => {
|
|
@@ -324,43 +208,23 @@ export function createEmployeeProvider(get, options, agent) {
|
|
|
324
208
|
};
|
|
325
209
|
runtime.dispose = cause => entry.dispose(undefined, cause ?? { layer: 'runtime', initiator: 'runtime.dispose', reason: 'RUNTIME_DISPOSE', action: 'dispose' });
|
|
326
210
|
runtime.detach = cause => entry.dispose(undefined, cause ?? { layer: 'runtime', initiator: 'runtime.detach', reason: 'RUNTIME_DETACH', action: 'detach' });
|
|
327
|
-
const revoke = (reason, initiator, changes) => { void entry.dispose(reason, { layer: 'endpoint', initiator, reason: reason.code, action: 'invalidate', changes }).catch(() => { }); };
|
|
328
|
-
disposers.push(api.onWorkspaceLocationState(next => {
|
|
329
|
-
const matches = next.items.filter(item => item.workspaceId === admitted.workspaceId);
|
|
330
|
-
if (get() !== api || next.phase !== 'ready' || next.instanceId !== connection.instanceId || next.epoch !== connection.epoch || matches.length !== 1 || matches[0].bindingId !== admitted.bindingId || matches[0].source !== 'local' || matches[0].status !== 'selected')
|
|
331
|
-
revoke(fail('EMPLOYEE_CONTEXT_CHANGED'), 'endpoint.workspace-change', [{ field: 'workspace.binding-id', before: admitted.bindingId, after: matches[0]?.bindingId ?? null }, { field: 'workspace.source', before: 'local', after: matches[0]?.source ?? null }, { field: 'workspace.status', before: 'selected', after: matches[0]?.status ?? null }, { field: 'workspace.match-count', before: 1, after: matches.length }]);
|
|
332
|
-
}), api.onConnectionState(next => { if (get() !== api || next.phase !== 'connected' || next.instanceId !== connection.instanceId || next.epoch !== connection.epoch)
|
|
333
|
-
revoke(fail('CONNECTION_LOST'), 'endpoint.connection-change', [{ field: 'connection.instance-id', before: connection.instanceId, after: next.instanceId }, { field: 'connection.epoch', before: connection.epoch, after: next.epoch }, { field: 'connection.phase', before: 'connected', after: next.phase }]); }), network.onState(event => { if (event.kind === 'lost' || employee.getNetwork() !== network)
|
|
334
|
-
revoke(fail('CONNECTION_LOST'), 'endpoint.network-loss', [{ field: 'network.phase', before: null, after: event.state.phase }]); }));
|
|
335
211
|
if (host.broker)
|
|
336
212
|
disposers.push(host.broker.onChange(() => { if (!host.broker.online)
|
|
337
213
|
revoke(fail('EMPLOYEE_EXEC_UNKNOWN'), 'endpoint.broker-offline'); }));
|
|
338
214
|
live.set(sessionId, entry);
|
|
339
|
-
try {
|
|
340
|
-
await validate(request.signal ?? new AbortController().signal);
|
|
341
|
-
}
|
|
342
|
-
catch (error) {
|
|
343
|
-
await entry.dispose(asNeed(error), { layer: 'endpoint', initiator: 'provider.final-validation', reason: closeCode(error), action: 'dispose' });
|
|
344
|
-
throw error;
|
|
345
|
-
}
|
|
346
215
|
return runtime;
|
|
347
216
|
}
|
|
348
217
|
catch (error) {
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
catch (failure) {
|
|
354
|
-
reporting.push(failure);
|
|
355
|
-
}
|
|
218
|
+
for (const off of disposers)
|
|
219
|
+
off();
|
|
220
|
+
if (opening && !candidate)
|
|
221
|
+
void opening.dispose?.({ layer: 'runtime', initiator: 'provider.open-failure', reason: closeCode(error), action: 'dispose' }).catch(() => console.warn('EMPLOYEE_CLEANUP_FAILED'));
|
|
356
222
|
try {
|
|
357
|
-
|
|
223
|
+
details?.record({ kind: 'need', severity: 'warning', code: errorCode(error), summary: '本轮执行连接中断,恢复后可在原会话继续。', payload: { reason: errorCode(error) } });
|
|
358
224
|
}
|
|
359
|
-
catch
|
|
360
|
-
|
|
225
|
+
catch {
|
|
226
|
+
console.warn('[remote-agent] 执行诊断未保存');
|
|
361
227
|
}
|
|
362
|
-
if (reporting.length)
|
|
363
|
-
throw withFailureEvidence(error, reporting, 'diagnostic', closeDiagnostics?.first?.reason);
|
|
364
228
|
throw error;
|
|
365
229
|
}
|
|
366
230
|
};
|
package/dist/employee-reasons.js
CHANGED
|
@@ -7,7 +7,6 @@ export const employeeAdapterCodes = new Set([
|
|
|
7
7
|
"AW_EMPLOYEE_CONFIG_ADMISSION_INVALID",
|
|
8
8
|
"AW_EMPLOYEE_CONFIG_ADMISSION_FAILED",
|
|
9
9
|
"AW_EMPLOYEE_HOST_PROJECT_CONFIG_UNSUPPORTED",
|
|
10
|
-
"AW_EMPLOYEE_EXECUTOR_MCP_UNSUPPORTED",
|
|
11
10
|
"AW_EMPLOYEE_EXECUTOR_CONFIG_PROTOCOL_INVALID",
|
|
12
11
|
"AW_EMPLOYEE_EXECUTOR_CONFIG_READ_FAILED",
|
|
13
12
|
"AW_EMPLOYEE_AGENT_CONFIG_UNAVAILABLE",
|
|
@@ -21,6 +20,7 @@ export const employeeAdapterCodes = new Set([
|
|
|
21
20
|
"AW_EMPLOYEE_ENVIRONMENT_REGISTRATION_FAILED",
|
|
22
21
|
"AW_EMPLOYEE_ENVIRONMENT_REQUIRED",
|
|
23
22
|
"AW_EMPLOYEE_EXECUTION_CLOSED",
|
|
23
|
+
"AW_EMPLOYEE_NATIVE_CLOSED_BEFORE_INITIALIZE_REPLY",
|
|
24
24
|
"AW_EMPLOYEE_EXECUTION_ID_REQUIRED",
|
|
25
25
|
"AW_EMPLOYEE_HOST_AUXILIARY_CONFIG_UNSUPPORTED",
|
|
26
26
|
"AW_EMPLOYEE_HOST_AUXILIARY_UNSUPPORTED",
|
|
@@ -40,10 +40,6 @@ export const employeeAdapterCodes = new Set([
|
|
|
40
40
|
]);
|
|
41
41
|
export const employeeOpenCauseCodes = new Set([
|
|
42
42
|
"AW_EMPLOYEE_BINARY_UNAVAILABLE",
|
|
43
|
-
"AW_EMPLOYEE_BINARY_HASH_FAILED",
|
|
44
|
-
"AW_EMPLOYEE_BINARY_HASH_MISMATCH",
|
|
45
|
-
"AW_EMPLOYEE_BINARY_VERSION_FAILED",
|
|
46
|
-
"AW_EMPLOYEE_BINARY_VERSION_MISMATCH",
|
|
47
43
|
"EMPLOYEE_CONTEXT_CHANGED",
|
|
48
44
|
"EMPLOYEE_ENDPOINT_INVALID",
|
|
49
45
|
"EMPLOYEE_BINARY_REQUIRED",
|
package/dist/employee-shell.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export type EmployeeShell = (request: {
|
|
|
16
16
|
interface Options {
|
|
17
17
|
closeDiagnostics?: CloseDiagnostics;
|
|
18
18
|
target: EmployeeEndpointTarget;
|
|
19
|
+
sshArgs: readonly string[];
|
|
19
20
|
maxBytes: number;
|
|
20
21
|
killGraceMs: number;
|
|
21
22
|
attach: (spec: {
|
|
@@ -23,6 +24,6 @@ interface Options {
|
|
|
23
24
|
args: string[];
|
|
24
25
|
}) => Promise<ChildProcessWithoutNullStreams>;
|
|
25
26
|
}
|
|
26
|
-
/**
|
|
27
|
+
/** Use only the target verified at this round lease opening. Cancellation cannot prove remote exit. */
|
|
27
28
|
export declare function createEmployeeShell(options: Options): EmployeeShell;
|
|
28
29
|
export {};
|
package/dist/employee-shell.js
CHANGED
|
@@ -1,24 +1,28 @@
|
|
|
1
1
|
import { closeCode } from "./close-diagnostics.js";
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import {
|
|
3
|
+
import { EMPLOYEE_PYTHON_ERRORS } from "./employee-relay.js";
|
|
4
4
|
import { NeedError } from "./remote.js";
|
|
5
|
-
const SHELL_PYTHON =
|
|
6
|
-
|
|
5
|
+
const SHELL_PYTHON = String.raw `
|
|
6
|
+
import sys,json,subprocess,shlex
|
|
7
|
+
def fail(code):
|
|
8
|
+
sys.stderr.write(json.dumps({"code":code})+"\n");sys.exit(254)
|
|
9
|
+
try:
|
|
10
|
+
x=json.loads(sys.argv[1]);e=x["endpoint"];args=x["sshArgs"];cwd=x["cwd"]
|
|
7
11
|
# User source is stdin, never argv. The sentinel preserves trailing newlines.
|
|
8
|
-
wrapper="cd -- "+shlex.quote(cwd)+" || exit 254; "+
|
|
12
|
+
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"'
|
|
9
13
|
command=shlex.join([e["endpointShell"],"-c",wrapper])
|
|
10
14
|
sys.stderr.write("AW_EMPLOYEE_SHELL_READY "+x["executionId"]+"\n");sys.stderr.flush()
|
|
11
15
|
p=subprocess.Popen(["/usr/bin/ssh","-T"]+args+[command],stdin=sys.stdin,stdout=sys.stdout,stderr=sys.stderr)
|
|
12
16
|
code=p.wait();sys.exit(code if code>=0 else 255)
|
|
13
17
|
` + EMPLOYEE_PYTHON_ERRORS;
|
|
14
18
|
const unknown = (code) => new NeedError(code, '本机命令结果未知;没有重放,也未改在主机执行。', '员工端真实退出回执');
|
|
15
|
-
/**
|
|
19
|
+
/** Use only the target verified at this round lease opening. Cancellation cannot prove remote exit. */
|
|
16
20
|
export function createEmployeeShell(options) {
|
|
17
21
|
return async ({ command, timeout, signal }) => {
|
|
18
22
|
signal.throwIfAborted();
|
|
19
23
|
const diagnostics = options.closeDiagnostics?.child();
|
|
20
24
|
const executionId = randomUUID(), target = options.target;
|
|
21
|
-
const child = await options.attach({ command: '/usr/bin/python3', args: ['-c', SHELL_PYTHON, JSON.stringify({ endpoint: target.endpoint,
|
|
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 })] });
|
|
22
26
|
return new Promise((yes, no) => {
|
|
23
27
|
let settled = false, bytes = 0, stdout = [], stderr = [], reason, grace, timer;
|
|
24
28
|
const finish = (error, code) => {
|
|
@@ -65,13 +69,6 @@ export function createEmployeeShell(options) {
|
|
|
65
69
|
no(unknown('EMPLOYEE_EXIT_UNKNOWN'));
|
|
66
70
|
return;
|
|
67
71
|
}
|
|
68
|
-
try {
|
|
69
|
-
diagnostics?.assertRecorded();
|
|
70
|
-
}
|
|
71
|
-
catch (failure) {
|
|
72
|
-
no(failure);
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
72
|
yes({ stdout: out, stderr: err.slice(header.length, start) + err.slice(start + started.length), stderrSource: 'combined-ssh-and-command', exitCode: code, signal: null });
|
|
76
73
|
};
|
|
77
74
|
const stop = (code) => { diagnostics?.record({ layer: 'executor', initiator: 'shell.stop', reason: code, action: 'terminate', changes: [{ field: 'shell.stop-code', before: reason?.code ?? null, after: settled || reason ? reason?.code ?? null : code }] }); if (settled || reason)
|
package/dist/employee-ssh.d.ts
CHANGED
|
@@ -9,15 +9,13 @@ export interface EmployeeSshOptions {
|
|
|
9
9
|
closeDiagnostics?: CloseDiagnostics;
|
|
10
10
|
/** Automatically discovered package binary path on this same employee device. */
|
|
11
11
|
employeeBinary: string;
|
|
12
|
-
binarySha256: string;
|
|
13
12
|
acpMaxMessageBytes: number;
|
|
14
13
|
argv: SshArgv;
|
|
15
|
-
validate(signal: AbortSignal): Promise<void>;
|
|
16
14
|
/** Fixture seam only. Production uses L's exported argv through sshIo. */
|
|
17
15
|
io?: BrokerIo;
|
|
18
16
|
}
|
|
19
17
|
/** Fixed bootstrap only; cwd for every native operation remains in its original RPC. */
|
|
20
|
-
export declare function employeeExecServerCommand(binary: string,
|
|
18
|
+
export declare function employeeExecServerCommand(binary: string, readyMarker: string): string;
|
|
21
19
|
/** Explicit read-only probe; production dispatch verifies S once in transport open. */
|
|
22
20
|
export declare function validateEmployeeSshTarget(options: EmployeeSshOptions, signal?: AbortSignal): Promise<void>;
|
|
23
21
|
/** Outer L-authenticated host SSH, then S-authenticated employee SSH; no host execution fallback. */
|
package/dist/employee-ssh.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { closeCode } from "./close-diagnostics.js";
|
|
2
3
|
import { sshIo } from "./broker.js";
|
|
3
4
|
import { createEmployeeProcessTransport } from "./employee-exec.js";
|
|
@@ -70,17 +71,10 @@ function readTarget(raw, expected, maxBytes) {
|
|
|
70
71
|
return args;
|
|
71
72
|
}
|
|
72
73
|
/** Fixed bootstrap only; cwd for every native operation remains in its original RPC. */
|
|
73
|
-
export function employeeExecServerCommand(binary,
|
|
74
|
-
if (!absolute(binary)
|
|
74
|
+
export function employeeExecServerCommand(binary, readyMarker) {
|
|
75
|
+
if (!absolute(binary))
|
|
75
76
|
throw fail('EMPLOYEE_BINARY_REQUIRED');
|
|
76
|
-
|
|
77
|
-
const stop = (code) => `{ printf '%s\\n' '${code}' >&2; exit 126; }`;
|
|
78
|
-
return `if [ ! -f ${file} ] || [ ! -x ${file} ]; then ${stop('AW_EMPLOYEE_BINARY_UNAVAILABLE')}; fi; ` +
|
|
79
|
-
`aw_employee_digest=$(/usr/bin/shasum -a 256 < ${file} 2>/dev/null) || ${stop('AW_EMPLOYEE_BINARY_HASH_FAILED')}; ` +
|
|
80
|
-
`aw_employee_digest=\${aw_employee_digest%% *}; [ "$aw_employee_digest" = ${digest} ] || ${stop('AW_EMPLOYEE_BINARY_HASH_MISMATCH')}; ` +
|
|
81
|
-
`aw_employee_version=$(${file} --version 2>/dev/null) || ${stop('AW_EMPLOYEE_BINARY_VERSION_FAILED')}; ` +
|
|
82
|
-
`[ "$aw_employee_version" = 'codex-cli 0.160.1' ] || ${stop('AW_EMPLOYEE_BINARY_VERSION_MISMATCH')}; ` +
|
|
83
|
-
`exec ${file} exec-server --listen stdio`;
|
|
77
|
+
return `printf '%s\n' ${shellQuote(readyMarker)} >&2; exec ${shellQuote(binary)} exec-server --listen stdio`;
|
|
84
78
|
}
|
|
85
79
|
async function currentTarget(options, signal) {
|
|
86
80
|
checkSignal(signal);
|
|
@@ -89,14 +83,14 @@ async function currentTarget(options, signal) {
|
|
|
89
83
|
const endpoint = structuredClone(options.endpoint), target = structuredClone(options.target);
|
|
90
84
|
checkEndpoint(endpoint, target);
|
|
91
85
|
const io = options.io ?? sshIo(target, options.argv, options.acpMaxMessageBytes);
|
|
92
|
-
const gate =
|
|
93
|
-
|
|
86
|
+
const gate = () => checkSignal(signal);
|
|
87
|
+
gate();
|
|
94
88
|
const b = endpoint.bridge, e = endpoint.endpoint;
|
|
95
89
|
const command = [b.command, 'target', '--settings', b.settingsPath, '--device', e.deviceId, '--if-instance', b.bridgeInstanceId, '--if-generation', String(b.generation), '--if-endpoint', e.endpointInstanceId, '--if-context', e.executionContextId].map(shellQuote).join(' ');
|
|
96
90
|
const run = () => awaitSignal(io.run(command, signal), signal);
|
|
97
91
|
const raw = await (options.closeDiagnostics ? options.closeDiagnostics.run('endpoint.target', run) : run());
|
|
98
92
|
const args = readTarget(raw, endpoint, options.acpMaxMessageBytes);
|
|
99
|
-
|
|
93
|
+
gate();
|
|
100
94
|
return { io, args, gate };
|
|
101
95
|
}
|
|
102
96
|
/** Explicit read-only probe; production dispatch verifies S once in transport open. */
|
|
@@ -104,18 +98,21 @@ export async function validateEmployeeSshTarget(options, signal = new AbortContr
|
|
|
104
98
|
/** Outer L-authenticated host SSH, then S-authenticated employee SSH; no host execution fallback. */
|
|
105
99
|
export async function createEmployeeSshTransport(options, signal = new AbortController().signal) {
|
|
106
100
|
checkSignal(signal);
|
|
107
|
-
const
|
|
101
|
+
const readyMarker = 'AW_EMPLOYEE_EXEC_SERVER_READY_' + randomUUID().replaceAll('-', '');
|
|
102
|
+
const remote = employeeExecServerCommand(options.employeeBinary, readyMarker);
|
|
108
103
|
const { io, args, gate } = await currentTarget(options, signal);
|
|
109
104
|
checkSignal(signal);
|
|
110
105
|
// EmployeeExecution races the user's open wait; this factory retains any late child cleanup.
|
|
111
106
|
const child = await io.attach('exec ' + ['/usr/bin/ssh', ...args, remote].map(shellQuote).join(' '));
|
|
112
|
-
const transport = createEmployeeProcessTransport(child, options.acpMaxMessageBytes, [
|
|
107
|
+
const transport = createEmployeeProcessTransport(child, options.acpMaxMessageBytes, [], options.closeDiagnostics?.capture(), readyMarker);
|
|
113
108
|
if (signal.aborted) {
|
|
114
109
|
await transport.close({ layer: 'endpoint', initiator: 'ssh.open-abort', reason: 'ABORTED', operation: 'open', action: 'abort' });
|
|
115
110
|
throw fail('ABORTED');
|
|
116
111
|
}
|
|
117
112
|
try {
|
|
118
|
-
|
|
113
|
+
const wait = () => awaitSignal(transport.ready, signal);
|
|
114
|
+
await (options.closeDiagnostics ? options.closeDiagnostics.run('employee-executor-ready', wait) : wait());
|
|
115
|
+
gate();
|
|
119
116
|
return transport;
|
|
120
117
|
}
|
|
121
118
|
catch (error) {
|