@ai-workbench/remote-agent 0.8.21 → 0.8.24

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.
Files changed (96) hide show
  1. package/README.md +28 -155
  2. package/dist/acp.d.ts +20 -15
  3. package/dist/acp.js +197 -224
  4. package/dist/agents.d.ts +0 -1
  5. package/dist/agents.js +2 -2
  6. package/dist/broker-runtime.d.ts +3 -5
  7. package/dist/broker-runtime.js +45 -342
  8. package/dist/broker.d.ts +6 -31
  9. package/dist/broker.js +42 -113
  10. package/dist/catalog-ssh.d.ts +2 -31
  11. package/dist/catalog-ssh.js +9 -39
  12. package/dist/claude-local.d.ts +1 -1
  13. package/dist/claude-local.js +40 -105
  14. package/dist/client.js +34 -127
  15. package/dist/close-diagnostics.d.ts +6 -20
  16. package/dist/close-diagnostics.js +13 -57
  17. package/dist/connect-contract.d.ts +4 -8
  18. package/dist/conversation-details.d.ts +0 -29
  19. package/dist/conversation-details.js +8 -99
  20. package/dist/employee-endpoint.d.ts +1 -3
  21. package/dist/employee-endpoint.js +8 -39
  22. package/dist/employee-exec.d.ts +4 -3
  23. package/dist/employee-exec.js +25 -13
  24. package/dist/employee-info.d.ts +5 -1
  25. package/dist/employee-info.js +16 -5
  26. package/dist/employee-location.js +3 -4
  27. package/dist/employee-mcp.js +44 -69
  28. package/dist/employee-native.d.ts +1 -3
  29. package/dist/employee-native.js +6 -27
  30. package/dist/employee-presentation.d.ts +0 -5
  31. package/dist/employee-presentation.js +0 -15
  32. package/dist/employee-provider.d.ts +3 -11
  33. package/dist/employee-provider.js +69 -205
  34. package/dist/employee-reasons.js +1 -5
  35. package/dist/employee-shell.d.ts +2 -1
  36. package/dist/employee-shell.js +10 -13
  37. package/dist/employee-ssh.d.ts +1 -3
  38. package/dist/employee-ssh.js +13 -16
  39. package/dist/employee-tools.d.ts +5 -5
  40. package/dist/failure-evidence.d.ts +2 -10
  41. package/dist/failure-evidence.js +12 -61
  42. package/dist/host-workspace.d.ts +4 -2
  43. package/dist/host-workspace.js +1 -1
  44. package/dist/index.d.ts +12 -42
  45. package/dist/index.js +203 -471
  46. package/dist/input.js +2 -3
  47. package/dist/local-user-turn.js +5 -6
  48. package/dist/mapping.d.ts +0 -3
  49. package/dist/mapping.js +0 -34
  50. package/dist/model-catalog.d.ts +2 -41
  51. package/dist/model-catalog.js +24 -282
  52. package/dist/model-configuration.d.ts +0 -3
  53. package/dist/model-configuration.js +8 -47
  54. package/dist/model-presentation.d.ts +1 -0
  55. package/dist/model-presentation.js +2 -2
  56. package/dist/permissions.d.ts +15 -0
  57. package/dist/permissions.js +30 -0
  58. package/dist/preflight-diagnostic.js +11 -22
  59. package/dist/preflight-payload.d.ts +1 -1
  60. package/dist/preflight-payload.js +2 -2
  61. package/dist/presentation.d.ts +0 -1
  62. package/dist/presentation.js +21 -42
  63. package/dist/provider.d.ts +12 -48
  64. package/dist/provider.js +18 -106
  65. package/dist/readiness.js +1 -7
  66. package/dist/remote-details.d.ts +41 -17
  67. package/dist/remote-details.js +84 -381
  68. package/dist/workspace-source.d.ts +1 -25
  69. package/dist/workspace-source.js +1 -138
  70. package/package.json +11 -12
  71. package/dist/catalog-diagnostics.d.ts +0 -16
  72. package/dist/catalog-diagnostics.js +0 -108
  73. package/dist/host-layout.d.ts +0 -8
  74. package/dist/host-layout.js +0 -9
  75. package/dist/job.d.ts +0 -17
  76. package/dist/job.js +0 -57
  77. package/dist/model-defaults.d.ts +0 -29
  78. package/dist/model-defaults.js +0 -62
  79. package/dist/oneshot.d.ts +0 -13
  80. package/dist/oneshot.js +0 -78
  81. package/dist/protocol-boundary.d.ts +0 -13
  82. package/dist/protocol-boundary.js +0 -49
  83. package/dist/provider-locale.d.ts +0 -19
  84. package/dist/provider-locale.js +0 -73
  85. package/dist/recovery-checkpoint.d.ts +0 -6
  86. package/dist/recovery-checkpoint.js +0 -71
  87. package/dist/recovery.d.ts +0 -51
  88. package/dist/recovery.js +0 -61
  89. package/dist/sdk-ssh-wrapper.d.ts +0 -2
  90. package/dist/sdk-ssh-wrapper.js +0 -34
  91. package/dist/stage-timing.d.ts +0 -31
  92. package/dist/stage-timing.js +0 -61
  93. package/dist/workspace-notice.d.ts +0 -47
  94. package/dist/workspace-notice.js +0 -273
  95. package/dist/workspace-source-confirmation.d.ts +0 -4
  96. 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 === 'changes' && Array.isArray(child))
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 (key === 'remoteReason' && (child === 'unavailable' || trustedEmployeeReason(child) !== undefined) || key === 'first' && typeof child === 'boolean')
330
+ if (['reason', 'remoteReason'].includes(key) && typeof child === 'string' && /^[A-Z][A-Z0-9_]*$/.test(child))
369
331
  return [key, child];
370
- if (['closeSequence', 'elapsedMs'].includes(key) && typeof child === 'number' && Number.isFinite(child) && child >= 0)
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' && ['REMOTE_CLOSE_FIRST', 'REMOTE_CLOSE_NEXT'].includes(observation.code) ? closeStructure(payload)
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' && observation.code === 'REMOTE_STAGE_TIMING' ? timingStructure(payload)
409
- : observation.kind === 'session' && ['CLAUDE_EMPLOYEE_OPERATION', 'CLAUDE_EMPLOYEE_PROCESS', 'CLAUDE_EMPLOYEE_REVOKED', 'CLAUDE_EMPLOYEE_SCOPE'].includes(observation.code) ? employeeStructure(payload)
410
- : observation.kind === 'need' ? payload
411
- : observation.kind === 'tool' || ['SDK_ITEM', 'SDK_TOOL', 'ACP_SESSION_UPDATE', 'ACP_IDLE_UPDATE'].includes(observation.code) ? toolStructure(payload)
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
- validate(callerSignal?: AbortSignal): Promise<EmployeeEndpointTarget>;
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, diagnosticFailure;
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 (error) {
50
- diagnosticFailure ??= error;
50
+ catch {
51
+ console.warn("EMPLOYEE_DIAGNOSTIC_DROPPED");
51
52
  }
52
53
  finally {
53
- lifetime.abort(diagnosticFailure ?? fail(code));
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, validate, validateDirectory, 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) {
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
- } if (diagnosticFailure)
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' });
@@ -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 declare function createEmployeeProcessTransport(child: ChildProcessWithoutNullStreams, acpMaxMessageBytes: number, stderrCodes?: readonly string[], observe?: CloseObserver): EmployeeExecutionTransport;
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;
@@ -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
- async gate(signal) { this.active(); cancelled(signal); await waiting(this.options.validate(signal), signal); this.active(); cancelled(signal); }
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
- await this.gate(current);
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
- await this.gate(current);
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
- await this.gate(signal);
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
- await this.gate(signal);
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
- await this.gate(signal);
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
- await this.gate(signal);
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
- /** NDJSON carrier with byte bounds and backpressure derived solely from acpMaxMessageBytes. */
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
- let stderrCandidates = knownCodes;
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 (sawOutput || closed || !knownCodes.length)
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 = knownCodes;
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)
@@ -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: 'reject' | 'ignore';
8
+ httpMcpPolicy: 'ignore';
5
9
  ignoredStdio: string[];
6
10
  ignoredHttp: string[];
7
11
  ignoredNativeHttp: string[];
@@ -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 || (value.httpMcpPolicy !== 'reject' && value.httpMcpPolicy !== 'ignore'))
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
- return { version: 1, httpMcpPolicy: value.httpMcpPolicy, ...lists };
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', info.httpMcpPolicy === 'ignore' ? '已按设置忽略本机 HTTP MCP' : '本机 HTTP MCP 未获支持,当前设置为拒绝'], ['ignoredNativeHttp', '已按原生能力忽略本机 HTTP MCP']]) {
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([...(info.httpMcpPolicy === 'ignore' ? info.ignoredHttp : []), ...info.ignoredNativeHttp])];
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 + (info.httpMcpPolicy === 'ignore' ? info.ignoredHttpUnshown : 0) + info.ignoredNativeHttpUnshown) > 0;
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 before = await read(signal); if (!same(before, initial))
46
- throw fail('EMPLOYEE_CONTEXT_CHANGED'); const result = await network.prepareEmployeeEndpoint(initial.request, signal); signal.throwIfAborted(); if (!result.ok)
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))