@ai-workbench/remote-agent 0.8.21 → 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 -156
- package/dist/acp.d.ts +20 -15
- package/dist/acp.js +197 -224
- 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 +40 -105
- package/dist/client.js +34 -127
- 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 -3
- package/dist/employee-endpoint.js +8 -39
- package/dist/employee-exec.d.ts +4 -3
- package/dist/employee-exec.js +25 -13
- package/dist/employee-info.d.ts +5 -1
- package/dist/employee-info.js +16 -5
- 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 +1 -1
- package/dist/index.d.ts +13 -43
- package/dist/index.js +190 -489
- package/dist/input.js +2 -3
- package/dist/local-user-turn.js +5 -6
- package/dist/mapping.d.ts +0 -3
- package/dist/mapping.js +0 -34
- 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
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
export type JsonValue = null | boolean | number | string | JsonValue[] | {
|
|
2
2
|
[key: string]: JsonValue;
|
|
3
3
|
};
|
|
4
|
-
export interface DetailScope {
|
|
5
|
-
instanceId: string;
|
|
6
|
-
epoch: number;
|
|
7
|
-
sessionId: string;
|
|
8
|
-
provider: 'claude' | 'codex';
|
|
9
|
-
turn: number;
|
|
10
|
-
step: number;
|
|
11
|
-
}
|
|
12
4
|
export interface DetailObservation {
|
|
13
5
|
kind: 'extension' | 'commands' | 'tool' | 'usage' | 'notice' | 'need' | 'session';
|
|
14
6
|
severity: 'info' | 'warning' | 'action-required';
|
|
@@ -20,28 +12,7 @@ export interface DetailObservation {
|
|
|
20
12
|
export type SanitizedObservation = Omit<DetailObservation, 'payload'> & {
|
|
21
13
|
payload: JsonValue;
|
|
22
14
|
};
|
|
23
|
-
export interface DetailRecord extends Omit<DetailObservation, 'payload'>, DetailScope {
|
|
24
|
-
version: 1;
|
|
25
|
-
eventId: string;
|
|
26
|
-
sequence: number;
|
|
27
|
-
payload: JsonValue;
|
|
28
|
-
}
|
|
29
|
-
export type DetailSink = {
|
|
30
|
-
append(record: Readonly<DetailRecord>): Promise<void>;
|
|
31
|
-
};
|
|
32
15
|
/** Only objects created by this module are trusted as redaction markers. */
|
|
33
16
|
export declare function isSanitizedRedaction(value: unknown): boolean;
|
|
34
17
|
export declare function sanitizeDisplayLabel(value: string): string;
|
|
35
18
|
export declare function sanitizeObservation(observation: DetailObservation, protectModelMaterial?: boolean): Readonly<SanitizedObservation>;
|
|
36
|
-
/** Internal diagnostic recorder; a sink is not an assertion that an L wire API exists. */
|
|
37
|
-
export declare class ConversationDetails {
|
|
38
|
-
private readonly scope;
|
|
39
|
-
private readonly sink;
|
|
40
|
-
private readonly records;
|
|
41
|
-
private tail;
|
|
42
|
-
private firstFailure;
|
|
43
|
-
constructor(scope: DetailScope, sink: DetailSink);
|
|
44
|
-
record(observation: DetailObservation): void;
|
|
45
|
-
flush(): Promise<void>;
|
|
46
|
-
snapshot(): readonly DetailRecord[];
|
|
47
|
-
}
|
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
import { employeeExecutionInfo } from "./employee-info.js";
|
|
2
|
-
import { trustedEmployeeReason } from "./employee-reasons.js";
|
|
3
|
-
import { randomUUID } from 'node:crypto';
|
|
4
|
-
import { STAGES } from "./stage-timing.js";
|
|
5
2
|
const redactionMarkers = new WeakSet();
|
|
6
3
|
function redaction(fields) {
|
|
7
4
|
const marker = { redacted: true, ...fields };
|
|
@@ -257,22 +254,6 @@ function usageStructure(value, key = '') {
|
|
|
257
254
|
return value;
|
|
258
255
|
return opaqueStructure(value);
|
|
259
256
|
}
|
|
260
|
-
/** Only fixed local stage names and finite nonnegative millisecond observations. */
|
|
261
|
-
function timingStructure(value) {
|
|
262
|
-
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
263
|
-
return opaqueStructure(value);
|
|
264
|
-
return Object.fromEntries(Object.entries(value).map(([key, child]) => {
|
|
265
|
-
if (key === 'count' && typeof child === 'number' && Number.isSafeInteger(child) && child >= 1)
|
|
266
|
-
return [key, child];
|
|
267
|
-
if (key === 'phase' && typeof child === 'string' && Object.hasOwn(STAGES, child))
|
|
268
|
-
return [key, child];
|
|
269
|
-
if (key === 'clock' && child === 'client-monotonic' || key === 'outcome' && (child === 'ok' || child === 'failed'))
|
|
270
|
-
return [key, child];
|
|
271
|
-
if (['durationMs', 'elapsedMs', 'totalDurationMs', 'minDurationMs', 'maxDurationMs'].includes(key) && typeof child === 'number' && Number.isFinite(child) && child >= 0)
|
|
272
|
-
return [key, child];
|
|
273
|
-
return [key, opaqueStructure(child)];
|
|
274
|
-
}));
|
|
275
|
-
}
|
|
276
257
|
/** Only our two model-configuration observations approve these exact diagnostic
|
|
277
258
|
* paths. Directory source, config metadata and arbitrary subtrees stay opaque. */
|
|
278
259
|
function modelConfigurationStructure(value, level = 'root') {
|
|
@@ -344,34 +325,11 @@ function closeStructure(value) {
|
|
|
344
325
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
345
326
|
return opaqueStructure(value);
|
|
346
327
|
return Object.fromEntries(Object.entries(value).map(([key, child]) => {
|
|
347
|
-
if (key === '
|
|
348
|
-
return [key, child.map(change => {
|
|
349
|
-
if (!change || typeof change !== 'object' || Array.isArray(change))
|
|
350
|
-
return opaqueStructure(change);
|
|
351
|
-
const field = change.field;
|
|
352
|
-
const known = typeof field === 'string' && (/^(?:observer.close-sequence|executor.ended|transport.closed|transport.termination-requested|provider.ended|broker.ended|broker.attached|broker.lease-aborted|lease.aborted|lease.closed|mcp.ended|mcp.closing|mcp.poisoned|shell.settled|shell.stop-code|tools.aborted|tools.stop-code|acp.close-requested|adapter.turn-aborted|adapter.detached|adapter.shutting-down|workspace.binding-id|workspace.source|workspace.status|workspace.match-count|network.phase|catalog.api-replaced|catalog.capability|catalog.signature-sha256|connection.instance-id|connection.epoch|connection.phase|connection.need-code)$/.test(field) || /^catalog\.(?:codex|claude)\.(?:ok|error-code|instance-id|epoch|provider|target-sha256|metadata-sha256|selected-model-valid)$/.test(field));
|
|
353
|
-
if (!known)
|
|
354
|
-
return opaqueStructure(change);
|
|
355
|
-
const scalar = (value) => value === null || typeof value === 'boolean' || typeof value === 'number' && Number.isFinite(value) || typeof value === 'string' && /^[a-zA-Z0-9_.:@-]+$/.test(value) ? value : opaqueStructure(value);
|
|
356
|
-
return Object.fromEntries(Object.entries(change).map(([name, value]) => [name, name === 'field' ? field : ['before', 'after'].includes(name) ? scalar(value) : opaqueStructure(value)]));
|
|
357
|
-
})];
|
|
358
|
-
if (key === 'pendingOperations' && Array.isArray(child))
|
|
359
|
-
return [key, child.map(field => typeof field === 'string' && /^[a-z][a-z0-9._/-]*$/.test(field) ? field : opaqueStructure(field))];
|
|
360
|
-
if (key === 'changedFields' && Array.isArray(child))
|
|
361
|
-
return [key, child.map(field => typeof field === 'string' && /^[a-z][a-z-]*$/.test(field) ? field : opaqueStructure(field))];
|
|
362
|
-
if (['closeId', 'parentCloseId'].includes(key) && typeof child === 'string' && /^[a-f0-9-]+$/.test(child))
|
|
363
|
-
return [key, child];
|
|
364
|
-
if (['layer', 'initiator', 'action', 'operation', 'stage', 'clock'].includes(key) && typeof child === 'string' && /^[a-z][a-z0-9._/-]*$/.test(child))
|
|
365
|
-
return [key, child];
|
|
366
|
-
if (key === 'reason' && typeof child === 'string' && /^[A-Z][A-Z0-9_]*$/.test(child))
|
|
328
|
+
if (['layer', 'initiator', 'stage', 'clock'].includes(key) && typeof child === 'string' && /^[a-z][a-z0-9._/-]*$/.test(child))
|
|
367
329
|
return [key, child];
|
|
368
|
-
if (
|
|
330
|
+
if (['reason', 'remoteReason'].includes(key) && typeof child === 'string' && /^[A-Z][A-Z0-9_]*$/.test(child))
|
|
369
331
|
return [key, child];
|
|
370
|
-
if (
|
|
371
|
-
return [key, child];
|
|
372
|
-
if (key === 'exitCode' && (child === null || typeof child === 'number' && Number.isSafeInteger(child)))
|
|
373
|
-
return [key, child];
|
|
374
|
-
if (key === 'signal' && (child === null || typeof child === 'string' && /^SIG[A-Z]+$/.test(child)))
|
|
332
|
+
if (key === 'elapsedMs' && typeof child === 'number' && Number.isFinite(child) && child >= 0)
|
|
375
333
|
return [key, child];
|
|
376
334
|
return [key, opaqueStructure(child)];
|
|
377
335
|
}));
|
|
@@ -401,62 +359,13 @@ export function sanitizeObservation(observation, protectModelMaterial = false) {
|
|
|
401
359
|
: observation.kind === 'session' && observation.code === 'CATALOG_SCOPE_INFORMATION' ? catalogScopeStructure(payload)
|
|
402
360
|
: observation.kind === 'session' && observation.code === 'CODEX_CONTEXT_SESSION_REPLACED' ? contextTransitionStructure(payload)
|
|
403
361
|
: observation.kind === 'session' && ['MODEL_CONFIGURATION_CONFIRMED', 'MODEL_CONFIGURATION_FAILED', 'MODEL_CONFIGURATION_INFORMATION', 'MODEL_CONFIGURATION_CHANGED_DURING_TURN', 'TURN_GATE_INFORMATION'].includes(observation.code) ? modelConfigurationStructure(payload)
|
|
404
|
-
: observation.kind === 'session' &&
|
|
362
|
+
: observation.kind === 'session' && observation.code === 'REMOTE_CLOSE_REASON' ? closeStructure(payload)
|
|
405
363
|
: observation.kind === 'commands' ? catalogStructure(payload)
|
|
406
364
|
: observation.kind === 'usage' ? usageStructure(payload)
|
|
407
365
|
: observation.kind === 'session' && observation.code === 'EMPLOYEE_MCP_INFORMATION' ? (employeeExecutionInfo(observation.payload) ?? { information: 'unconfirmed' })
|
|
408
|
-
: observation.kind === 'session' &&
|
|
409
|
-
: observation.kind === '
|
|
410
|
-
: observation.kind === '
|
|
411
|
-
:
|
|
412
|
-
: opaqueStructure(payload),
|
|
366
|
+
: observation.kind === 'session' && ['CLAUDE_EMPLOYEE_OPERATION', 'CLAUDE_EMPLOYEE_PROCESS', 'CLAUDE_EMPLOYEE_REVOKED', 'CLAUDE_EMPLOYEE_SCOPE'].includes(observation.code) ? employeeStructure(payload)
|
|
367
|
+
: observation.kind === 'need' ? payload
|
|
368
|
+
: observation.kind === 'tool' || ['SDK_ITEM', 'SDK_TOOL', 'ACP_SESSION_UPDATE', 'ACP_IDLE_UPDATE'].includes(observation.code) ? toolStructure(payload)
|
|
369
|
+
: opaqueStructure(payload),
|
|
413
370
|
});
|
|
414
371
|
}
|
|
415
|
-
function validatedScope(scope) {
|
|
416
|
-
for (const name of ['instanceId', 'sessionId']) {
|
|
417
|
-
if (typeof scope[name] !== 'string' || scope[name].trim().length === 0)
|
|
418
|
-
throw new TypeError(`Invalid detail scope ${name}`);
|
|
419
|
-
}
|
|
420
|
-
for (const name of ['epoch', 'turn', 'step']) {
|
|
421
|
-
if (!Number.isInteger(scope[name]) || scope[name] < (name === 'epoch' ? 0 : 1))
|
|
422
|
-
throw new TypeError(`Invalid detail scope ${name}`);
|
|
423
|
-
}
|
|
424
|
-
if (scope.provider !== 'claude' && scope.provider !== 'codex')
|
|
425
|
-
throw new TypeError('Invalid detail scope provider');
|
|
426
|
-
return { instanceId: scope.instanceId, epoch: scope.epoch, sessionId: scope.sessionId, provider: scope.provider, turn: scope.turn, step: scope.step };
|
|
427
|
-
}
|
|
428
|
-
/** Internal diagnostic recorder; a sink is not an assertion that an L wire API exists. */
|
|
429
|
-
export class ConversationDetails {
|
|
430
|
-
scope;
|
|
431
|
-
sink;
|
|
432
|
-
records = [];
|
|
433
|
-
tail = Promise.resolve();
|
|
434
|
-
firstFailure;
|
|
435
|
-
constructor(scope, sink) {
|
|
436
|
-
this.scope = freezeDeep(validatedScope(scope));
|
|
437
|
-
this.sink = sink;
|
|
438
|
-
}
|
|
439
|
-
record(observation) {
|
|
440
|
-
const record = freezeDeep({
|
|
441
|
-
...this.scope, version: 1, eventId: randomUUID(), sequence: this.records.length + 1,
|
|
442
|
-
...sanitizeObservation(observation),
|
|
443
|
-
});
|
|
444
|
-
this.records.push(record);
|
|
445
|
-
this.tail = this.tail.then(async () => {
|
|
446
|
-
try {
|
|
447
|
-
await this.sink.append(cloneFrozen(record));
|
|
448
|
-
}
|
|
449
|
-
catch (error) {
|
|
450
|
-
// Keep processing later entries in order, but never turn a failed flush green.
|
|
451
|
-
if (!this.firstFailure)
|
|
452
|
-
this.firstFailure = { error };
|
|
453
|
-
}
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
async flush() {
|
|
457
|
-
await this.tail;
|
|
458
|
-
if (this.firstFailure)
|
|
459
|
-
throw this.firstFailure.error;
|
|
460
|
-
}
|
|
461
|
-
snapshot() { return cloneFrozen(this.records); }
|
|
462
|
-
}
|
|
@@ -87,9 +87,7 @@ export interface EmployeeLease {
|
|
|
87
87
|
target: EmployeeEndpointTarget;
|
|
88
88
|
request: Readonly<LocalWorkspaceContextRequest>;
|
|
89
89
|
signal: AbortSignal;
|
|
90
|
-
|
|
90
|
+
sshArgs: readonly string[];
|
|
91
91
|
close(cause?: CloseCause): Promise<void>;
|
|
92
|
-
/** Existing physical fence, relocated to actual local tools rather than broker messages. */
|
|
93
|
-
validateDirectory(callerSignal?: AbortSignal): Promise<EmployeeEndpointTarget>;
|
|
94
92
|
}
|
|
95
93
|
export declare function openEmployeeLease(options: EmployeeLeaseOptions, initialSignal?: AbortSignal): Promise<EmployeeLease>;
|
|
@@ -41,16 +41,17 @@ export async function openEmployeeLease(options, initialSignal) {
|
|
|
41
41
|
if (connection.phase !== 'connected' || state.phase !== 'ready' || state.instanceId !== connection.instanceId || state.epoch !== connection.epoch || items.length !== 1 || items[0].source !== 'local' || items[0].status !== 'selected' || !items[0].bindingId)
|
|
42
42
|
throw fail();
|
|
43
43
|
const request = { instanceId: connection.instanceId, epoch: connection.epoch, workspaceId: workspace.workspaceId, expectedBindingId: items[0].bindingId };
|
|
44
|
-
let target, closed = false
|
|
44
|
+
let target, closed = false;
|
|
45
|
+
let sshArgs = [];
|
|
45
46
|
const invalidate = (code, initiator = 'lease.validate') => { options.closeDiagnostics?.record({ layer: 'endpoint', initiator, reason: code, action: 'invalidate', changes: [{ field: 'lease.aborted', before: lifetime.signal.aborted, after: true }] }); if (lifetime.signal.aborted)
|
|
46
47
|
return; try {
|
|
47
48
|
options.detail({ kind: 'session', severity: 'warning', code: 'CLAUDE_EMPLOYEE_REVOKED', summary: '本机旧绑定已停止派发,在途结果按实际确认;不会重放', payload: { reason: code, workspaceId: request.workspaceId, bindingId: request.expectedBindingId } });
|
|
48
49
|
}
|
|
49
|
-
catch
|
|
50
|
-
|
|
50
|
+
catch {
|
|
51
|
+
console.warn("EMPLOYEE_DIAGNOSTIC_DROPPED");
|
|
51
52
|
}
|
|
52
53
|
finally {
|
|
53
|
-
lifetime.abort(
|
|
54
|
+
lifetime.abort(fail(code));
|
|
54
55
|
} };
|
|
55
56
|
const current = () => {
|
|
56
57
|
if (options.getConnect() !== api || options.getNetwork() !== network)
|
|
@@ -79,8 +80,6 @@ export async function openEmployeeLease(options, initialSignal) {
|
|
|
79
80
|
}
|
|
80
81
|
// Remote endpoint discovery belongs to lease opening only.
|
|
81
82
|
async function openTarget(callerSignal) {
|
|
82
|
-
if (diagnosticFailure)
|
|
83
|
-
throw diagnosticFailure;
|
|
84
83
|
callerSignal?.throwIfAborted();
|
|
85
84
|
const signal = AbortSignal.any([lifetime.signal, ...callerSignal ? [callerSignal] : []]);
|
|
86
85
|
try {
|
|
@@ -112,7 +111,7 @@ export async function openEmployeeLease(options, initialSignal) {
|
|
|
112
111
|
catch {
|
|
113
112
|
throw fail('EMPLOYEE_BRIDGE_PROTOCOL_INVALID');
|
|
114
113
|
}
|
|
115
|
-
verifyBridgeTarget(result, next);
|
|
114
|
+
sshArgs = verifyBridgeTarget(result, next).sshArgs;
|
|
116
115
|
const final = await lstat(workspace.directory.path);
|
|
117
116
|
signal.throwIfAborted();
|
|
118
117
|
current();
|
|
@@ -127,44 +126,14 @@ export async function openEmployeeLease(options, initialSignal) {
|
|
|
127
126
|
throw error instanceof NeedError || callerSignal?.aborted ? error : fail();
|
|
128
127
|
}
|
|
129
128
|
}
|
|
130
|
-
async function validate(callerSignal) {
|
|
131
|
-
if (diagnosticFailure)
|
|
132
|
-
throw diagnosticFailure;
|
|
133
|
-
callerSignal?.throwIfAborted();
|
|
134
|
-
try {
|
|
135
|
-
current();
|
|
136
|
-
return structuredClone(target);
|
|
137
|
-
}
|
|
138
|
-
catch (error) {
|
|
139
|
-
if (!callerSignal?.aborted)
|
|
140
|
-
invalidate(error instanceof NeedError ? error.code : 'EMPLOYEE_CONTEXT_CHANGED');
|
|
141
|
-
throw error instanceof NeedError || callerSignal?.aborted ? error : fail();
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
async function validateDirectory(callerSignal) {
|
|
145
|
-
if (diagnosticFailure)
|
|
146
|
-
throw diagnosticFailure;
|
|
147
|
-
callerSignal?.throwIfAborted();
|
|
148
|
-
const signal = AbortSignal.any([lifetime.signal, ...callerSignal ? [callerSignal] : []]);
|
|
149
|
-
try {
|
|
150
|
-
await physicalDirectory(signal);
|
|
151
|
-
return await validate(callerSignal);
|
|
152
|
-
}
|
|
153
|
-
catch (error) {
|
|
154
|
-
if (!callerSignal?.aborted)
|
|
155
|
-
invalidate(error instanceof NeedError ? error.code : 'EMPLOYEE_CONTEXT_CHANGED');
|
|
156
|
-
throw error instanceof NeedError || callerSignal?.aborted ? error : fail();
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
129
|
try {
|
|
160
130
|
await openTarget(initialSignal);
|
|
161
|
-
return { target: structuredClone(target), request: Object.freeze({ ...request }), signal: lifetime.signal,
|
|
131
|
+
return { target: structuredClone(target), sshArgs: [...sshArgs], request: Object.freeze({ ...request }), signal: lifetime.signal, close: async (cause = { layer: 'endpoint', initiator: 'lease.close', reason: 'EMPLOYEE_EXECUTOR_CLOSED', action: 'close' }) => { options.closeDiagnostics?.record({ ...cause, changes: cause.changes ?? [{ field: 'lease.closed', before: closed, after: true }] }); if (!closed) {
|
|
162
132
|
closed = true;
|
|
163
133
|
lifetime.abort(fail('EMPLOYEE_EXECUTOR_CLOSED'));
|
|
164
134
|
for (const off of offs)
|
|
165
135
|
off();
|
|
166
|
-
}
|
|
167
|
-
throw diagnosticFailure; } };
|
|
136
|
+
} } };
|
|
168
137
|
}
|
|
169
138
|
catch (error) {
|
|
170
139
|
options.closeDiagnostics?.record({ layer: 'endpoint', initiator: 'lease.open-failure', reason: closeCode(error), action: 'failure' });
|
package/dist/employee-exec.d.ts
CHANGED
|
@@ -12,8 +12,6 @@ export interface EmployeeExecutionOptions {
|
|
|
12
12
|
bindingId: string;
|
|
13
13
|
closeDiagnostics?: CloseDiagnostics;
|
|
14
14
|
acpMaxMessageBytes: number;
|
|
15
|
-
/** Rechecks the current L CAS/directory/account and N/S five-tuple. No cached permit. */
|
|
16
|
-
validate(signal: AbortSignal): Promise<void>;
|
|
17
15
|
/** Once allocation starts, settle only after returning its executor or confirming aborted cleanup. */
|
|
18
16
|
transport(signal: AbortSignal): Promise<EmployeeExecutionTransport>;
|
|
19
17
|
}
|
|
@@ -58,4 +56,7 @@ export declare class EmployeeExecution {
|
|
|
58
56
|
private read;
|
|
59
57
|
}
|
|
60
58
|
/** NDJSON carrier with byte bounds and backpressure derived solely from acpMaxMessageBytes. */
|
|
61
|
-
export
|
|
59
|
+
export interface EmployeeProcessTransport extends EmployeeExecutionTransport {
|
|
60
|
+
readonly ready: Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
export declare function createEmployeeProcessTransport(child: ChildProcessWithoutNullStreams, acpMaxMessageBytes: number, stderrCodes?: readonly string[], observe?: CloseObserver, readyMarker?: string): EmployeeProcessTransport;
|
package/dist/employee-exec.js
CHANGED
|
@@ -86,7 +86,7 @@ export class EmployeeExecution {
|
|
|
86
86
|
}
|
|
87
87
|
active() { if (this.ended)
|
|
88
88
|
throw this.error ?? need('EMPLOYEE_EXEC_CLOSED'); }
|
|
89
|
-
|
|
89
|
+
gate(signal) { this.active(); cancelled(signal); }
|
|
90
90
|
stop(error, cause = { layer: 'executor', initiator: 'executor.stop', reason: closeCode(error, 'EMPLOYEE_EXEC_CLOSED'), action: 'close' }, observe = this.options.closeDiagnostics?.capture()) {
|
|
91
91
|
observe?.({ ...cause, changes: cause.changes ?? [{ field: 'executor.ended', before: this.ended, after: true }] });
|
|
92
92
|
if (this.ended)
|
|
@@ -147,12 +147,12 @@ export class EmployeeExecution {
|
|
|
147
147
|
this.queuedWriteBytes += size;
|
|
148
148
|
const operation = this.writeTail.then(async () => {
|
|
149
149
|
try {
|
|
150
|
-
|
|
150
|
+
this.gate(current);
|
|
151
151
|
const io = this.io;
|
|
152
152
|
if (!io)
|
|
153
153
|
throw need('EMPLOYEE_EXEC_CLOSED');
|
|
154
154
|
await waiting(io.write(value.message, current), current);
|
|
155
|
-
|
|
155
|
+
this.gate(current);
|
|
156
156
|
return { version: 1 };
|
|
157
157
|
}
|
|
158
158
|
catch (error) {
|
|
@@ -172,7 +172,7 @@ export class EmployeeExecution {
|
|
|
172
172
|
throw need('EMPLOYEE_EXEC_ALREADY_OPEN');
|
|
173
173
|
this.opening = true;
|
|
174
174
|
try {
|
|
175
|
-
|
|
175
|
+
this.gate(signal);
|
|
176
176
|
// A factory may finish after revoke/abort. Its late executor belongs to this binding and must close.
|
|
177
177
|
const created = Promise.resolve().then(() => { cancelled(signal); return this.options.transport(signal); }).then(async (io) => { if (this.ended || signal.aborted) {
|
|
178
178
|
this.closeTransport(io, { layer: 'executor', initiator: 'executor.late-factory', reason: this.error?.code ?? 'ABORTED', operation: 'open', action: 'close' }, observe);
|
|
@@ -187,7 +187,7 @@ export class EmployeeExecution {
|
|
|
187
187
|
} });
|
|
188
188
|
void this.factoryReceipt.catch(() => { });
|
|
189
189
|
await waiting(created, signal);
|
|
190
|
-
|
|
190
|
+
this.gate(signal);
|
|
191
191
|
this.executionId = randomUUID();
|
|
192
192
|
return { version: 1, executionId: this.executionId };
|
|
193
193
|
}
|
|
@@ -206,7 +206,7 @@ export class EmployeeExecution {
|
|
|
206
206
|
this.pendingRead = true;
|
|
207
207
|
let consumed = false;
|
|
208
208
|
try {
|
|
209
|
-
|
|
209
|
+
this.gate(signal);
|
|
210
210
|
const io = this.io;
|
|
211
211
|
if (!io)
|
|
212
212
|
throw need('EMPLOYEE_EXEC_CLOSED');
|
|
@@ -216,7 +216,7 @@ export class EmployeeExecution {
|
|
|
216
216
|
throw this.error;
|
|
217
217
|
return { version: 1, messages: [], closed: true };
|
|
218
218
|
}
|
|
219
|
-
|
|
219
|
+
this.gate(signal);
|
|
220
220
|
if (message === null) {
|
|
221
221
|
this.stop(undefined, { layer: 'executor', initiator: 'executor.read-eof', reason: 'EXECUTOR_EOF', operation: 'read', action: 'eof' }, observe);
|
|
222
222
|
return { version: 1, messages: [], closed: true };
|
|
@@ -248,16 +248,21 @@ export class EmployeeExecution {
|
|
|
248
248
|
}
|
|
249
249
|
}
|
|
250
250
|
}
|
|
251
|
-
|
|
252
|
-
export function createEmployeeProcessTransport(child, acpMaxMessageBytes, stderrCodes = [], observe) {
|
|
251
|
+
export function createEmployeeProcessTransport(child, acpMaxMessageBytes, stderrCodes = [], observe, readyMarker) {
|
|
253
252
|
capacity(acpMaxMessageBytes);
|
|
254
253
|
let partial = Buffer.alloc(0), queuedBytes = 0, writingBytes = 0, closed = false, reason;
|
|
255
254
|
let processClosed = false, terminationRequested = false;
|
|
256
255
|
const receipt = Promise.withResolvers();
|
|
257
256
|
void receipt.promise.catch(() => { });
|
|
258
257
|
let sawOutput = false, diagnosticCode, stderrPosition = 0;
|
|
258
|
+
const readiness = Promise.withResolvers();
|
|
259
|
+
void readiness.promise.catch(() => { });
|
|
260
|
+
let readySeen = readyMarker === undefined;
|
|
261
|
+
if (readySeen)
|
|
262
|
+
readiness.resolve();
|
|
259
263
|
const knownCodes = stderrCodes.filter(code => /^[A-Z][A-Z0-9_]*$/.test(code));
|
|
260
|
-
|
|
264
|
+
const stderrPatterns = readyMarker ? [...knownCodes, readyMarker] : knownCodes;
|
|
265
|
+
let stderrCandidates = stderrPatterns;
|
|
261
266
|
const queue = [];
|
|
262
267
|
let pending;
|
|
263
268
|
const writes = new Set();
|
|
@@ -290,6 +295,8 @@ export function createEmployeeProcessTransport(child, acpMaxMessageBytes, stderr
|
|
|
290
295
|
if (!closed) {
|
|
291
296
|
closed = true;
|
|
292
297
|
reason = error;
|
|
298
|
+
if (!readySeen)
|
|
299
|
+
readiness.reject(error ?? need('EMPLOYEE_EXEC_UNKNOWN'));
|
|
293
300
|
partial = Buffer.alloc(0);
|
|
294
301
|
if (error) {
|
|
295
302
|
queue.length = 0;
|
|
@@ -355,15 +362,19 @@ export function createEmployeeProcessTransport(child, acpMaxMessageBytes, stderr
|
|
|
355
362
|
});
|
|
356
363
|
// Match fixed startup codes byte by byte. No arbitrary diagnostic bytes are retained.
|
|
357
364
|
child.stderr.on('data', (chunk) => {
|
|
358
|
-
if (
|
|
365
|
+
if (closed || !stderrPatterns.length)
|
|
359
366
|
return;
|
|
360
367
|
for (const byte of chunk) {
|
|
361
368
|
if (byte === 10) {
|
|
362
369
|
const match = stderrCandidates.find(code => code.length === stderrPosition);
|
|
363
|
-
if (match)
|
|
370
|
+
if (match === readyMarker && !readySeen) {
|
|
371
|
+
readySeen = true;
|
|
372
|
+
readiness.resolve();
|
|
373
|
+
}
|
|
374
|
+
else if (match && !sawOutput && knownCodes.includes(match))
|
|
364
375
|
diagnosticCode = match;
|
|
365
376
|
stderrPosition = 0;
|
|
366
|
-
stderrCandidates =
|
|
377
|
+
stderrCandidates = stderrPatterns;
|
|
367
378
|
}
|
|
368
379
|
else if (stderrCandidates.length) {
|
|
369
380
|
stderrCandidates = stderrCandidates.filter(code => code.charCodeAt(stderrPosition) === byte);
|
|
@@ -377,6 +388,7 @@ export function createEmployeeProcessTransport(child, acpMaxMessageBytes, stderr
|
|
|
377
388
|
child.prependOnceListener('close', (code, signal) => observe?.({ layer: 'relay', initiator: 'ssh.process.close', reason: diagnosticCode ?? (code === 0 && !signal ? 'PROCESS_EXIT' : 'EMPLOYEE_EXEC_UNKNOWN'), action: 'exit', exitCode: code, signal }));
|
|
378
389
|
child.once('close', (code, signal) => { processClosed = true; finish(partial.length ? need('EMPLOYEE_EXEC_PROTOCOL_INVALID') : code === 0 && !signal ? undefined : need(diagnosticCode ?? 'EMPLOYEE_EXEC_UNKNOWN'), false, { layer: 'relay', initiator: 'ssh.process-close-settle', reason: diagnosticCode ?? (code === 0 && !signal ? 'PROCESS_EXIT' : 'EMPLOYEE_EXEC_UNKNOWN'), action: 'exit', exitCode: code, signal }); receipt.resolve(); });
|
|
379
390
|
return {
|
|
391
|
+
ready: readiness.promise,
|
|
380
392
|
write: async (message, signal) => {
|
|
381
393
|
cancelled(signal);
|
|
382
394
|
if (closed)
|
package/dist/employee-info.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/** Optional protocol information only. Malformed metadata cannot stop a turn. */
|
|
2
|
+
export interface EmployeeInitializationInfo {
|
|
3
|
+
phase: 'awaiting-request' | 'awaiting-response' | 'response-delivered';
|
|
4
|
+
}
|
|
2
5
|
export interface EmployeeExecutionInfo {
|
|
6
|
+
initialization?: EmployeeInitializationInfo;
|
|
3
7
|
version: 1;
|
|
4
|
-
httpMcpPolicy: '
|
|
8
|
+
httpMcpPolicy: 'ignore';
|
|
5
9
|
ignoredStdio: string[];
|
|
6
10
|
ignoredHttp: string[];
|
|
7
11
|
ignoredNativeHttp: string[];
|
package/dist/employee-info.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
2
2
|
const safeName = (value) => typeof value === 'string' && value.length > 0 && !/[\p{C}\r\n]/u.test(value);
|
|
3
3
|
export function employeeExecutionInfo(value) {
|
|
4
|
-
if (!object(value) || value.version !== 1 ||
|
|
4
|
+
if (!object(value) || value.version !== 1 || value.httpMcpPolicy !== 'ignore')
|
|
5
5
|
return;
|
|
6
6
|
const lists = {};
|
|
7
7
|
for (const key of ['ignoredStdio', 'ignoredHttp', 'ignoredNativeHttp']) {
|
|
@@ -12,25 +12,36 @@ export function employeeExecutionInfo(value) {
|
|
|
12
12
|
const unshown = value[key + 'Unshown'];
|
|
13
13
|
lists[key + 'Unshown'] = (Number.isSafeInteger(unshown) && Number(unshown) >= 0 ? Number(unshown) : 0) + entries.filter(entry => !safeName(entry)).length;
|
|
14
14
|
}
|
|
15
|
-
|
|
15
|
+
const initialization = employeeInitializationInfo(value.initialization);
|
|
16
|
+
return { version: 1, httpMcpPolicy: value.httpMcpPolicy, ...lists, ...(initialization ? { initialization } : {}) };
|
|
17
|
+
}
|
|
18
|
+
function employeeInitializationInfo(value) {
|
|
19
|
+
if (!object(value) || !['awaiting-request', 'awaiting-response', 'response-delivered'].includes(value.phase))
|
|
20
|
+
return;
|
|
21
|
+
return { phase: value.phase };
|
|
16
22
|
}
|
|
17
23
|
// Code-span delimiter exceeds every backtick run, so names cannot inject Markdown/HTML.
|
|
18
24
|
const quoted = (names) => names.map(name => { let longest = 0; for (const run of name.matchAll(/`+/g))
|
|
19
25
|
longest = Math.max(longest, run[0].length); const delimiter = '`'.repeat(longest + 1); return delimiter + ' ' + name + ' ' + delimiter; }).join('、');
|
|
20
26
|
export function employeeMcpSummary(info) {
|
|
21
27
|
const parts = [];
|
|
22
|
-
for (const [key, label] of [['ignoredStdio', '已忽略本机 stdio MCP'], ['ignoredHttp',
|
|
28
|
+
for (const [key, label] of [['ignoredStdio', '已忽略本机 stdio MCP'], ['ignoredHttp', '已忽略本机 HTTP MCP'], ['ignoredNativeHttp', '已按原生能力忽略本机 HTTP MCP']]) {
|
|
23
29
|
const names = info[key], unshown = info[key + 'Unshown'];
|
|
24
30
|
if (names.length || unshown)
|
|
25
31
|
parts.push(label + ': ' + [...(names.length ? [quoted(names)] : []), ...(unshown ? [`${unshown} 个名称含不可显示内容`] : [])].join(';'));
|
|
26
32
|
}
|
|
33
|
+
const init = info.initialization;
|
|
34
|
+
if (init) {
|
|
35
|
+
const phase = { 'awaiting-request': '未收到初始化请求', 'awaiting-response': '初始化回应尚未送达', 'response-delivered': '初始化回应已送达' }[init.phase];
|
|
36
|
+
parts.push('员工执行器初始化:' + phase);
|
|
37
|
+
}
|
|
27
38
|
return parts.join('。');
|
|
28
39
|
}
|
|
29
40
|
/** Employee-facing execution fact; protocol terms and stdio names remain in details. */
|
|
30
41
|
export function employeeHttpNotice(info, sanitizeLabel, showUnshown) {
|
|
31
|
-
const rawNames = [...new Set([...
|
|
42
|
+
const rawNames = [...new Set([...info.ignoredHttp, ...info.ignoredNativeHttp])];
|
|
32
43
|
const names = rawNames.filter(name => sanitizeLabel(name) === name && !name.includes('[已脱敏:'));
|
|
33
|
-
const unshown = showUnshown && (rawNames.length - names.length +
|
|
44
|
+
const unshown = showUnshown && (rawNames.length - names.length + info.ignoredHttpUnshown + info.ignoredNativeHttpUnshown) > 0;
|
|
34
45
|
if (!names.length)
|
|
35
46
|
return unshown ? '你电脑上的一项本地扩展本次不可用,详情见技术详情。' : '';
|
|
36
47
|
const named = names.map(name => '『' + quoted([name]) + '』').join('、');
|
|
@@ -39,12 +39,11 @@ export async function admitEmployeeLocation(options) {
|
|
|
39
39
|
throw fail('EMPLOYEE_CONTEXT_CHANGED');
|
|
40
40
|
return { request, local: { instanceId: c.instanceId, epoch: c.epoch, workspaceId: c.workspaceId, bindingId: c.bindingId, directory: structuredClone(c.directory) } };
|
|
41
41
|
};
|
|
42
|
-
const initial = await read(options.signal);
|
|
42
|
+
const initial = options.expectedLocal ? { request: { instanceId: connection.instanceId, epoch: connection.epoch, workspaceId: workspace.id, expectedBindingId: options.expectedLocal.bindingId }, local: structuredClone(options.expectedLocal) } : await read(options.signal);
|
|
43
43
|
if (options.expectedLocal && !same(initial.local, options.expectedLocal))
|
|
44
44
|
throw fail('EMPLOYEE_CONTEXT_CHANGED');
|
|
45
|
-
const prepare = async (signal) => { signal.throwIfAborted(); const
|
|
46
|
-
throw
|
|
47
|
-
throw new NeedError(result.error.code, result.error.message, result.error.needs); const fresh = endpointValue(result.value, initial.local, settingsPath); const after = await read(signal); if (!same(after, initial))
|
|
45
|
+
const prepare = async (signal) => { signal.throwIfAborted(); const result = await network.prepareEmployeeEndpoint(initial.request, signal); signal.throwIfAborted(); if (!result.ok)
|
|
46
|
+
throw new NeedError(result.error.code, result.error.message, result.error.needs); const fresh = endpointValue(result.value, initial.local, settingsPath); const state = api.getWorkspaceLocationState(); if (state.phase !== 'ready' || state.instanceId !== connection.instanceId || state.epoch !== connection.epoch || !state.items.some(row => row.workspaceId === workspace.id && row.bindingId === initial.local.bindingId && row.source === 'local' && row.status === 'selected'))
|
|
48
47
|
throw fail('EMPLOYEE_CONTEXT_CHANGED'); return fresh; };
|
|
49
48
|
const endpoint = await prepare(options.signal ?? new AbortController().signal);
|
|
50
49
|
return { cwd: initial.local.directory.path, bindingId: initial.local.bindingId, workspaceId: initial.local.workspaceId, endpoint, validate: async (signal) => { const fresh = await prepare(signal); if (!same(fresh, endpoint))
|