@zq-silk/yui 0.8.1 → 0.8.2
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/ARCHITECTURE.md +27 -28
- package/README.md +35 -46
- package/dist/cli/commandCatalog.js +49 -14
- package/dist/cli/interactionPolicy.js +4 -10
- package/dist/cli/invocationRouter.js +2 -1
- package/dist/cli.js +73 -21
- package/dist/commands/taskCommands.js +108 -53
- package/dist/controller/fileSchedulerStoreAdapter.js +389 -70
- package/dist/controller/resourceInventory.js +9 -5
- package/dist/controller/runtime.js +80 -7
- package/dist/controller/runtimeLaunchCoordinator.js +18 -78
- package/dist/controller/structuredProviderObservation.js +273 -0
- package/dist/executor/agentAdapter.js +40 -0
- package/dist/executor/agentExecutor.js +31 -7
- package/dist/executor/executorRegistry.js +11 -49
- package/dist/executor/fileRoleLaunchPlanner.js +115 -37
- package/dist/lifecycle/canonicalLifecycleEvent.js +5 -2
- package/dist/run/agentRun.js +2 -2
- package/dist/runtime/agentHost.js +767 -158
- package/dist/runtime/builtinAgentDrivers.js +1 -5
- package/dist/runtime/codexAppServerRuntime.js +67 -60
- package/dist/runtime/exactControlPlane.js +7 -2
- package/dist/runtime/index.js +6 -2
- package/dist/runtime/launchBroker.js +30 -8
- package/dist/runtime/providerAuthorityFence.js +24 -0
- package/dist/runtime/providerControl.js +63 -0
- package/dist/runtime/providerRecoveryDecision.js +55 -0
- package/dist/runtime/providerRuntimeIdentity.js +269 -19
- package/dist/runtime/runtimeBinding.js +20 -11
- package/dist/runtime/structuredProviderHost.js +476 -0
- package/dist/runtime/tmuxAdapters.js +143 -42
- package/dist/scheduler/activeRoleRunDelivery.js +206 -120
- package/dist/scheduler/leaderWakeupProcessor.js +141 -16
- package/dist/storage/migration/productionRegistry.js +111 -0
- package/dist/storage/taskStore.js +1 -1
- package/dist/tmux/tmuxManager.js +1 -1
- package/i18n/README.zh-CN.md +11 -8
- package/package.json +1 -1
|
@@ -1,16 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
2
|
import { appendFileSync, chmodSync, closeSync, fsyncSync, mkdirSync, openSync, rmSync } from "node:fs";
|
|
4
3
|
import { readdir, readFile, rename, unlink } from "node:fs/promises";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { dirname, join, resolve } from "node:path";
|
|
6
|
+
import { createConnection, createServer } from "node:net";
|
|
7
|
+
import { createInterface } from "node:readline";
|
|
8
|
+
import { callController, ControllerClientError } from "../core/controllerClient.js";
|
|
9
|
+
import { readHomeFilesystemId } from "../core/homeFilesystemIdentity.js";
|
|
10
|
+
import { publishStructuredProviderAccepted, publishStructuredProviderActivationTerminal, publishStructuredConversationRecoverability, publishStructuredProviderOpened, publishStructuredProviderTerminal } from "../controller/structuredProviderObservation.js";
|
|
8
11
|
import { validateAgentHostLaunchPayload } from "./launchBroker.js";
|
|
12
|
+
import { ProviderDeliveryUnknownError, ProviderConversationMissingError, ProviderTurnRejectedError, startStructuredProviderSession } from "./structuredProviderHost.js";
|
|
13
|
+
import { sameProviderAuthorityFence, validateProviderAuthorityFence } from "./providerAuthorityFence.js";
|
|
9
14
|
import { validateRuntimeProcessExitObservation } from "./processExitObservation.js";
|
|
10
15
|
import { readRuntimeStopReceipt, removeRuntimeStopReceipt } from "./runtimeStopReceipt.js";
|
|
11
|
-
export const AGENT_HOST_CONTROL_PROTOCOL = "yui-agent-host/
|
|
12
|
-
const HOST_CONTROL_TIMEOUT_MS =
|
|
13
|
-
const
|
|
16
|
+
export const AGENT_HOST_CONTROL_PROTOCOL = "yui-agent-host/v2";
|
|
17
|
+
const HOST_CONTROL_TIMEOUT_MS = 35_000;
|
|
18
|
+
const HOST_READY_TIMEOUT_MS = 35_000;
|
|
19
|
+
const HOST_CONTROL_MAX_BYTES = 32 * 1024;
|
|
14
20
|
export function serializeAgentHostLaunchControl(control) {
|
|
15
21
|
return JSON.stringify(validateControl(control));
|
|
16
22
|
}
|
|
@@ -18,65 +24,609 @@ export async function runAgentHost(input) {
|
|
|
18
24
|
const hostInstanceId = randomUUID();
|
|
19
25
|
let hostSequence = 0;
|
|
20
26
|
let payload = await redeem(input.home, input.launchId, input.ticket);
|
|
27
|
+
let session;
|
|
28
|
+
let sessionPayload;
|
|
29
|
+
let activeTurnPayload;
|
|
30
|
+
let activationId;
|
|
31
|
+
let conversationRecoverability = "unknown";
|
|
32
|
+
let authority;
|
|
33
|
+
let hostStopRequested = false;
|
|
34
|
+
let snapshot = hostSnapshot("idle");
|
|
35
|
+
let dispatchTail = Promise.resolve();
|
|
36
|
+
let promptHuman = () => { };
|
|
21
37
|
await replayExitOutbox(input.home);
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
38
|
+
const updateSnapshot = (next) => {
|
|
39
|
+
snapshot = validateSnapshot(next);
|
|
40
|
+
};
|
|
41
|
+
const authorityFields = () => authority === undefined ? {} : {
|
|
42
|
+
authorityEpoch: authority.epoch,
|
|
43
|
+
authorityOwner: authority.owner,
|
|
44
|
+
authorityHolderId: authority.holderId
|
|
45
|
+
};
|
|
46
|
+
const handleTerminal = (terminal) => {
|
|
47
|
+
const terminalPayload = activeTurnPayload;
|
|
48
|
+
if (terminalPayload === undefined)
|
|
49
|
+
return;
|
|
50
|
+
const terminalActivationId = activationId ?? terminalPayload.launchId;
|
|
51
|
+
void enqueueSerialized(async () => {
|
|
52
|
+
if (activeTurnPayload !== terminalPayload)
|
|
53
|
+
return;
|
|
54
|
+
if (session !== undefined) {
|
|
55
|
+
updateSnapshot(hostSnapshot("settling", {
|
|
56
|
+
launchId: terminalPayload.launchId,
|
|
57
|
+
adapterId: terminalPayload.providerControl.adapterId,
|
|
58
|
+
processInstanceId: session.processInstanceId,
|
|
59
|
+
nativeSessionId: terminal.nativeSessionId,
|
|
60
|
+
conversationId: terminal.conversationId,
|
|
61
|
+
nativeTurnId: terminal.nativeTurnId,
|
|
62
|
+
...authorityFields()
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
await publishStructuredProviderTerminal({
|
|
67
|
+
home: input.home,
|
|
68
|
+
environment: terminalPayload.environment,
|
|
69
|
+
activationId: terminalActivationId,
|
|
70
|
+
terminal
|
|
71
|
+
});
|
|
72
|
+
if (activeTurnPayload !== terminalPayload)
|
|
73
|
+
return;
|
|
74
|
+
activeTurnPayload = undefined;
|
|
75
|
+
if (session === undefined)
|
|
76
|
+
return;
|
|
77
|
+
const currentPayload = sessionPayload ?? terminalPayload;
|
|
78
|
+
updateSnapshot(hostSnapshot("idle", {
|
|
79
|
+
launchId: currentPayload.launchId,
|
|
80
|
+
adapterId: currentPayload.providerControl.adapterId,
|
|
81
|
+
processInstanceId: session.processInstanceId,
|
|
82
|
+
nativeSessionId: terminal.nativeSessionId,
|
|
83
|
+
conversationId: terminal.conversationId,
|
|
84
|
+
...authorityFields()
|
|
85
|
+
}));
|
|
86
|
+
promptHuman();
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
updateSnapshot(hostSnapshot("failed", {
|
|
90
|
+
launchId: terminalPayload.launchId,
|
|
91
|
+
adapterId: terminalPayload.providerControl.adapterId,
|
|
92
|
+
processInstanceId: session?.processInstanceId,
|
|
93
|
+
nativeSessionId: terminal.nativeSessionId,
|
|
94
|
+
conversationId: terminal.conversationId,
|
|
95
|
+
nativeTurnId: terminal.nativeTurnId,
|
|
96
|
+
...authorityFields(),
|
|
97
|
+
detail: errorText(error)
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
}).catch(() => { });
|
|
101
|
+
};
|
|
102
|
+
const observeExit = (providerSession, launched) => {
|
|
103
|
+
void providerSession.waitForExit().then((result) => enqueueSerialized(async () => {
|
|
104
|
+
const ownsCurrentSession = session === providerSession;
|
|
105
|
+
const currentPayload = ownsCurrentSession ? sessionPayload ?? launched : launched;
|
|
106
|
+
const currentActivationId = ownsCurrentSession
|
|
107
|
+
? activationId ?? launched.launchId
|
|
108
|
+
: launched.launchId;
|
|
109
|
+
const exitAuthority = authorityFields();
|
|
110
|
+
if (ownsCurrentSession) {
|
|
111
|
+
session = undefined;
|
|
112
|
+
activationId = undefined;
|
|
113
|
+
conversationRecoverability = "unknown";
|
|
114
|
+
authority = undefined;
|
|
28
115
|
}
|
|
29
|
-
control.setActive(true, payload.launchId);
|
|
30
|
-
const result = await runAgentHostProviderChild(payload);
|
|
31
|
-
control.setActive(false);
|
|
32
116
|
hostSequence += 1;
|
|
33
|
-
const stopReceipt = readRuntimeStopReceipt(input.home,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
117
|
+
const stopReceipt = readRuntimeStopReceipt(input.home, currentPayload.launchId);
|
|
118
|
+
const observedAt = new Date().toISOString();
|
|
119
|
+
const failures = [];
|
|
120
|
+
try {
|
|
121
|
+
await publishStructuredProviderActivationTerminal({
|
|
122
|
+
home: input.home,
|
|
123
|
+
environment: currentPayload.environment,
|
|
124
|
+
conversationId: providerSession.conversationId,
|
|
125
|
+
nativeSessionId: providerSession.nativeSessionId,
|
|
126
|
+
activationId: currentActivationId,
|
|
127
|
+
status: stopReceipt !== null || hostStopRequested ? "ended" : "failed",
|
|
128
|
+
observedAt
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
failures.push(`activation terminal: ${errorText(error)}`);
|
|
133
|
+
}
|
|
134
|
+
let exitPersisted = false;
|
|
135
|
+
try {
|
|
136
|
+
await persistAndSubmitExit(input.home, validateRuntimeProcessExitObservation({
|
|
137
|
+
schemaVersion: 1,
|
|
138
|
+
observationId: `${hostInstanceId}-${hostSequence}`,
|
|
139
|
+
hostSequence,
|
|
140
|
+
hostInstanceId,
|
|
141
|
+
providerProcessInstanceId: result.processInstanceId,
|
|
142
|
+
...(currentPayload.environment.YUI_TASK_ID === undefined
|
|
143
|
+
? {}
|
|
144
|
+
: { taskId: currentPayload.environment.YUI_TASK_ID }),
|
|
145
|
+
roleName: currentPayload.environment.YUI_ROLE ?? "unknown-role",
|
|
146
|
+
...(currentPayload.environment.YUI_RUN_ID === undefined
|
|
147
|
+
? {}
|
|
148
|
+
: { runId: currentPayload.environment.YUI_RUN_ID }),
|
|
149
|
+
launchId: currentPayload.launchId,
|
|
150
|
+
...(providerSession.nativeSessionId.length === 0
|
|
151
|
+
? {}
|
|
152
|
+
: { nativeSessionId: providerSession.nativeSessionId }),
|
|
153
|
+
processKind: "provider-child",
|
|
154
|
+
...(result.code === null ? {} : { exitCode: result.code }),
|
|
155
|
+
...(result.signal === null ? {} : { signal: result.signal }),
|
|
156
|
+
...(stopReceipt === null ? {} : { stopReceiptId: stopReceipt.receiptId }),
|
|
157
|
+
observedAt
|
|
158
|
+
}));
|
|
159
|
+
exitPersisted = true;
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
failures.push(`process exit: ${errorText(error)}`);
|
|
163
|
+
}
|
|
164
|
+
if (stopReceipt !== null && exitPersisted) {
|
|
165
|
+
removeRuntimeStopReceipt(input.home, currentPayload.launchId);
|
|
166
|
+
}
|
|
167
|
+
if (hostStopRequested)
|
|
168
|
+
return;
|
|
169
|
+
updateSnapshot(hostSnapshot(failures.length === 0 ? "exited" : "failed", {
|
|
170
|
+
launchId: currentPayload.launchId,
|
|
171
|
+
adapterId: currentPayload.providerControl?.adapterId,
|
|
172
|
+
processInstanceId: result.processInstanceId,
|
|
173
|
+
nativeSessionId: providerSession.nativeSessionId,
|
|
174
|
+
conversationId: providerSession.conversationId,
|
|
175
|
+
...exitAuthority,
|
|
176
|
+
...(failures.length !== 0
|
|
177
|
+
? { detail: failures.join("; ") }
|
|
178
|
+
: activeTurnPayload === undefined
|
|
179
|
+
? {}
|
|
180
|
+
: { detail: "Provider process exited before the active Turn reached a terminal boundary." })
|
|
181
|
+
}));
|
|
182
|
+
})).catch((error) => updateSnapshot(hostSnapshot("failed", {
|
|
183
|
+
launchId: launched.launchId,
|
|
184
|
+
adapterId: launched.providerControl?.adapterId,
|
|
185
|
+
processInstanceId: providerSession.processInstanceId,
|
|
186
|
+
...authorityFields(),
|
|
187
|
+
detail: errorText(error)
|
|
188
|
+
})));
|
|
189
|
+
};
|
|
190
|
+
const dispatch = async (next) => {
|
|
191
|
+
const providerControl = next.providerControl;
|
|
192
|
+
if (providerControl === undefined) {
|
|
193
|
+
throw new Error("Agent Host accepts only managed Provider control launches.");
|
|
194
|
+
}
|
|
195
|
+
if (activeTurnPayload !== undefined
|
|
196
|
+
|| ["starting", "ready", "settling", "delivery-unknown"].includes(snapshot.state)) {
|
|
197
|
+
throw new Error("Agent Host still owns an unsettled Provider Turn.");
|
|
198
|
+
}
|
|
199
|
+
const requestedAuthority = validateProviderAuthorityFence(providerControl.authority);
|
|
200
|
+
if (authority === undefined)
|
|
201
|
+
authority = requestedAuthority;
|
|
202
|
+
else if (!sameProviderAuthorityFence(authority, requestedAuthority)) {
|
|
203
|
+
throw new Error("Agent Host launch carries a stale Provider authority fence.");
|
|
204
|
+
}
|
|
205
|
+
updateSnapshot(hostSnapshot("starting", {
|
|
206
|
+
launchId: next.launchId,
|
|
207
|
+
adapterId: providerControl.adapterId,
|
|
208
|
+
...(providerControl.initialTurn === undefined
|
|
209
|
+
? {}
|
|
210
|
+
: { attemptId: providerControl.initialTurn.attemptId }),
|
|
211
|
+
...(session === undefined ? {} : {
|
|
212
|
+
processInstanceId: session.processInstanceId,
|
|
213
|
+
nativeSessionId: session.nativeSessionId,
|
|
214
|
+
conversationId: session.conversationId
|
|
215
|
+
}),
|
|
216
|
+
...authorityFields()
|
|
217
|
+
}));
|
|
218
|
+
let providerAcceptedAttemptId;
|
|
219
|
+
let durableInitialTurn;
|
|
220
|
+
try {
|
|
221
|
+
if (session !== undefined) {
|
|
222
|
+
if (session.adapterId !== providerControl.adapterId
|
|
223
|
+
|| providerControl.mode !== "resume"
|
|
224
|
+
|| providerControl.nativeSessionId !== session.nativeSessionId) {
|
|
225
|
+
throw new Error("Agent Host launch does not match its live Provider Conversation.");
|
|
226
|
+
}
|
|
227
|
+
sessionPayload = next;
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
activationId = next.launchId;
|
|
231
|
+
conversationRecoverability = providerControl.adapterId === "codex"
|
|
232
|
+
? "recoverable"
|
|
233
|
+
: "unknown";
|
|
234
|
+
const started = await startStructuredProviderSession(next, { onTerminal: handleTerminal });
|
|
235
|
+
session = started.session;
|
|
236
|
+
sessionPayload = next;
|
|
237
|
+
observeExit(started.session, next);
|
|
238
|
+
}
|
|
239
|
+
await publishStructuredProviderOpened({
|
|
240
|
+
home: input.home,
|
|
241
|
+
environment: next.environment,
|
|
242
|
+
conversationId: session.conversationId,
|
|
243
|
+
nativeSessionId: session.nativeSessionId,
|
|
244
|
+
activationId: activationId ?? next.launchId,
|
|
245
|
+
recoverability: conversationRecoverability,
|
|
55
246
|
observedAt: new Date().toISOString()
|
|
247
|
+
});
|
|
248
|
+
let receipt;
|
|
249
|
+
if (providerControl.initialTurn !== undefined) {
|
|
250
|
+
durableInitialTurn = hostTurnControlParams(next, session.nativeSessionId, requestedAuthority, providerControl.initialTurn.attemptId);
|
|
251
|
+
await beginDurableProviderTurn(input.home, durableInitialTurn);
|
|
252
|
+
activeTurnPayload = next;
|
|
253
|
+
try {
|
|
254
|
+
receipt = await session.submitTurn(providerControl.initialTurn);
|
|
255
|
+
providerAcceptedAttemptId = receipt.attemptId;
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
await resolveProviderTurnSubmission(input.home, durableInitialTurn, error);
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (receipt !== undefined) {
|
|
263
|
+
conversationRecoverability = "recoverable";
|
|
264
|
+
try {
|
|
265
|
+
await publishStructuredProviderAccepted({
|
|
266
|
+
home: input.home,
|
|
267
|
+
environment: next.environment,
|
|
268
|
+
activationId: activationId ?? next.launchId,
|
|
269
|
+
receipt
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
await resolveProviderTurnSubmission(input.home, durableInitialTurn, new ProviderDeliveryUnknownError(`Provider accepted input but its durable acknowledgement could not be confirmed: ${errorText(error)}`, receipt.attemptId));
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
updateSnapshot(hostSnapshot(receipt === undefined ? "idle" : "ready", {
|
|
278
|
+
launchId: next.launchId,
|
|
279
|
+
adapterId: providerControl.adapterId,
|
|
280
|
+
processInstanceId: session.processInstanceId,
|
|
281
|
+
nativeSessionId: receipt?.nativeSessionId ?? session.nativeSessionId,
|
|
282
|
+
conversationId: receipt?.conversationId ?? session.conversationId,
|
|
283
|
+
...(receipt === undefined ? {} : {
|
|
284
|
+
attemptId: receipt.attemptId,
|
|
285
|
+
nativeTurnId: receipt.nativeTurnId
|
|
286
|
+
}),
|
|
287
|
+
...authorityFields()
|
|
288
|
+
}));
|
|
289
|
+
return snapshot;
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
if (error instanceof ProviderConversationMissingError) {
|
|
293
|
+
await publishStructuredConversationRecoverability({
|
|
294
|
+
home: input.home,
|
|
295
|
+
environment: next.environment,
|
|
296
|
+
conversationId: error.conversationId,
|
|
297
|
+
activationId: activationId ?? next.launchId,
|
|
298
|
+
recoverability: "unrecoverable",
|
|
299
|
+
observedAt: new Date().toISOString()
|
|
300
|
+
}).catch(() => { });
|
|
301
|
+
}
|
|
302
|
+
if (session === undefined) {
|
|
303
|
+
activationId = undefined;
|
|
304
|
+
conversationRecoverability = "unknown";
|
|
305
|
+
authority = undefined;
|
|
306
|
+
}
|
|
307
|
+
const deliveryUnknown = error instanceof ProviderDeliveryUnknownError
|
|
308
|
+
|| providerAcceptedAttemptId !== undefined;
|
|
309
|
+
const state = deliveryUnknown
|
|
310
|
+
? "delivery-unknown"
|
|
311
|
+
: error instanceof ProviderTurnRejectedError ? "rejected" : "failed";
|
|
312
|
+
updateSnapshot(hostSnapshot(state, {
|
|
313
|
+
launchId: next.launchId,
|
|
314
|
+
adapterId: providerControl.adapterId,
|
|
315
|
+
processInstanceId: session?.processInstanceId,
|
|
316
|
+
nativeSessionId: session?.nativeSessionId ?? providerControl.nativeSessionId,
|
|
317
|
+
conversationId: session?.conversationId ?? providerControl.nativeSessionId,
|
|
318
|
+
...(providerControl.initialTurn === undefined
|
|
319
|
+
? {}
|
|
320
|
+
: { attemptId: providerControl.initialTurn.attemptId }),
|
|
321
|
+
...authorityFields(),
|
|
322
|
+
detail: errorText(error)
|
|
323
|
+
}));
|
|
324
|
+
if (state !== "delivery-unknown")
|
|
325
|
+
activeTurnPayload = undefined;
|
|
326
|
+
if (deliveryUnknown && !(error instanceof ProviderDeliveryUnknownError)) {
|
|
327
|
+
throw new ProviderDeliveryUnknownError(`Provider accepted input but its durable acknowledgement could not be confirmed: ${errorText(error)}`, providerAcceptedAttemptId);
|
|
328
|
+
}
|
|
329
|
+
throw error;
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
const enqueueSerialized = (action) => {
|
|
333
|
+
const operation = dispatchTail.then(action);
|
|
334
|
+
dispatchTail = operation.then(() => undefined, () => undefined);
|
|
335
|
+
return operation;
|
|
336
|
+
};
|
|
337
|
+
const enqueueDispatch = (next) => (enqueueSerialized(() => dispatch(next)));
|
|
338
|
+
const submitTurn = async (request) => {
|
|
339
|
+
if (session === undefined || sessionPayload === undefined) {
|
|
340
|
+
throw new Error("Agent Host has no live Provider Conversation.");
|
|
341
|
+
}
|
|
342
|
+
if (request.nativeSessionId !== session.nativeSessionId) {
|
|
343
|
+
throw new Error("Agent Host Turn targets a different Provider Conversation.");
|
|
344
|
+
}
|
|
345
|
+
if (authority === undefined
|
|
346
|
+
|| !sameProviderAuthorityFence(authority, request.authority)) {
|
|
347
|
+
throw new Error("Agent Host rejected a stale Provider writer fence.");
|
|
348
|
+
}
|
|
349
|
+
if (activeTurnPayload !== undefined || snapshot.state === "settling") {
|
|
350
|
+
throw new Error("Agent Host still owns an unsettled Provider Turn.");
|
|
351
|
+
}
|
|
352
|
+
activeTurnPayload = sessionPayload;
|
|
353
|
+
updateSnapshot(hostSnapshot("starting", {
|
|
354
|
+
launchId: request.launchId,
|
|
355
|
+
adapterId: session.adapterId,
|
|
356
|
+
processInstanceId: session.processInstanceId,
|
|
357
|
+
nativeSessionId: session.nativeSessionId,
|
|
358
|
+
conversationId: session.conversationId,
|
|
359
|
+
attemptId: request.turn.attemptId,
|
|
360
|
+
...authorityFields()
|
|
361
|
+
}));
|
|
362
|
+
let providerAccepted = false;
|
|
363
|
+
try {
|
|
364
|
+
const receipt = await session.submitTurn(request.turn);
|
|
365
|
+
providerAccepted = true;
|
|
366
|
+
await publishStructuredProviderAccepted({
|
|
367
|
+
home: input.home,
|
|
368
|
+
environment: sessionPayload.environment,
|
|
369
|
+
activationId: activationId ?? sessionPayload.launchId,
|
|
370
|
+
receipt
|
|
371
|
+
});
|
|
372
|
+
updateSnapshot(hostSnapshot("ready", {
|
|
373
|
+
launchId: request.launchId,
|
|
374
|
+
adapterId: session.adapterId,
|
|
375
|
+
processInstanceId: session.processInstanceId,
|
|
376
|
+
nativeSessionId: receipt.nativeSessionId,
|
|
377
|
+
conversationId: receipt.conversationId,
|
|
378
|
+
attemptId: receipt.attemptId,
|
|
379
|
+
nativeTurnId: receipt.nativeTurnId,
|
|
380
|
+
...authorityFields()
|
|
56
381
|
}));
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
382
|
+
return snapshot;
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
const deliveryUnknown = error instanceof ProviderDeliveryUnknownError || providerAccepted;
|
|
386
|
+
const state = deliveryUnknown
|
|
387
|
+
? "delivery-unknown"
|
|
388
|
+
: error instanceof ProviderTurnRejectedError ? "rejected" : "failed";
|
|
389
|
+
updateSnapshot(hostSnapshot(state, {
|
|
390
|
+
launchId: request.launchId,
|
|
391
|
+
adapterId: session.adapterId,
|
|
392
|
+
processInstanceId: session.processInstanceId,
|
|
393
|
+
nativeSessionId: session.nativeSessionId,
|
|
394
|
+
conversationId: session.conversationId,
|
|
395
|
+
attemptId: request.turn.attemptId,
|
|
396
|
+
...authorityFields(),
|
|
397
|
+
detail: errorText(error)
|
|
398
|
+
}));
|
|
399
|
+
if (state !== "delivery-unknown")
|
|
400
|
+
activeTurnPayload = undefined;
|
|
401
|
+
if (deliveryUnknown && !(error instanceof ProviderDeliveryUnknownError)) {
|
|
402
|
+
throw new ProviderDeliveryUnknownError(`Provider accepted input but its durable acknowledgement could not be confirmed: ${errorText(error)}`, request.turn.attemptId);
|
|
403
|
+
}
|
|
404
|
+
throw error;
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
const setAuthority = (request) => {
|
|
408
|
+
if (session === undefined || request.nativeSessionId !== session.nativeSessionId) {
|
|
409
|
+
throw new Error("Agent Host authority targets a different Provider Conversation.");
|
|
410
|
+
}
|
|
411
|
+
if (activeTurnPayload !== undefined
|
|
412
|
+
|| ["starting", "ready", "settling", "delivery-unknown"].includes(snapshot.state)) {
|
|
413
|
+
throw new Error("Agent Host authority cannot transfer while a Turn is unsettled.");
|
|
414
|
+
}
|
|
415
|
+
const next = validateProviderAuthorityFence(request.authority);
|
|
416
|
+
if (authority !== undefined) {
|
|
417
|
+
if (sameProviderAuthorityFence(authority, next))
|
|
418
|
+
return snapshot;
|
|
419
|
+
if (next.epoch <= authority.epoch) {
|
|
420
|
+
throw new Error("Agent Host authority epoch did not advance monotonically.");
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
authority = next;
|
|
424
|
+
updateSnapshot(hostSnapshot("idle", {
|
|
425
|
+
launchId: sessionPayload?.launchId ?? snapshot.launchId,
|
|
426
|
+
adapterId: session.adapterId,
|
|
427
|
+
processInstanceId: session.processInstanceId,
|
|
428
|
+
nativeSessionId: session.nativeSessionId,
|
|
429
|
+
conversationId: session.conversationId,
|
|
430
|
+
...authorityFields()
|
|
431
|
+
}));
|
|
432
|
+
promptHuman();
|
|
433
|
+
return snapshot;
|
|
434
|
+
};
|
|
435
|
+
const control = await openAgentHostControl(input.home, payload, () => snapshot, async (request) => {
|
|
436
|
+
if (request.type === "status") {
|
|
437
|
+
return controlResult("status", snapshot);
|
|
438
|
+
}
|
|
439
|
+
if (request.type === "submit-turn") {
|
|
440
|
+
const accepted = await enqueueSerialized(() => submitTurn(request));
|
|
441
|
+
return controlResult("accepted", accepted);
|
|
66
442
|
}
|
|
443
|
+
if (request.type === "set-authority") {
|
|
444
|
+
const accepted = await enqueueSerialized(async () => setAuthority(request));
|
|
445
|
+
return controlResult("accepted", accepted);
|
|
446
|
+
}
|
|
447
|
+
if (snapshot.launchId === request.launchId
|
|
448
|
+
&& ["starting", "ready", "settling", "delivery-unknown"].includes(snapshot.state)) {
|
|
449
|
+
return controlResult("active-same-launch", snapshot);
|
|
450
|
+
}
|
|
451
|
+
const redeemed = await redeem(input.home, request.launchId, request.ticket);
|
|
452
|
+
if (activeTurnPayload !== undefined
|
|
453
|
+
|| ["starting", "ready", "settling", "delivery-unknown"].includes(snapshot.state)) {
|
|
454
|
+
return controlResult("active-other-launch", snapshot);
|
|
455
|
+
}
|
|
456
|
+
const accepted = await enqueueDispatch(redeemed);
|
|
457
|
+
return controlResult("accepted", accepted);
|
|
458
|
+
});
|
|
459
|
+
const humanConsole = process.stdin.isTTY
|
|
460
|
+
? createInterface({ input: process.stdin, output: process.stdout, terminal: true })
|
|
461
|
+
: undefined;
|
|
462
|
+
promptHuman = () => {
|
|
463
|
+
if (humanConsole !== undefined && authority?.owner === "human"
|
|
464
|
+
&& activeTurnPayload === undefined
|
|
465
|
+
&& ["idle", "rejected", "failed"].includes(snapshot.state)) {
|
|
466
|
+
humanConsole.setPrompt("yui(provider)> ");
|
|
467
|
+
humanConsole.prompt();
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
humanConsole?.on("line", (line) => {
|
|
471
|
+
void enqueueSerialized(async () => {
|
|
472
|
+
const currentAuthority = authority;
|
|
473
|
+
const currentSession = session;
|
|
474
|
+
const currentPayload = sessionPayload;
|
|
475
|
+
if (currentAuthority?.owner !== "human"
|
|
476
|
+
|| currentSession === undefined
|
|
477
|
+
|| currentPayload === undefined) {
|
|
478
|
+
process.stderr.write("Provider input rejected: human authority is not active.\n");
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const boundedText = line.trim();
|
|
482
|
+
if (boundedText.length === 0)
|
|
483
|
+
return;
|
|
484
|
+
const attemptId = `human:${currentAuthority.holderId}:${randomUUID()}`;
|
|
485
|
+
const turnControl = {
|
|
486
|
+
protocol: AGENT_HOST_CONTROL_PROTOCOL,
|
|
487
|
+
type: "submit-turn",
|
|
488
|
+
launchId: currentPayload.launchId,
|
|
489
|
+
nativeSessionId: currentSession.nativeSessionId,
|
|
490
|
+
authority: currentAuthority,
|
|
491
|
+
turn: {
|
|
492
|
+
attemptId,
|
|
493
|
+
boundedText
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
const durableTurn = hostTurnControlParams(currentPayload, currentSession.nativeSessionId, currentAuthority, attemptId);
|
|
497
|
+
try {
|
|
498
|
+
await beginDurableProviderTurn(input.home, durableTurn);
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
await callController(input.home, "runtime.provider-turn-submission-resolve", {
|
|
502
|
+
...durableTurn,
|
|
503
|
+
status: "rejected",
|
|
504
|
+
reason: `Human Turn intent acknowledgement failed before Provider write: ${errorText(error)}`,
|
|
505
|
+
observedAt: new Date().toISOString()
|
|
506
|
+
}).catch(() => { });
|
|
507
|
+
throw error;
|
|
508
|
+
}
|
|
509
|
+
try {
|
|
510
|
+
await submitTurn(turnControl);
|
|
511
|
+
}
|
|
512
|
+
catch (error) {
|
|
513
|
+
await callController(input.home, "runtime.provider-turn-submission-resolve", {
|
|
514
|
+
...durableTurn,
|
|
515
|
+
status: error instanceof ProviderDeliveryUnknownError
|
|
516
|
+
? "delivery-unknown"
|
|
517
|
+
: "rejected",
|
|
518
|
+
reason: errorText(error),
|
|
519
|
+
observedAt: new Date().toISOString()
|
|
520
|
+
}).catch(() => { });
|
|
521
|
+
throw error;
|
|
522
|
+
}
|
|
523
|
+
process.stdout.write("Provider accepted the human Turn; waiting for its terminal boundary.\n");
|
|
524
|
+
}).catch((error) => {
|
|
525
|
+
process.stderr.write(`Provider input failed: ${errorText(error)}\n`);
|
|
526
|
+
promptHuman();
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
promptHuman();
|
|
530
|
+
let stopResolve;
|
|
531
|
+
const stopped = new Promise((resolvePromise) => {
|
|
532
|
+
stopResolve = resolvePromise;
|
|
533
|
+
});
|
|
534
|
+
const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
535
|
+
const handlers = new Map();
|
|
536
|
+
let forceKillTimer;
|
|
537
|
+
for (const signal of signals) {
|
|
538
|
+
const handler = () => {
|
|
539
|
+
if (hostStopRequested)
|
|
540
|
+
return;
|
|
541
|
+
hostStopRequested = true;
|
|
542
|
+
session?.terminate(signal);
|
|
543
|
+
forceKillTimer = setTimeout(() => session?.terminate("SIGKILL"), 10_000);
|
|
544
|
+
forceKillTimer.unref();
|
|
545
|
+
stopResolve();
|
|
546
|
+
};
|
|
547
|
+
handlers.set(signal, handler);
|
|
548
|
+
process.on(signal, handler);
|
|
549
|
+
}
|
|
550
|
+
try {
|
|
551
|
+
if (payload.startMode === "provider") {
|
|
552
|
+
void enqueueDispatch(payload).catch(() => { });
|
|
553
|
+
}
|
|
554
|
+
await stopped;
|
|
555
|
+
return 0;
|
|
67
556
|
}
|
|
68
557
|
finally {
|
|
558
|
+
for (const [signal, handler] of handlers)
|
|
559
|
+
process.removeListener(signal, handler);
|
|
560
|
+
if (forceKillTimer !== undefined)
|
|
561
|
+
clearTimeout(forceKillTimer);
|
|
562
|
+
humanConsole?.close();
|
|
563
|
+
session?.terminate("SIGTERM");
|
|
69
564
|
await control.close();
|
|
565
|
+
void sessionPayload;
|
|
70
566
|
}
|
|
71
567
|
}
|
|
72
568
|
export function agentHostControlSocketPath(input) {
|
|
73
569
|
const owner = input.scope === "task" ? input.taskId ?? "missing-task" : "global";
|
|
74
|
-
const
|
|
75
|
-
.
|
|
76
|
-
.
|
|
77
|
-
|
|
570
|
+
const homeDigest = createHash("sha256")
|
|
571
|
+
.update(readHomeFilesystemId(resolve(input.home)))
|
|
572
|
+
.digest("hex")
|
|
573
|
+
.slice(0, 16);
|
|
574
|
+
const ownerDigest = createHash("sha256")
|
|
575
|
+
.update(`${input.scope}\0${owner}\0${input.roleName}`)
|
|
576
|
+
.digest("hex")
|
|
577
|
+
.slice(0, 16);
|
|
578
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : 0;
|
|
579
|
+
// Linux sockaddr_un paths have a small fixed budget. Keep the endpoint
|
|
580
|
+
// independent of a potentially deep YUI_HOME while fencing aliases and
|
|
581
|
+
// copied Homes by their physical filesystem identity.
|
|
582
|
+
const root = process.platform === "linux" ? "/tmp" : tmpdir();
|
|
583
|
+
return join(root, `yui-${uid}`, "agent-host", `${homeDigest}-${ownerDigest}.sock`);
|
|
78
584
|
}
|
|
79
585
|
export async function sendAgentHostLaunchControl(input) {
|
|
586
|
+
return await sendAgentHostControl(input);
|
|
587
|
+
}
|
|
588
|
+
export async function sendAgentHostTurnControl(input) {
|
|
589
|
+
return await sendAgentHostControl(input);
|
|
590
|
+
}
|
|
591
|
+
export async function sendAgentHostAuthorityControl(input) {
|
|
592
|
+
return await sendAgentHostControl(input);
|
|
593
|
+
}
|
|
594
|
+
export async function inspectAgentHost(input) {
|
|
595
|
+
const result = await sendAgentHostControl({
|
|
596
|
+
...input,
|
|
597
|
+
control: { protocol: AGENT_HOST_CONTROL_PROTOCOL, type: "status" }
|
|
598
|
+
});
|
|
599
|
+
return result.snapshot;
|
|
600
|
+
}
|
|
601
|
+
export async function waitForAgentHostLaunchAck(input) {
|
|
602
|
+
const deadline = Date.now() + (input.timeoutMs ?? HOST_READY_TIMEOUT_MS);
|
|
603
|
+
let lastError;
|
|
604
|
+
while (Date.now() < deadline) {
|
|
605
|
+
try {
|
|
606
|
+
const snapshot = await inspectAgentHost(input);
|
|
607
|
+
if (snapshot.launchId === input.launchId) {
|
|
608
|
+
if (snapshot.state === "ready"
|
|
609
|
+
|| (input.requireTurnAck !== true && snapshot.state === "idle"))
|
|
610
|
+
return snapshot;
|
|
611
|
+
if (snapshot.state === "delivery-unknown" || snapshot.state === "rejected") {
|
|
612
|
+
return snapshot;
|
|
613
|
+
}
|
|
614
|
+
if (snapshot.state === "failed" || snapshot.state === "exited") {
|
|
615
|
+
throw new Error(`Agent Host Provider launch ${snapshot.state}: ${snapshot.detail ?? "no detail"}.`);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
catch (error) {
|
|
620
|
+
lastError = error;
|
|
621
|
+
const code = error.code;
|
|
622
|
+
if (code !== "ENOENT" && code !== "ECONNREFUSED")
|
|
623
|
+
throw error;
|
|
624
|
+
}
|
|
625
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, 50));
|
|
626
|
+
}
|
|
627
|
+
throw new Error(`Agent Host did not acknowledge Provider launch ${input.launchId}: ${errorText(lastError)}.`);
|
|
628
|
+
}
|
|
629
|
+
async function sendAgentHostControl(input) {
|
|
80
630
|
const path = agentHostControlSocketPath(input);
|
|
81
631
|
return await new Promise((resolvePromise, reject) => {
|
|
82
632
|
const client = createConnection(path);
|
|
@@ -85,12 +635,16 @@ export async function sendAgentHostLaunchControl(input) {
|
|
|
85
635
|
client.destroy();
|
|
86
636
|
reject(new Error("Agent Host control request timed out."));
|
|
87
637
|
}, HOST_CONTROL_TIMEOUT_MS);
|
|
638
|
+
let settled = false;
|
|
88
639
|
const settle = (callback, value) => {
|
|
640
|
+
if (settled)
|
|
641
|
+
return;
|
|
642
|
+
settled = true;
|
|
89
643
|
clearTimeout(timer);
|
|
90
644
|
callback(value);
|
|
91
645
|
};
|
|
92
646
|
client.setEncoding("utf8");
|
|
93
|
-
client.once("connect", () => client.end(`${
|
|
647
|
+
client.once("connect", () => client.end(`${JSON.stringify(validateControl(input.control))}\n`));
|
|
94
648
|
client.on("data", (chunk) => {
|
|
95
649
|
response += chunk;
|
|
96
650
|
if (Buffer.byteLength(response, "utf8") > HOST_CONTROL_MAX_BYTES) {
|
|
@@ -99,12 +653,12 @@ export async function sendAgentHostLaunchControl(input) {
|
|
|
99
653
|
});
|
|
100
654
|
client.once("error", (error) => settle(reject, error));
|
|
101
655
|
client.once("close", () => {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
656
|
+
try {
|
|
657
|
+
settle(resolvePromise, validateControlResult(JSON.parse(response.trim())));
|
|
658
|
+
}
|
|
659
|
+
catch (error) {
|
|
660
|
+
settle(reject, error);
|
|
661
|
+
}
|
|
108
662
|
});
|
|
109
663
|
});
|
|
110
664
|
}
|
|
@@ -116,66 +670,6 @@ async function redeem(home, launchId, ticket) {
|
|
|
116
670
|
});
|
|
117
671
|
return validateAgentHostLaunchPayload(result);
|
|
118
672
|
}
|
|
119
|
-
export async function runAgentHostProviderChild(payload) {
|
|
120
|
-
const processInstanceId = randomUUID();
|
|
121
|
-
const child = spawn(payload.command, [...payload.args], {
|
|
122
|
-
cwd: payload.cwd,
|
|
123
|
-
env: { ...payload.environment },
|
|
124
|
-
stdio: payload.providerInput === undefined ? "inherit" : ["pipe", "inherit", "inherit"],
|
|
125
|
-
detached: true
|
|
126
|
-
});
|
|
127
|
-
if (payload.providerInput !== undefined) {
|
|
128
|
-
if (child.stdin === null)
|
|
129
|
-
throw new Error("Provider input pipe is unavailable.");
|
|
130
|
-
const providerInput = `${JSON.stringify({
|
|
131
|
-
type: "user",
|
|
132
|
-
message: {
|
|
133
|
-
role: "user",
|
|
134
|
-
content: [{ type: "text", text: payload.providerInput.boundedText }]
|
|
135
|
-
}
|
|
136
|
-
})}\n`;
|
|
137
|
-
child.stdin.end(providerInput, "utf8");
|
|
138
|
-
}
|
|
139
|
-
const forward = (signal) => {
|
|
140
|
-
if (child.pid === undefined)
|
|
141
|
-
return;
|
|
142
|
-
try {
|
|
143
|
-
process.kill(-child.pid, signal);
|
|
144
|
-
}
|
|
145
|
-
catch (error) {
|
|
146
|
-
if (error.code !== "ESRCH")
|
|
147
|
-
throw error;
|
|
148
|
-
}
|
|
149
|
-
};
|
|
150
|
-
const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
151
|
-
const handlers = new Map();
|
|
152
|
-
let hostStopRequested = false;
|
|
153
|
-
let forceKillTimer;
|
|
154
|
-
for (const signal of signals) {
|
|
155
|
-
const handler = () => {
|
|
156
|
-
hostStopRequested = true;
|
|
157
|
-
forward(signal);
|
|
158
|
-
forceKillTimer ??= setTimeout(() => forward("SIGKILL"), 10_000);
|
|
159
|
-
forceKillTimer.unref();
|
|
160
|
-
};
|
|
161
|
-
handlers.set(signal, handler);
|
|
162
|
-
process.on(signal, handler);
|
|
163
|
-
}
|
|
164
|
-
try {
|
|
165
|
-
return await new Promise((resolve, reject) => {
|
|
166
|
-
child.once("error", reject);
|
|
167
|
-
child.once("close", (code, signal) => {
|
|
168
|
-
resolve({ code, signal, processInstanceId, hostStopRequested });
|
|
169
|
-
});
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
finally {
|
|
173
|
-
for (const [signal, handler] of handlers)
|
|
174
|
-
process.removeListener(signal, handler);
|
|
175
|
-
if (forceKillTimer !== undefined)
|
|
176
|
-
clearTimeout(forceKillTimer);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
673
|
async function persistAndSubmitExit(home, observation) {
|
|
180
674
|
const directory = resolve(join(home, "runtime", "agent-host-outbox"));
|
|
181
675
|
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
@@ -222,9 +716,8 @@ async function replayExitOutbox(home) {
|
|
|
222
716
|
await unlink(claimed);
|
|
223
717
|
}
|
|
224
718
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
719
|
+
/** Internal socket boundary exported for transport-level verification. */
|
|
720
|
+
export async function openAgentHostControl(home, payload, snapshot, dispatch) {
|
|
228
721
|
const path = agentHostControlSocketPath({
|
|
229
722
|
home,
|
|
230
723
|
scope: payload.environment.YUI_SESSION_SCOPE ?? "task",
|
|
@@ -233,16 +726,17 @@ async function openHostControl(home, payload) {
|
|
|
233
726
|
: { taskId: payload.environment.YUI_TASK_ID }),
|
|
234
727
|
roleName: payload.environment.YUI_ROLE ?? "unknown-role"
|
|
235
728
|
});
|
|
729
|
+
const directory = dirname(path);
|
|
730
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
236
731
|
if (await hostControlSocketIsLive(path)) {
|
|
237
732
|
throw new Error(`Agent Host control socket is already owned: ${path}.`);
|
|
238
733
|
}
|
|
239
734
|
rmSync(path, { force: true });
|
|
240
|
-
|
|
241
|
-
let activeLaunchId;
|
|
242
|
-
const queued = [];
|
|
243
|
-
const waiters = [];
|
|
244
|
-
const server = createServer((socket) => {
|
|
735
|
+
const server = createServer({ allowHalfOpen: true }, (socket) => {
|
|
245
736
|
socket.setEncoding("utf8");
|
|
737
|
+
// A control client can disappear after sending its bounded request. Keep
|
|
738
|
+
// that connection-local failure from terminating the persistent Host.
|
|
739
|
+
socket.on("error", () => { });
|
|
246
740
|
let body = "";
|
|
247
741
|
socket.on("data", (chunk) => {
|
|
248
742
|
body += chunk;
|
|
@@ -251,24 +745,20 @@ async function openHostControl(home, payload) {
|
|
|
251
745
|
}
|
|
252
746
|
});
|
|
253
747
|
socket.once("end", () => {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
socket.end(
|
|
258
|
-
? "active-same-launch\n"
|
|
259
|
-
: "active-other-launch\n");
|
|
260
|
-
return;
|
|
748
|
+
void (async () => {
|
|
749
|
+
try {
|
|
750
|
+
const request = validateControl(JSON.parse(body.trim()));
|
|
751
|
+
socket.end(`${JSON.stringify(await dispatch(request))}\n`);
|
|
261
752
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
753
|
+
catch (error) {
|
|
754
|
+
const current = snapshot();
|
|
755
|
+
socket.end(`${JSON.stringify(controlResult("rejected", validateSnapshot({
|
|
756
|
+
...current,
|
|
757
|
+
detail: errorText(error),
|
|
758
|
+
updatedAt: new Date().toISOString()
|
|
759
|
+
})))}\n`);
|
|
760
|
+
}
|
|
761
|
+
})();
|
|
272
762
|
});
|
|
273
763
|
});
|
|
274
764
|
await new Promise((resolvePromise, reject) => {
|
|
@@ -277,16 +767,6 @@ async function openHostControl(home, payload) {
|
|
|
277
767
|
});
|
|
278
768
|
chmodSync(path, 0o600);
|
|
279
769
|
return Object.freeze({
|
|
280
|
-
setActive(value, launchId) {
|
|
281
|
-
active = value;
|
|
282
|
-
activeLaunchId = value ? launchId : undefined;
|
|
283
|
-
},
|
|
284
|
-
next() {
|
|
285
|
-
const available = queued.shift();
|
|
286
|
-
if (available !== undefined)
|
|
287
|
-
return Promise.resolve(available);
|
|
288
|
-
return new Promise((resolvePromise) => waiters.push(resolvePromise));
|
|
289
|
-
},
|
|
290
770
|
close: async () => {
|
|
291
771
|
await new Promise((resolvePromise) => server.close(() => resolvePromise()));
|
|
292
772
|
rmSync(path, { force: true });
|
|
@@ -313,15 +793,144 @@ async function hostControlSocketIsLive(path) {
|
|
|
313
793
|
});
|
|
314
794
|
}
|
|
315
795
|
function validateControl(control) {
|
|
316
|
-
if (control.protocol !== AGENT_HOST_CONTROL_PROTOCOL
|
|
796
|
+
if (control.protocol !== AGENT_HOST_CONTROL_PROTOCOL) {
|
|
317
797
|
throw new Error("Agent Host control protocol is invalid.");
|
|
318
798
|
}
|
|
319
|
-
if (
|
|
320
|
-
|
|
321
|
-
|
|
799
|
+
if (control.type === "status")
|
|
800
|
+
return Object.freeze({ ...control });
|
|
801
|
+
if (control.type === "submit-turn") {
|
|
802
|
+
validateLaunchId(control.launchId);
|
|
803
|
+
validateIdentity(control.nativeSessionId, "native Session id");
|
|
804
|
+
validateProviderAuthorityFence(control.authority);
|
|
805
|
+
validateIdentity(control.turn.attemptId, "Provider input attempt id");
|
|
806
|
+
if (typeof control.turn.boundedText !== "string"
|
|
807
|
+
|| control.turn.boundedText.includes("\0")
|
|
808
|
+
|| Buffer.byteLength(control.turn.boundedText, "utf8") > 32 * 1024) {
|
|
809
|
+
throw new Error("Agent Host Provider input is invalid.");
|
|
810
|
+
}
|
|
811
|
+
return Object.freeze({
|
|
812
|
+
...control,
|
|
813
|
+
turn: Object.freeze({ ...control.turn })
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
if (control.type === "set-authority") {
|
|
817
|
+
validateIdentity(control.nativeSessionId, "native Session id");
|
|
818
|
+
return Object.freeze({
|
|
819
|
+
...control,
|
|
820
|
+
authority: validateProviderAuthorityFence(control.authority)
|
|
821
|
+
});
|
|
322
822
|
}
|
|
823
|
+
if (control.type !== "launch")
|
|
824
|
+
throw new Error("Agent Host control type is invalid.");
|
|
825
|
+
validateLaunchId(control.launchId);
|
|
323
826
|
if (typeof control.ticket !== "string" || !/^[a-f0-9]{64}$/u.test(control.ticket)) {
|
|
324
827
|
throw new Error("Agent Host launch control ticket is invalid.");
|
|
325
828
|
}
|
|
326
829
|
return Object.freeze({ ...control });
|
|
327
830
|
}
|
|
831
|
+
function validateLaunchId(value) {
|
|
832
|
+
if (typeof value !== "string" || value.length === 0
|
|
833
|
+
|| value.length > 256 || value.includes("\0")) {
|
|
834
|
+
throw new Error("Agent Host launch control identity is invalid.");
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
function validateIdentity(value, label) {
|
|
838
|
+
if (typeof value !== "string" || value.trim().length === 0 || value.includes("\0")) {
|
|
839
|
+
throw new Error(`Agent Host ${label} is invalid.`);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
function validateControlResult(result) {
|
|
843
|
+
if (result.protocol !== AGENT_HOST_CONTROL_PROTOCOL
|
|
844
|
+
|| !["status", "accepted", "rejected", "active-same-launch", "active-other-launch"].includes(result.outcome)) {
|
|
845
|
+
throw new Error("Agent Host control response is invalid.");
|
|
846
|
+
}
|
|
847
|
+
return Object.freeze({ ...result, snapshot: validateSnapshot(result.snapshot) });
|
|
848
|
+
}
|
|
849
|
+
function validateSnapshot(snapshot) {
|
|
850
|
+
if (snapshot.schemaVersion !== 1
|
|
851
|
+
|| !["idle", "starting", "ready", "settling", "delivery-unknown", "rejected", "failed", "exited"]
|
|
852
|
+
.includes(snapshot.state)) {
|
|
853
|
+
throw new Error("Agent Host snapshot is invalid.");
|
|
854
|
+
}
|
|
855
|
+
if (!Number.isFinite(Date.parse(snapshot.updatedAt))) {
|
|
856
|
+
throw new Error("Agent Host snapshot timestamp is invalid.");
|
|
857
|
+
}
|
|
858
|
+
const authorityFields = [
|
|
859
|
+
snapshot.authorityEpoch,
|
|
860
|
+
snapshot.authorityOwner,
|
|
861
|
+
snapshot.authorityHolderId
|
|
862
|
+
];
|
|
863
|
+
if (authorityFields.some((value) => value !== undefined)) {
|
|
864
|
+
if (authorityFields.some((value) => value === undefined)) {
|
|
865
|
+
throw new Error("Agent Host snapshot authority is incomplete.");
|
|
866
|
+
}
|
|
867
|
+
validateProviderAuthorityFence({
|
|
868
|
+
epoch: snapshot.authorityEpoch,
|
|
869
|
+
owner: snapshot.authorityOwner,
|
|
870
|
+
holderId: snapshot.authorityHolderId
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
return Object.freeze({ ...snapshot });
|
|
874
|
+
}
|
|
875
|
+
function hostSnapshot(state, fields = {}) {
|
|
876
|
+
return validateSnapshot({
|
|
877
|
+
schemaVersion: 1,
|
|
878
|
+
state,
|
|
879
|
+
...definedFields(fields),
|
|
880
|
+
updatedAt: new Date().toISOString()
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
function controlResult(outcome, snapshot) {
|
|
884
|
+
return Object.freeze({ protocol: AGENT_HOST_CONTROL_PROTOCOL, outcome, snapshot });
|
|
885
|
+
}
|
|
886
|
+
function definedFields(value) {
|
|
887
|
+
return Object.fromEntries(Object.entries(value).filter(([, member]) => member !== undefined));
|
|
888
|
+
}
|
|
889
|
+
function errorText(error) {
|
|
890
|
+
return error instanceof Error ? error.message : String(error ?? "unknown error");
|
|
891
|
+
}
|
|
892
|
+
function hostTurnControlParams(payload, nativeSessionId, authority, attemptId) {
|
|
893
|
+
const environment = payload.environment;
|
|
894
|
+
return Object.freeze({
|
|
895
|
+
taskId: requiredEnvironment(environment.YUI_TASK_ID, "Task id"),
|
|
896
|
+
roleName: requiredEnvironment(environment.YUI_ROLE, "Role name"),
|
|
897
|
+
runId: requiredEnvironment(environment.YUI_RUN_ID, "Run id"),
|
|
898
|
+
agentId: requiredEnvironment(environment.YUI_AGENT_ID, "Agent id"),
|
|
899
|
+
launchId: payload.launchId,
|
|
900
|
+
nativeSessionId: requiredEnvironment(nativeSessionId, "native Session id"),
|
|
901
|
+
attemptId,
|
|
902
|
+
authorityEpoch: authority.epoch,
|
|
903
|
+
authorityOwner: authority.owner,
|
|
904
|
+
holderId: authority.holderId,
|
|
905
|
+
observedAt: new Date().toISOString()
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
async function beginDurableProviderTurn(home, durableTurn) {
|
|
909
|
+
try {
|
|
910
|
+
await callController(home, "runtime.provider-turn-begin", durableTurn);
|
|
911
|
+
}
|
|
912
|
+
catch (error) {
|
|
913
|
+
if (!(error instanceof ControllerClientError) || error.code !== "INTERNAL_ERROR") {
|
|
914
|
+
throw error;
|
|
915
|
+
}
|
|
916
|
+
// The exact attempt is idempotent, so one lost acknowledgement can be
|
|
917
|
+
// retried without creating a second Provider write.
|
|
918
|
+
await callController(home, "runtime.provider-turn-begin", durableTurn);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
async function resolveProviderTurnSubmission(home, durableTurn, error) {
|
|
922
|
+
await callController(home, "runtime.provider-turn-submission-resolve", {
|
|
923
|
+
...durableTurn,
|
|
924
|
+
status: error instanceof ProviderDeliveryUnknownError
|
|
925
|
+
? "delivery-unknown"
|
|
926
|
+
: "rejected",
|
|
927
|
+
reason: errorText(error),
|
|
928
|
+
observedAt: new Date().toISOString()
|
|
929
|
+
}).catch(() => { });
|
|
930
|
+
}
|
|
931
|
+
function requiredEnvironment(value, label) {
|
|
932
|
+
if (typeof value !== "string" || value.trim().length === 0 || value.includes("\0")) {
|
|
933
|
+
throw new Error(`Agent Host ${label} is unavailable.`);
|
|
934
|
+
}
|
|
935
|
+
return value.trim();
|
|
936
|
+
}
|