@agentshouse/kit 0.0.1 → 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +1696 -2
- package/bin/connect-linux.sh +127 -0
- package/bin/kit.js +1023 -0
- package/package.json +33 -3
- package/src/acp.js +99 -0
- package/src/adapter-mcp.js +44 -0
- package/src/adapter.js +252 -0
- package/src/agent-logins.js +65 -0
- package/src/agents.js +449 -0
- package/src/apply.js +29 -0
- package/src/attach.js +52 -0
- package/src/binding.js +90 -0
- package/src/blocks.js +156 -0
- package/src/bridge.js +449 -0
- package/src/bytes.js +89 -0
- package/src/channel.js +37 -0
- package/src/command.js +109 -0
- package/src/configuration.js +465 -0
- package/src/connector-login.js +91 -0
- package/src/connector.js +107 -0
- package/src/continuation.js +61 -0
- package/src/contract.js +363 -0
- package/src/conversation-originals.js +143 -0
- package/src/custom/authorization.js +63 -0
- package/src/declaration.js +35 -0
- package/src/delivery.js +191 -0
- package/src/directory.js +102 -0
- package/src/draft.js +89 -0
- package/src/google/calendar.js +177 -0
- package/src/google/declared.js +146 -0
- package/src/google/drive.js +435 -0
- package/src/google/gateway.js +166 -0
- package/src/google/gmail.js +249 -0
- package/src/google/id.js +1 -0
- package/src/google/oauth.js +273 -0
- package/src/google/source.js +25 -0
- package/src/hook.js +376 -0
- package/src/host.js +121 -0
- package/src/house-main.js +79 -0
- package/src/house.js +1085 -0
- package/src/inbound.js +175 -0
- package/src/instance/google.js +1 -0
- package/src/launcher-main.js +19 -0
- package/src/launcher.js +276 -0
- package/src/local-files.js +113 -0
- package/src/login.js +274 -0
- package/src/mcp.js +71 -0
- package/src/migrator.js +248 -0
- package/src/mount.js +582 -0
- package/src/operation.js +20 -0
- package/src/original.js +631 -0
- package/src/peer.js +124 -0
- package/src/plan.js +57 -0
- package/src/question.js +316 -0
- package/src/refusal.js +79 -0
- package/src/repository.js +521 -0
- package/src/session-bin/house +2 -0
- package/src/settlement.js +57 -0
- package/src/setup.js +185 -0
- package/src/source-originals.js +116 -0
- package/src/source.js +286 -0
- package/src/starter-set/grilling/LICENSE +21 -0
- package/src/starter-set/grilling/SKILL.md +30 -0
- package/src/starter-set/grilling/agents/openai.yaml +3 -0
- package/src/starter-set.js +17 -0
- package/src/state.js +157 -0
- package/src/sync-library.js +293 -0
- package/src/sync-state.js +116 -0
- package/src/sync.js +477 -0
- package/src/telegram.js +1045 -0
- package/src/terminal.js +57 -0
- package/src/tmux.js +119 -0
- package/src/worker.js +756 -0
- package/src/workspace.js +500 -0
package/src/house.js
ADDED
|
@@ -0,0 +1,1085 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { assertAcquiredCommandAdapter } from "./adapter.js";
|
|
4
|
+
import {
|
|
5
|
+
assistantDraftsPath,
|
|
6
|
+
assistantTextPath,
|
|
7
|
+
ADAPTER_KINDS,
|
|
8
|
+
INTERACTION_CORRELATION_MAX,
|
|
9
|
+
INTERACTION_OUTCOMES,
|
|
10
|
+
INTERACTION_PROPERTY_MAX,
|
|
11
|
+
INTERACTION_VALUES_MAX,
|
|
12
|
+
INTERACTION_VALUE_MAX,
|
|
13
|
+
interactionOutcomePath,
|
|
14
|
+
KIT_CONTRACT_VERSION,
|
|
15
|
+
KIT_CREDENTIAL,
|
|
16
|
+
KIT_AGENTS_REPORT_PATH,
|
|
17
|
+
KIT_AGENTS_DESIRED_PATH,
|
|
18
|
+
KIT_HOST_CAPACITY_PATH,
|
|
19
|
+
KIT_LOGIN_REPORT_PATH,
|
|
20
|
+
KIT_SESSIONS_REPORT_MAX,
|
|
21
|
+
KIT_SESSIONS_REPORT_PATH,
|
|
22
|
+
KIT_SETUP_ACQUIRE_PATH,
|
|
23
|
+
KIT_SETUP_REPORT_PATH,
|
|
24
|
+
KIT_WORKSPACES_ACQUIRE_PATH,
|
|
25
|
+
LEASE_RENEWAL_MARGIN_MS,
|
|
26
|
+
MCP_PROTOCOL_VERSION,
|
|
27
|
+
WORKER_RUN_TOKEN_MARGIN_MS,
|
|
28
|
+
runOutcomePath,
|
|
29
|
+
secretInputPath,
|
|
30
|
+
SECRET_VALUE_MAX,
|
|
31
|
+
sessionInteractionsPath,
|
|
32
|
+
workspaceOutcomePath,
|
|
33
|
+
} from "./contract.js";
|
|
34
|
+
import { declarationReader } from "./declaration.js";
|
|
35
|
+
import { houseRefusal, KitRefusal, throwHouseAnswer } from "./refusal.js";
|
|
36
|
+
|
|
37
|
+
const LEASE = /^ahl_[A-Za-z0-9_-]+$/;
|
|
38
|
+
const ASSISTANT_DRAFT_STATES = ["streamed", "unsupported"];
|
|
39
|
+
const ITEM_OUTCOMES = ["accepted", "duplicate", "rejected"];
|
|
40
|
+
|
|
41
|
+
async function readCredential(directory) {
|
|
42
|
+
let stored;
|
|
43
|
+
try {
|
|
44
|
+
stored = (await readFile(join(directory, "credential"), "utf8")).trim();
|
|
45
|
+
} catch {
|
|
46
|
+
throw new KitRefusal("kit_credential_rejected", directory);
|
|
47
|
+
}
|
|
48
|
+
if (!KIT_CREDENTIAL.test(stored)) throw new KitRefusal("kit_credential_rejected", directory);
|
|
49
|
+
return stored;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function houseAnswer(house, path, init, dependencies, signal) {
|
|
53
|
+
const fetcher = dependencies.fetch ?? fetch;
|
|
54
|
+
let response;
|
|
55
|
+
try {
|
|
56
|
+
response = await fetcher(`${house}${path}`, { ...init, signal });
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (signal?.aborted === true || error?.name === "AbortError") {
|
|
59
|
+
throw new KitRefusal("kit_command_cancelled");
|
|
60
|
+
}
|
|
61
|
+
throw new KitRefusal("kit_house_unavailable", path);
|
|
62
|
+
}
|
|
63
|
+
let answer = null;
|
|
64
|
+
try {
|
|
65
|
+
answer = await response.json();
|
|
66
|
+
} catch {
|
|
67
|
+
answer = null;
|
|
68
|
+
}
|
|
69
|
+
throwHouseAnswer(response.status, answer, path);
|
|
70
|
+
if (answer === null) throw new KitRefusal("kit_house_unavailable", path);
|
|
71
|
+
return answer;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function houseGetJson(url, dependencies, signal) {
|
|
75
|
+
return houseAnswer(url, "", { method: "GET" }, dependencies, signal);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function houseRequest(house, authorization, path, body, dependencies, signal) {
|
|
79
|
+
return houseAnswer(
|
|
80
|
+
house,
|
|
81
|
+
path,
|
|
82
|
+
{
|
|
83
|
+
method: "POST",
|
|
84
|
+
headers: {
|
|
85
|
+
...(authorization === null ? {} : { authorization }),
|
|
86
|
+
"content-type": "application/json",
|
|
87
|
+
"x-house-kit-version": KIT_CONTRACT_VERSION,
|
|
88
|
+
},
|
|
89
|
+
body: JSON.stringify(body),
|
|
90
|
+
},
|
|
91
|
+
dependencies,
|
|
92
|
+
signal,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function heldLease(answer) {
|
|
97
|
+
if (
|
|
98
|
+
typeof answer?.lease !== "string" ||
|
|
99
|
+
!LEASE.test(answer.lease) ||
|
|
100
|
+
typeof answer.expires_at !== "string" ||
|
|
101
|
+
Number.isNaN(Date.parse(answer.expires_at))
|
|
102
|
+
) {
|
|
103
|
+
throw new KitRefusal("kit_lease_rejected");
|
|
104
|
+
}
|
|
105
|
+
return { lease: answer.lease, expiresAt: Date.parse(answer.expires_at) };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function openSession(options, dependencies = {}) {
|
|
109
|
+
const session = {
|
|
110
|
+
house: options.house,
|
|
111
|
+
dependencies,
|
|
112
|
+
now: dependencies.now ?? (() => new Date()),
|
|
113
|
+
lease: null,
|
|
114
|
+
expiresAt: 0,
|
|
115
|
+
signal: options.signal,
|
|
116
|
+
};
|
|
117
|
+
const credential = await readCredential(options.directory);
|
|
118
|
+
Object.assign(
|
|
119
|
+
session,
|
|
120
|
+
heldLease(
|
|
121
|
+
await houseRequest(
|
|
122
|
+
session.house,
|
|
123
|
+
`Bearer ${credential}`,
|
|
124
|
+
"/kit/lease",
|
|
125
|
+
{},
|
|
126
|
+
session.dependencies,
|
|
127
|
+
session.signal,
|
|
128
|
+
),
|
|
129
|
+
),
|
|
130
|
+
);
|
|
131
|
+
return session;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function heldControl(session, signal = session.signal) {
|
|
135
|
+
if (session.expiresAt - session.now().getTime() > LEASE_RENEWAL_MARGIN_MS) {
|
|
136
|
+
return `Bearer ${session.lease}`;
|
|
137
|
+
}
|
|
138
|
+
Object.assign(
|
|
139
|
+
session,
|
|
140
|
+
heldLease(
|
|
141
|
+
await houseRequest(
|
|
142
|
+
session.house,
|
|
143
|
+
`Bearer ${session.lease}`,
|
|
144
|
+
"/kit/lease/renew",
|
|
145
|
+
{},
|
|
146
|
+
session.dependencies,
|
|
147
|
+
signal,
|
|
148
|
+
),
|
|
149
|
+
),
|
|
150
|
+
);
|
|
151
|
+
return `Bearer ${session.lease}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function controlSleep(milliseconds, signal) {
|
|
155
|
+
return new Promise((resolve, reject) => {
|
|
156
|
+
if (signal.aborted) {
|
|
157
|
+
reject(new KitRefusal("kit_command_cancelled"));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const timer = setTimeout(finish, milliseconds);
|
|
161
|
+
function finish() {
|
|
162
|
+
signal.removeEventListener("abort", cancel);
|
|
163
|
+
resolve();
|
|
164
|
+
}
|
|
165
|
+
function cancel() {
|
|
166
|
+
clearTimeout(timer);
|
|
167
|
+
signal.removeEventListener("abort", cancel);
|
|
168
|
+
reject(new KitRefusal("kit_command_cancelled"));
|
|
169
|
+
}
|
|
170
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function toolMutations(session, signal = session.signal) {
|
|
175
|
+
const answer = await houseRequest(
|
|
176
|
+
session.house,
|
|
177
|
+
await heldControl(session, signal),
|
|
178
|
+
"/kit/tools/mutations",
|
|
179
|
+
{},
|
|
180
|
+
session.dependencies,
|
|
181
|
+
signal,
|
|
182
|
+
);
|
|
183
|
+
if (!Array.isArray(answer?.tools)) throw new KitRefusal("kit_tool_classification_invalid");
|
|
184
|
+
const writes = new Map();
|
|
185
|
+
for (const tool of answer.tools) {
|
|
186
|
+
if (typeof tool?.name !== "string") throw new KitRefusal("kit_tool_classification_invalid");
|
|
187
|
+
if (typeof tool.writes !== "boolean") throw new KitRefusal("kit_tool_classification_invalid");
|
|
188
|
+
writes.set(tool.name, tool.writes);
|
|
189
|
+
}
|
|
190
|
+
return writes;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function mutatingCall(classification) {
|
|
194
|
+
return (name) => classification.get(name) === true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export async function releaseControl(session, options = {}) {
|
|
198
|
+
if (session.lease === null) return;
|
|
199
|
+
const held = session.lease;
|
|
200
|
+
try {
|
|
201
|
+
await houseRequest(
|
|
202
|
+
session.house,
|
|
203
|
+
`Bearer ${held}`,
|
|
204
|
+
"/kit/lease/release",
|
|
205
|
+
{},
|
|
206
|
+
session.dependencies,
|
|
207
|
+
);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
if (options.required === true) throw error;
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
session.lease = null;
|
|
213
|
+
session.expiresAt = 0;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export async function maintainControl(session, operation, signal, { release = true } = {}) {
|
|
217
|
+
if (signal?.aborted === true) throw new KitRefusal("kit_command_cancelled");
|
|
218
|
+
const maintenance = new AbortController();
|
|
219
|
+
const operationSignal =
|
|
220
|
+
signal === undefined ? maintenance.signal : AbortSignal.any([signal, maintenance.signal]);
|
|
221
|
+
let stopped = false;
|
|
222
|
+
let rejectFailure;
|
|
223
|
+
const failure = new Promise((_, reject) => {
|
|
224
|
+
rejectFailure = reject;
|
|
225
|
+
});
|
|
226
|
+
let cancelOperation;
|
|
227
|
+
const cancellation = new Promise((_, reject) => {
|
|
228
|
+
cancelOperation = () => reject(new KitRefusal("kit_command_cancelled"));
|
|
229
|
+
signal?.addEventListener("abort", cancelOperation, { once: true });
|
|
230
|
+
});
|
|
231
|
+
const renewal = (async () => {
|
|
232
|
+
const sleep = session.dependencies.controlSleep ?? controlSleep;
|
|
233
|
+
while (!stopped) {
|
|
234
|
+
const delay = Math.max(
|
|
235
|
+
0,
|
|
236
|
+
session.expiresAt - session.now().getTime() - LEASE_RENEWAL_MARGIN_MS,
|
|
237
|
+
);
|
|
238
|
+
try {
|
|
239
|
+
await sleep(delay, maintenance.signal);
|
|
240
|
+
if (stopped) return;
|
|
241
|
+
const previousExpiry = session.expiresAt;
|
|
242
|
+
const renewalDue = previousExpiry - session.now().getTime() <= LEASE_RENEWAL_MARGIN_MS;
|
|
243
|
+
await heldControl(session, operationSignal);
|
|
244
|
+
if (renewalDue && session.expiresAt <= previousExpiry) {
|
|
245
|
+
throw new KitRefusal("kit_lease_rejected");
|
|
246
|
+
}
|
|
247
|
+
} catch (error) {
|
|
248
|
+
if (stopped) return;
|
|
249
|
+
rejectFailure(error);
|
|
250
|
+
maintenance.abort();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
})();
|
|
255
|
+
const running = Promise.resolve().then(() => operation(operationSignal));
|
|
256
|
+
let ended = false;
|
|
257
|
+
try {
|
|
258
|
+
const answer = await Promise.race([running, failure, cancellation]);
|
|
259
|
+
ended = true;
|
|
260
|
+
return answer;
|
|
261
|
+
} catch (error) {
|
|
262
|
+
ended = error instanceof KitRefusal && error.code === "kit_command_cancelled";
|
|
263
|
+
throw error;
|
|
264
|
+
} finally {
|
|
265
|
+
stopped = true;
|
|
266
|
+
maintenance.abort();
|
|
267
|
+
signal?.removeEventListener("abort", cancelOperation);
|
|
268
|
+
await Promise.all([renewal, running.catch(() => undefined)]);
|
|
269
|
+
if (ended && release) await releaseControl(session);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function capturedOutcome(answer, count, rooms) {
|
|
274
|
+
const refused = () => {
|
|
275
|
+
throw new KitRefusal("kit_capture_rejected");
|
|
276
|
+
};
|
|
277
|
+
if (!Array.isArray(answer?.items) || answer.items.length > count) refused();
|
|
278
|
+
if (!Array.isArray(answer.blocked_routes)) refused();
|
|
279
|
+
if (answer.items.length < count && answer.items.at(-1)?.outcome !== "rejected") refused();
|
|
280
|
+
for (const result of answer.items) {
|
|
281
|
+
if (!ITEM_OUTCOMES.includes(result?.outcome)) refused();
|
|
282
|
+
if (result.outcome !== "rejected") {
|
|
283
|
+
if (
|
|
284
|
+
!Array.isArray(result.rooms) ||
|
|
285
|
+
result.rooms.length !== rooms.length ||
|
|
286
|
+
result.rooms.some(
|
|
287
|
+
(room, position) =>
|
|
288
|
+
room?.room !== rooms[position] ||
|
|
289
|
+
typeof room.record !== "string" ||
|
|
290
|
+
room.record.length === 0,
|
|
291
|
+
)
|
|
292
|
+
) {
|
|
293
|
+
refused();
|
|
294
|
+
}
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (typeof result.code !== "string") refused();
|
|
298
|
+
if (result.disposition !== "retry" && result.disposition !== "blocked") refused();
|
|
299
|
+
if (result.scope !== "item" && result.scope !== "route") refused();
|
|
300
|
+
}
|
|
301
|
+
return { items: answer.items, blocked_routes: answer.blocked_routes };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export async function applyDeclared(session, document, signal = session.signal) {
|
|
305
|
+
return houseRequest(
|
|
306
|
+
session.house,
|
|
307
|
+
await heldControl(session, signal),
|
|
308
|
+
"/kit/apply",
|
|
309
|
+
document,
|
|
310
|
+
session.dependencies,
|
|
311
|
+
signal,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export async function captureBatch(session, batch) {
|
|
316
|
+
return capturedOutcome(
|
|
317
|
+
await houseRequest(
|
|
318
|
+
session.house,
|
|
319
|
+
await heldControl(session),
|
|
320
|
+
"/kit/capture",
|
|
321
|
+
{ source_ref: batch.sourceRef, rooms: batch.rooms, items: batch.items },
|
|
322
|
+
session.dependencies,
|
|
323
|
+
session.signal,
|
|
324
|
+
),
|
|
325
|
+
batch.items.length,
|
|
326
|
+
batch.rooms,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export async function openPublicationManifest(session, manifest, signal) {
|
|
331
|
+
return houseRequest(
|
|
332
|
+
session.house,
|
|
333
|
+
await heldControl(session, signal),
|
|
334
|
+
"/kit/repository/manifest",
|
|
335
|
+
manifest,
|
|
336
|
+
session.dependencies,
|
|
337
|
+
signal,
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export async function deliverPublicationChunk(session, declaration, body, signal) {
|
|
342
|
+
return houseAnswer(
|
|
343
|
+
session.house,
|
|
344
|
+
"/kit/repository/chunk",
|
|
345
|
+
{
|
|
346
|
+
method: "POST",
|
|
347
|
+
headers: {
|
|
348
|
+
authorization: await heldControl(session, signal),
|
|
349
|
+
"content-type": "application/x-tar",
|
|
350
|
+
"x-house-kit-version": KIT_CONTRACT_VERSION,
|
|
351
|
+
"x-agents-house-room": declaration.room,
|
|
352
|
+
"x-agents-house-publication": declaration.publication,
|
|
353
|
+
},
|
|
354
|
+
body,
|
|
355
|
+
duplex: "half",
|
|
356
|
+
},
|
|
357
|
+
session.dependencies,
|
|
358
|
+
signal,
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export async function acquireWorkspaces(session, max, signal = session.signal) {
|
|
363
|
+
const answer = await houseRequest(
|
|
364
|
+
session.house,
|
|
365
|
+
await heldControl(session, signal),
|
|
366
|
+
KIT_WORKSPACES_ACQUIRE_PATH,
|
|
367
|
+
{ max },
|
|
368
|
+
session.dependencies,
|
|
369
|
+
signal,
|
|
370
|
+
);
|
|
371
|
+
if (!Array.isArray(answer?.workspaces) || !Array.isArray(answer?.operations)) {
|
|
372
|
+
throw new KitRefusal("kit_workspace_work_rejected");
|
|
373
|
+
}
|
|
374
|
+
return answer;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export async function reportWorkspaceOutcome(session, operation, report, signal = session.signal) {
|
|
378
|
+
return houseRequest(
|
|
379
|
+
session.house,
|
|
380
|
+
await heldControl(session, signal),
|
|
381
|
+
workspaceOutcomePath(operation),
|
|
382
|
+
report,
|
|
383
|
+
session.dependencies,
|
|
384
|
+
signal,
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export async function reportHostCapacity(session, observation, signal = session.signal) {
|
|
389
|
+
return houseRequest(
|
|
390
|
+
session.house,
|
|
391
|
+
await heldControl(session, signal),
|
|
392
|
+
KIT_HOST_CAPACITY_PATH,
|
|
393
|
+
observation,
|
|
394
|
+
session.dependencies,
|
|
395
|
+
signal,
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export async function reportAgentCapabilities(session, agents, signal = session.signal) {
|
|
400
|
+
return houseRequest(
|
|
401
|
+
session.house,
|
|
402
|
+
await heldControl(session, signal),
|
|
403
|
+
KIT_AGENTS_REPORT_PATH,
|
|
404
|
+
{ agents },
|
|
405
|
+
session.dependencies,
|
|
406
|
+
signal,
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export async function desiredAgents(session, signal = session.signal) {
|
|
411
|
+
const answer = await houseRequest(
|
|
412
|
+
session.house,
|
|
413
|
+
await heldControl(session, signal),
|
|
414
|
+
KIT_AGENTS_DESIRED_PATH,
|
|
415
|
+
{},
|
|
416
|
+
session.dependencies,
|
|
417
|
+
signal,
|
|
418
|
+
);
|
|
419
|
+
if (
|
|
420
|
+
!Array.isArray(answer?.agents) ||
|
|
421
|
+
answer.agents.some((kind) => typeof kind !== "string" || !ADAPTER_KINDS.includes(kind)) ||
|
|
422
|
+
new Set(answer.agents).size !== answer.agents.length
|
|
423
|
+
) {
|
|
424
|
+
throw new KitRefusal("kit_agents_rejected");
|
|
425
|
+
}
|
|
426
|
+
return answer.agents;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const HOUSE_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
430
|
+
|
|
431
|
+
export async function acquireSetup(session, signal = session.signal) {
|
|
432
|
+
const answer = await houseRequest(
|
|
433
|
+
session.house,
|
|
434
|
+
await heldControl(session, signal),
|
|
435
|
+
KIT_SETUP_ACQUIRE_PATH,
|
|
436
|
+
{},
|
|
437
|
+
session.dependencies,
|
|
438
|
+
signal,
|
|
439
|
+
);
|
|
440
|
+
if (answer?.setup === null) return null;
|
|
441
|
+
if (typeof answer?.setup?.request !== "string" || !HOUSE_UUID.test(answer.setup.request)) {
|
|
442
|
+
throw new KitRefusal("kit_setup_rejected", "setup");
|
|
443
|
+
}
|
|
444
|
+
return { request: answer.setup.request };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export async function reportSetup(session, request, modules, signal = session.signal) {
|
|
448
|
+
const answer = await houseRequest(
|
|
449
|
+
session.house,
|
|
450
|
+
await heldControl(session, signal),
|
|
451
|
+
KIT_SETUP_REPORT_PATH,
|
|
452
|
+
{ request, modules },
|
|
453
|
+
session.dependencies,
|
|
454
|
+
signal,
|
|
455
|
+
);
|
|
456
|
+
if (answer?.request !== request) throw new KitRefusal("kit_setup_rejected", "request");
|
|
457
|
+
return answer;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const LOGIN_STATES = ["renewal_open", "clear"];
|
|
461
|
+
|
|
462
|
+
export async function reportLogin(session, login, outcome, signal = session.signal) {
|
|
463
|
+
const answer = await houseRequest(
|
|
464
|
+
session.house,
|
|
465
|
+
await heldControl(session, signal),
|
|
466
|
+
KIT_LOGIN_REPORT_PATH,
|
|
467
|
+
{ ...login, outcome },
|
|
468
|
+
session.dependencies,
|
|
469
|
+
signal,
|
|
470
|
+
);
|
|
471
|
+
if (
|
|
472
|
+
!LOGIN_STATES.includes(answer?.state) ||
|
|
473
|
+
(answer.state === "renewal_open" &&
|
|
474
|
+
(typeof answer.episode !== "string" || !HOUSE_UUID.test(answer.episode)))
|
|
475
|
+
) {
|
|
476
|
+
throw new KitRefusal("kit_login_rejected", login.integration);
|
|
477
|
+
}
|
|
478
|
+
return { state: answer.state, episode: answer.state === "renewal_open" ? answer.episode : null };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const envelope = declarationReader("kit_acquire_rejected");
|
|
482
|
+
|
|
483
|
+
function acquiredRoute(value, declaration) {
|
|
484
|
+
const route = envelope.plain(value, declaration);
|
|
485
|
+
if (!Array.isArray(route.arguments)) envelope.refuse(`${declaration}.arguments`);
|
|
486
|
+
return {
|
|
487
|
+
id: envelope.text(route.id, `${declaration}.id`, 128),
|
|
488
|
+
adapter: assertAcquiredCommandAdapter(route.adapter),
|
|
489
|
+
model: envelope.text(route.model, `${declaration}.model`, 128),
|
|
490
|
+
effort: route.effort === null ? null : envelope.text(route.effort, `${declaration}.effort`, 128),
|
|
491
|
+
arguments: route.arguments.map((argument, position) =>
|
|
492
|
+
envelope.text(argument, `${declaration}.arguments[${position}]`, 4096),
|
|
493
|
+
),
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function conversationText(value, declaration) {
|
|
498
|
+
if (value === null) return null;
|
|
499
|
+
if (typeof value !== "string" || value.length > 4096) envelope.refuse(declaration);
|
|
500
|
+
return value;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function conversationMessage(value, declaration) {
|
|
504
|
+
const message = envelope.plain(value, declaration);
|
|
505
|
+
const text = conversationText(message.text, `${declaration}.text`);
|
|
506
|
+
const caption = conversationText(message.caption, `${declaration}.caption`);
|
|
507
|
+
if (!Array.isArray(message.files)) envelope.refuse(`${declaration}.files`);
|
|
508
|
+
const files = message.files.map((value, index) => {
|
|
509
|
+
const name = `${declaration}.files[${index}]`;
|
|
510
|
+
const file = envelope.plain(value, name);
|
|
511
|
+
if (file.bytes !== null && (!Number.isSafeInteger(file.bytes) || file.bytes < 0)) {
|
|
512
|
+
envelope.refuse(`${name}.bytes`);
|
|
513
|
+
}
|
|
514
|
+
return {
|
|
515
|
+
path: envelope.text(file.path, `${name}.path`, 2048),
|
|
516
|
+
media_kind: envelope.text(file.media_kind, `${name}.media_kind`, 128),
|
|
517
|
+
media_type: file.media_type === null ? null : envelope.text(file.media_type, `${name}.media_type`, 255),
|
|
518
|
+
bytes: file.bytes,
|
|
519
|
+
};
|
|
520
|
+
});
|
|
521
|
+
if (!text && !caption && files.length === 0) envelope.refuse(declaration);
|
|
522
|
+
return {
|
|
523
|
+
message_id: envelope.text(message.message_id, `${declaration}.message_id`, 128),
|
|
524
|
+
text, caption, files,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function acquiredRequest(acquired, declaration) {
|
|
529
|
+
if (acquired.kind === "conversation") {
|
|
530
|
+
if (acquired.origin !== "user") envelope.refuse(`${declaration}.origin`);
|
|
531
|
+
const conversation = envelope.plain(acquired.conversation, `${declaration}.conversation`);
|
|
532
|
+
const turn = envelope.plain(conversation.turn, `${declaration}.conversation.turn`);
|
|
533
|
+
return {
|
|
534
|
+
origin: "user",
|
|
535
|
+
conversation: {
|
|
536
|
+
turn: {
|
|
537
|
+
id: envelope.text(turn.id, `${declaration}.conversation.turn.id`, 128),
|
|
538
|
+
sequence: envelope.positiveCount(turn.sequence, `${declaration}.conversation.turn.sequence`),
|
|
539
|
+
messages: envelope.filled(turn.messages, `${declaration}.conversation.turn.messages`)
|
|
540
|
+
.map((message, index) => conversationMessage(message, `${declaration}.conversation.turn.messages[${index}]`)),
|
|
541
|
+
},
|
|
542
|
+
},
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
if (acquired.kind !== "routine") envelope.refuse(`${declaration}.kind`);
|
|
546
|
+
const request = envelope.plain(acquired.request, `${declaration}.request`);
|
|
547
|
+
const routine = envelope.plain(acquired.routine, `${declaration}.routine`);
|
|
548
|
+
return {
|
|
549
|
+
origin: envelope.text(acquired.origin, `${declaration}.origin`, 32),
|
|
550
|
+
routine: {
|
|
551
|
+
name: envelope.text(routine.name, `${declaration}.routine.name`, 256),
|
|
552
|
+
version: envelope.positiveCount(routine.version, `${declaration}.routine.version`),
|
|
553
|
+
},
|
|
554
|
+
payload: envelope.plain(request.payload, `${declaration}.request.payload`),
|
|
555
|
+
requested_by: envelope.text(
|
|
556
|
+
request.requested_by,
|
|
557
|
+
`${declaration}.request.requested_by`,
|
|
558
|
+
128,
|
|
559
|
+
),
|
|
560
|
+
requested_at: envelope.text(
|
|
561
|
+
request.requested_at,
|
|
562
|
+
`${declaration}.request.requested_at`,
|
|
563
|
+
64,
|
|
564
|
+
),
|
|
565
|
+
deliver_at:
|
|
566
|
+
request.deliver_at === null
|
|
567
|
+
? null
|
|
568
|
+
: envelope.text(request.deliver_at, `${declaration}.request.deliver_at`, 64),
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function acquiredAssignment(value, declaration) {
|
|
573
|
+
const acquired = envelope.plain(value, declaration);
|
|
574
|
+
const bridge = envelope.plain(acquired.session, `${declaration}.session`);
|
|
575
|
+
const plan = envelope.plain(acquired.plan, `${declaration}.plan`);
|
|
576
|
+
const route = acquiredRoute(plan.route, `${declaration}.plan.route`);
|
|
577
|
+
const expiresAt = envelope.text(
|
|
578
|
+
bridge.bridge_token_expires_at,
|
|
579
|
+
`${declaration}.session.bridge_token_expires_at`,
|
|
580
|
+
64,
|
|
581
|
+
);
|
|
582
|
+
if (Number.isNaN(Date.parse(expiresAt))) {
|
|
583
|
+
envelope.refuse(`${declaration}.session.bridge_token_expires_at`);
|
|
584
|
+
}
|
|
585
|
+
return {
|
|
586
|
+
run: envelope.text(acquired.run, `${declaration}.run`, 128),
|
|
587
|
+
agentId: envelope.text(plan.agent_id, `${declaration}.plan.agent_id`, 128),
|
|
588
|
+
workingDirectory: envelope.text(
|
|
589
|
+
plan.working_directory,
|
|
590
|
+
`${declaration}.plan.working_directory`,
|
|
591
|
+
2048,
|
|
592
|
+
),
|
|
593
|
+
route,
|
|
594
|
+
conversation: acquired.kind === "conversation",
|
|
595
|
+
session: {
|
|
596
|
+
id: envelope.text(bridge.id, `${declaration}.session.id`, 128),
|
|
597
|
+
token: envelope.text(bridge.bridge_token, `${declaration}.session.bridge_token`, 512),
|
|
598
|
+
expiresAt: Date.parse(expiresAt),
|
|
599
|
+
},
|
|
600
|
+
request: acquiredRequest(acquired, declaration),
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
export async function acquireOneRun(session) {
|
|
605
|
+
const answer = await houseRequest(
|
|
606
|
+
session.house,
|
|
607
|
+
await heldControl(session),
|
|
608
|
+
"/kit/runs/acquire",
|
|
609
|
+
{ max: 1 },
|
|
610
|
+
session.dependencies,
|
|
611
|
+
session.signal,
|
|
612
|
+
);
|
|
613
|
+
if (!Array.isArray(answer?.runs) || answer.runs.length > 1) envelope.refuse("runs");
|
|
614
|
+
if (answer.runs.length === 0) return null;
|
|
615
|
+
return acquiredAssignment(answer.runs[0], "runs[0]");
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
export async function commitRun(session, run) {
|
|
619
|
+
const answer = await houseRequest(
|
|
620
|
+
session.house,
|
|
621
|
+
await heldControl(session),
|
|
622
|
+
`/kit/runs/${encodeURIComponent(run)}/commit`,
|
|
623
|
+
{},
|
|
624
|
+
session.dependencies,
|
|
625
|
+
session.signal,
|
|
626
|
+
);
|
|
627
|
+
if (answer?.state !== "committed") throw new KitRefusal("kit_commit_rejected", run);
|
|
628
|
+
return answer;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
export async function reportRunOutcome(session, run, reported) {
|
|
632
|
+
const answer = await houseRequest(
|
|
633
|
+
session.house,
|
|
634
|
+
await heldControl(session),
|
|
635
|
+
runOutcomePath(run),
|
|
636
|
+
reported.cause === null
|
|
637
|
+
? { operation_id: reported.operation, outcome: reported.outcome }
|
|
638
|
+
: { operation_id: reported.operation, outcome: reported.outcome, cause: reported.cause },
|
|
639
|
+
session.dependencies,
|
|
640
|
+
session.signal,
|
|
641
|
+
);
|
|
642
|
+
if (answer?.run !== run || typeof answer.state !== "string" || answer.state.length === 0) {
|
|
643
|
+
throw new KitRefusal("kit_outcome_rejected", run);
|
|
644
|
+
}
|
|
645
|
+
return { state: answer.state, cause: answer.cause ?? null };
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function heldOutcome(run, reported) {
|
|
649
|
+
if (reported === null) return { session: run, outcome: null };
|
|
650
|
+
if (reported.cause === null) return { session: run, outcome: reported.outcome };
|
|
651
|
+
return { session: run, outcome: reported.outcome, cause: reported.cause };
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
export async function reportSessions(session, held) {
|
|
655
|
+
const sessions = held.map((entry) => heldOutcome(entry.run, entry.reported));
|
|
656
|
+
let offset = 0;
|
|
657
|
+
do {
|
|
658
|
+
await houseRequest(
|
|
659
|
+
session.house,
|
|
660
|
+
await heldControl(session),
|
|
661
|
+
KIT_SESSIONS_REPORT_PATH,
|
|
662
|
+
{ sessions: sessions.slice(offset, offset + KIT_SESSIONS_REPORT_MAX) },
|
|
663
|
+
session.dependencies,
|
|
664
|
+
session.signal,
|
|
665
|
+
);
|
|
666
|
+
offset += KIT_SESSIONS_REPORT_MAX;
|
|
667
|
+
} while (offset < sessions.length);
|
|
668
|
+
return sessions;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
export async function streamAssistantDraft(session, workerSession, blocks) {
|
|
672
|
+
const answer = await houseRequest(
|
|
673
|
+
session.house,
|
|
674
|
+
await heldControl(session),
|
|
675
|
+
assistantDraftsPath(workerSession),
|
|
676
|
+
{ blocks },
|
|
677
|
+
session.dependencies,
|
|
678
|
+
session.signal,
|
|
679
|
+
);
|
|
680
|
+
if (!ASSISTANT_DRAFT_STATES.includes(answer?.state)) {
|
|
681
|
+
throw new KitRefusal("kit_assistant_draft_rejected", workerSession);
|
|
682
|
+
}
|
|
683
|
+
return { state: answer.state };
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
export async function acceptAssistantText(session, workerSession, sealed) {
|
|
687
|
+
const answer = await houseRequest(
|
|
688
|
+
session.house,
|
|
689
|
+
await heldControl(session),
|
|
690
|
+
assistantTextPath(workerSession),
|
|
691
|
+
{ operation_id: sealed.operation, blocks: sealed.blocks },
|
|
692
|
+
session.dependencies,
|
|
693
|
+
session.signal,
|
|
694
|
+
);
|
|
695
|
+
if (
|
|
696
|
+
answer?.session !== workerSession ||
|
|
697
|
+
typeof answer.turn !== "string" ||
|
|
698
|
+
answer.turn.length === 0 ||
|
|
699
|
+
(answer.delivery !== null && typeof answer.delivery !== "string")
|
|
700
|
+
) {
|
|
701
|
+
throw new KitRefusal("kit_assistant_text_rejected", workerSession);
|
|
702
|
+
}
|
|
703
|
+
return { turn: answer.turn, delivery: answer.delivery };
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
export async function admitPlanSnapshot(session, workerSession, snapshot) {
|
|
707
|
+
const answer = await houseRequest(
|
|
708
|
+
session.house,
|
|
709
|
+
await heldControl(session),
|
|
710
|
+
`/kit/sessions/${encodeURIComponent(workerSession)}/plans`,
|
|
711
|
+
snapshot,
|
|
712
|
+
session.dependencies,
|
|
713
|
+
session.signal,
|
|
714
|
+
);
|
|
715
|
+
if (
|
|
716
|
+
answer?.session !== workerSession ||
|
|
717
|
+
typeof answer.plan !== "string" ||
|
|
718
|
+
!["live", "frozen"].includes(answer.state)
|
|
719
|
+
) {
|
|
720
|
+
throw new KitRefusal("kit_plan_rejected", workerSession);
|
|
721
|
+
}
|
|
722
|
+
return { plan: answer.plan, state: answer.state };
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
export async function observeSessionStart(session, workerSession) {
|
|
726
|
+
return houseRequest(
|
|
727
|
+
session.house,
|
|
728
|
+
await heldControl(session),
|
|
729
|
+
`/kit/sessions/${encodeURIComponent(workerSession)}/started`,
|
|
730
|
+
{},
|
|
731
|
+
session.dependencies,
|
|
732
|
+
session.signal,
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
export async function observeSessionStop(session, workerSession, turnCompleted = false) {
|
|
737
|
+
return houseRequest(
|
|
738
|
+
session.house,
|
|
739
|
+
await heldControl(session),
|
|
740
|
+
`/kit/sessions/${encodeURIComponent(workerSession)}/stopped`,
|
|
741
|
+
turnCompleted ? { turn_completed: true } : {},
|
|
742
|
+
session.dependencies,
|
|
743
|
+
session.signal,
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
export async function reportContinuation(session, workerSession, report) {
|
|
748
|
+
return houseRequest(
|
|
749
|
+
session.house,
|
|
750
|
+
await heldControl(session),
|
|
751
|
+
`/kit/sessions/${encodeURIComponent(workerSession)}/continuation`,
|
|
752
|
+
{
|
|
753
|
+
provider_continuation: report.providerContinuation,
|
|
754
|
+
provider_capacity_exhausted: report.providerCapacityExhausted,
|
|
755
|
+
},
|
|
756
|
+
session.dependencies,
|
|
757
|
+
session.signal,
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
export async function replaceSessionAuthority(session, workerSession, signal = session.signal) {
|
|
762
|
+
const replaced = await houseRequest(
|
|
763
|
+
session.house,
|
|
764
|
+
await heldControl(session, signal),
|
|
765
|
+
`/kit/sessions/${encodeURIComponent(workerSession)}/token`,
|
|
766
|
+
{},
|
|
767
|
+
session.dependencies,
|
|
768
|
+
signal,
|
|
769
|
+
);
|
|
770
|
+
const expiresAt = Date.parse(replaced?.bridge_token_expires_at);
|
|
771
|
+
if (typeof replaced?.bridge_token !== "string" || Number.isNaN(expiresAt)) {
|
|
772
|
+
throw new KitRefusal("kit_session_token_rejected", workerSession);
|
|
773
|
+
}
|
|
774
|
+
return { token: replaced.bridge_token, expiresAt };
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
export async function sessionAuthority(session, workerSession, bridge, signal = session.signal) {
|
|
778
|
+
if (bridge.expiresAt - session.now().getTime() > WORKER_RUN_TOKEN_MARGIN_MS) return bridge.token;
|
|
779
|
+
Object.assign(bridge, await replaceSessionAuthority(session, workerSession, signal));
|
|
780
|
+
return bridge.token;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const commands = declarationReader("kit_command_rejected");
|
|
784
|
+
|
|
785
|
+
function acquiredStop(value, declaration) {
|
|
786
|
+
const acquired = commands.plain(value, declaration);
|
|
787
|
+
if (acquired.command !== "stop") return null;
|
|
788
|
+
return {
|
|
789
|
+
run: commands.text(acquired.run, `${declaration}.run`, 128),
|
|
790
|
+
session: commands.text(acquired.session, `${declaration}.session`, 128),
|
|
791
|
+
requestedAt: commands.text(acquired.requested_at, `${declaration}.requested_at`, 64),
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function acquiredInterrupt(value, declaration) {
|
|
796
|
+
const acquired = commands.plain(value, declaration);
|
|
797
|
+
if (acquired.command !== "interrupt") return null;
|
|
798
|
+
return {
|
|
799
|
+
interrupt: commands.text(acquired.interrupt, `${declaration}.interrupt`, 128),
|
|
800
|
+
run: commands.text(acquired.run, `${declaration}.run`, 128),
|
|
801
|
+
session: commands.text(acquired.session, `${declaration}.session`, 128),
|
|
802
|
+
requestedAt: commands.text(acquired.requested_at, `${declaration}.requested_at`, 64),
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
export async function reportSessionHeartbeat(session, workerSession) {
|
|
807
|
+
await houseRequest(
|
|
808
|
+
session.house,
|
|
809
|
+
await heldControl(session),
|
|
810
|
+
`/kit/sessions/${encodeURIComponent(workerSession)}/heartbeat`,
|
|
811
|
+
{},
|
|
812
|
+
session.dependencies,
|
|
813
|
+
session.signal,
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function answeredValue(value, declaration) {
|
|
818
|
+
if (typeof value === "boolean") return value;
|
|
819
|
+
if (typeof value === "number") {
|
|
820
|
+
if (!Number.isFinite(value)) commands.refuse(declaration);
|
|
821
|
+
return value;
|
|
822
|
+
}
|
|
823
|
+
if (typeof value === "string") {
|
|
824
|
+
if (value.length > INTERACTION_VALUE_MAX) commands.refuse(declaration);
|
|
825
|
+
return value;
|
|
826
|
+
}
|
|
827
|
+
if (!Array.isArray(value) || value.length > INTERACTION_VALUES_MAX) commands.refuse(declaration);
|
|
828
|
+
return value.map((item, position) => {
|
|
829
|
+
const named = `${declaration}[${position}]`;
|
|
830
|
+
if (typeof item !== "string" || item.length > INTERACTION_VALUE_MAX) commands.refuse(named);
|
|
831
|
+
return item;
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function answeredContent(value, declaration) {
|
|
836
|
+
const content = commands.plain(value, declaration);
|
|
837
|
+
const named = Object.keys(content);
|
|
838
|
+
return Object.fromEntries(
|
|
839
|
+
named.map((name) => {
|
|
840
|
+
if (name.length === 0 || name.length > INTERACTION_PROPERTY_MAX) {
|
|
841
|
+
commands.refuse(`${declaration}.${name}`);
|
|
842
|
+
}
|
|
843
|
+
return [name, answeredValue(content[name], `${declaration}.${name}`)];
|
|
844
|
+
}),
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function acquiredResponse(value, declaration) {
|
|
849
|
+
const response = commands.closed(value, ["action", "content"], declaration);
|
|
850
|
+
if (response.action === "decline") {
|
|
851
|
+
if (response.content !== undefined) commands.refuse(`${declaration}.content`);
|
|
852
|
+
return { action: "decline" };
|
|
853
|
+
}
|
|
854
|
+
if (response.action !== "accept") commands.refuse(`${declaration}.action`);
|
|
855
|
+
return { action: "accept", content: answeredContent(response.content, `${declaration}.content`) };
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function acquiredAnswer(value, declaration) {
|
|
859
|
+
const acquired = commands.plain(value, declaration);
|
|
860
|
+
if (acquired.command !== "answer") return null;
|
|
861
|
+
return {
|
|
862
|
+
interaction: commands.text(acquired.interaction, `${declaration}.interaction`, 128),
|
|
863
|
+
run: commands.text(acquired.run, `${declaration}.run`, 128),
|
|
864
|
+
session: commands.text(acquired.session, `${declaration}.session`, 128),
|
|
865
|
+
providerTurn: commands.text(
|
|
866
|
+
acquired.provider_turn,
|
|
867
|
+
`${declaration}.provider_turn`,
|
|
868
|
+
INTERACTION_CORRELATION_MAX,
|
|
869
|
+
),
|
|
870
|
+
correlation: commands.text(
|
|
871
|
+
acquired.correlation,
|
|
872
|
+
`${declaration}.correlation`,
|
|
873
|
+
INTERACTION_CORRELATION_MAX,
|
|
874
|
+
),
|
|
875
|
+
revision: commands.positiveCount(acquired.revision, `${declaration}.revision`),
|
|
876
|
+
response: acquiredResponse(acquired.response, `${declaration}.response`),
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
export async function acquireCommands(session, max, kinds = ["stop"]) {
|
|
881
|
+
const answer = await houseRequest(
|
|
882
|
+
session.house,
|
|
883
|
+
await heldControl(session),
|
|
884
|
+
"/kit/commands/acquire",
|
|
885
|
+
{ max, kinds },
|
|
886
|
+
session.dependencies,
|
|
887
|
+
session.signal,
|
|
888
|
+
);
|
|
889
|
+
if (!Array.isArray(answer?.commands)) commands.refuse("commands");
|
|
890
|
+
const acquired = answer.commands.map((command, position) => ({
|
|
891
|
+
command,
|
|
892
|
+
declaration: `commands[${position}]`,
|
|
893
|
+
}));
|
|
894
|
+
return {
|
|
895
|
+
stops: acquired
|
|
896
|
+
.map((entry) => acquiredStop(entry.command, entry.declaration))
|
|
897
|
+
.filter((stop) => stop !== null),
|
|
898
|
+
answers: acquired
|
|
899
|
+
.map((entry) => acquiredAnswer(entry.command, entry.declaration))
|
|
900
|
+
.filter((answer) => answer !== null),
|
|
901
|
+
interrupts: acquired
|
|
902
|
+
.map((entry) => acquiredInterrupt(entry.command, entry.declaration))
|
|
903
|
+
.filter((interrupt) => interrupt !== null),
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const INTERACTION_STATES = ["pending", "answered", "settled"];
|
|
908
|
+
|
|
909
|
+
export async function admitInteraction(session, workerSession, batch) {
|
|
910
|
+
const answer = await houseRequest(
|
|
911
|
+
session.house,
|
|
912
|
+
await heldControl(session),
|
|
913
|
+
sessionInteractionsPath(workerSession),
|
|
914
|
+
batch,
|
|
915
|
+
session.dependencies,
|
|
916
|
+
session.signal,
|
|
917
|
+
);
|
|
918
|
+
if (
|
|
919
|
+
answer?.session !== workerSession ||
|
|
920
|
+
!INTERACTION_STATES.includes(answer.state) ||
|
|
921
|
+
typeof answer.interaction !== "string" ||
|
|
922
|
+
answer.interaction.length === 0 ||
|
|
923
|
+
answer.interaction.length > 128
|
|
924
|
+
) {
|
|
925
|
+
throw new KitRefusal("kit_interaction_rejected", workerSession);
|
|
926
|
+
}
|
|
927
|
+
return {
|
|
928
|
+
interaction: answer.interaction,
|
|
929
|
+
state: answer.state,
|
|
930
|
+
disposition: answer.disposition ?? null,
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
const SECRET_RELAY_OUTCOMES = ["collected", "released"];
|
|
935
|
+
|
|
936
|
+
function relayedCollection(answer, ceremony) {
|
|
937
|
+
if (!SECRET_RELAY_OUTCOMES.includes(answer?.outcome)) return null;
|
|
938
|
+
if (answer.outcome === "released") return { collected: null };
|
|
939
|
+
const content = answer.content;
|
|
940
|
+
if (typeof content !== "object" || content === null || Array.isArray(content)) return null;
|
|
941
|
+
const wanted = ceremony.steps.filter((step) => step.kind === "collect").map((step) => step.name);
|
|
942
|
+
const given = Object.keys(content);
|
|
943
|
+
if (given.length !== wanted.length || wanted.some((name) => !given.includes(name))) return null;
|
|
944
|
+
if (
|
|
945
|
+
given.some(
|
|
946
|
+
(name) => typeof content[name] !== "string" || content[name].length > SECRET_VALUE_MAX,
|
|
947
|
+
)
|
|
948
|
+
) {
|
|
949
|
+
return null;
|
|
950
|
+
}
|
|
951
|
+
return { collected: content };
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
export async function awaitSecretInput(session, interaction, ceremony, signal = session.signal) {
|
|
955
|
+
const answer = await houseRequest(
|
|
956
|
+
session.house,
|
|
957
|
+
await heldControl(session, signal),
|
|
958
|
+
secretInputPath(interaction),
|
|
959
|
+
{ steps: ceremony.steps },
|
|
960
|
+
session.dependencies,
|
|
961
|
+
signal,
|
|
962
|
+
);
|
|
963
|
+
const relayed = relayedCollection(answer, ceremony);
|
|
964
|
+
if (relayed === null) throw new KitRefusal("kit_interaction_rejected", interaction);
|
|
965
|
+
return relayed;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
export async function reportInteractionOutcome(session, interaction, outcome) {
|
|
969
|
+
if (!INTERACTION_OUTCOMES.includes(outcome)) {
|
|
970
|
+
throw new KitRefusal("kit_interaction_rejected", interaction);
|
|
971
|
+
}
|
|
972
|
+
const answer = await houseRequest(
|
|
973
|
+
session.house,
|
|
974
|
+
await heldControl(session),
|
|
975
|
+
interactionOutcomePath(interaction),
|
|
976
|
+
{ outcome },
|
|
977
|
+
session.dependencies,
|
|
978
|
+
session.signal,
|
|
979
|
+
);
|
|
980
|
+
if (answer?.interaction !== interaction) {
|
|
981
|
+
throw new KitRefusal("kit_interaction_rejected", interaction);
|
|
982
|
+
}
|
|
983
|
+
return { disposition: answer.disposition ?? null };
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
export async function reportInterruptOutcome(session, interrupt, outcome) {
|
|
987
|
+
const answer = await houseRequest(
|
|
988
|
+
session.house,
|
|
989
|
+
await heldControl(session),
|
|
990
|
+
`/kit/interrupts/${encodeURIComponent(interrupt)}/outcome`,
|
|
991
|
+
{ outcome },
|
|
992
|
+
session.dependencies,
|
|
993
|
+
session.signal,
|
|
994
|
+
);
|
|
995
|
+
if (answer?.interrupt !== interrupt || answer.outcome !== outcome) {
|
|
996
|
+
throw new KitRefusal("kit_interrupt_rejected", interrupt);
|
|
997
|
+
}
|
|
998
|
+
return { outcome: answer.outcome };
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
export async function acquireOneResumption(session) {
|
|
1002
|
+
const answer = await houseRequest(session.house, await heldControl(session), "/kit/commands/acquire",
|
|
1003
|
+
{ max: 1, kinds: ["resume"] }, session.dependencies, session.signal);
|
|
1004
|
+
if (!Array.isArray(answer?.commands) || answer.commands.length > 1) commands.refuse("commands");
|
|
1005
|
+
if (answer.commands.length === 0) return null;
|
|
1006
|
+
const command = commands.plain(answer.commands[0], "commands[0]");
|
|
1007
|
+
if (command.command !== "resume" || command.origin !== "resumption") commands.refuse("commands[0].command");
|
|
1008
|
+
const bridge = commands.plain(command.session, "commands[0].session");
|
|
1009
|
+
const expiresAt = Date.parse(bridge.bridge_token_expires_at);
|
|
1010
|
+
if (!Number.isFinite(expiresAt)) commands.refuse("commands[0].session.bridge_token_expires_at");
|
|
1011
|
+
return {
|
|
1012
|
+
run: commands.text(command.run, "commands[0].run", 128),
|
|
1013
|
+
route: acquiredRoute(command.route, "commands[0].route"),
|
|
1014
|
+
session: {
|
|
1015
|
+
id: commands.text(bridge.id, "commands[0].session.id", 128),
|
|
1016
|
+
token: commands.text(bridge.bridge_token, "commands[0].session.bridge_token", 512),
|
|
1017
|
+
expiresAt,
|
|
1018
|
+
},
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
export async function rejectResumption(session, workerSession) {
|
|
1023
|
+
return houseRequest(session.house, await heldControl(session),
|
|
1024
|
+
`/kit/sessions/${encodeURIComponent(workerSession)}/resumption-rejected`, {}, session.dependencies, session.signal);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function mcpNameHeader(frame) {
|
|
1028
|
+
const value = frame.method === "resources/read"
|
|
1029
|
+
? frame.params?.uri
|
|
1030
|
+
: ["tools/call", "prompts/get"].includes(frame.method) ? frame.params?.name : undefined;
|
|
1031
|
+
if (typeof value !== "string") return {};
|
|
1032
|
+
const plain = /^[\t\x20-\x7e]+$/.test(value) && value === value.trim() &&
|
|
1033
|
+
!(value.startsWith("=?base64?") && value.endsWith("?="));
|
|
1034
|
+
return { "mcp-name": plain ? value : `=?base64?${Buffer.from(value).toString("base64")}?=` };
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
export async function callHouseMcp(session, token, frame, signal = session.signal) {
|
|
1038
|
+
if (signal?.aborted) throw new KitRefusal("kit_command_cancelled");
|
|
1039
|
+
const fetcher = session.dependencies.fetch ?? fetch;
|
|
1040
|
+
let response;
|
|
1041
|
+
try {
|
|
1042
|
+
response = await fetcher(`${session.house}/`, {
|
|
1043
|
+
method: "POST",
|
|
1044
|
+
headers: {
|
|
1045
|
+
accept: "application/json, text/event-stream",
|
|
1046
|
+
authorization: `Bearer ${token}`,
|
|
1047
|
+
"content-type": "application/json",
|
|
1048
|
+
"mcp-protocol-version": MCP_PROTOCOL_VERSION,
|
|
1049
|
+
...(typeof frame.method === "string" ? { "mcp-method": frame.method } : {}),
|
|
1050
|
+
...mcpNameHeader(frame),
|
|
1051
|
+
},
|
|
1052
|
+
body: JSON.stringify(frame),
|
|
1053
|
+
signal,
|
|
1054
|
+
});
|
|
1055
|
+
} catch {
|
|
1056
|
+
if (signal?.aborted) throw new KitRefusal("kit_command_cancelled");
|
|
1057
|
+
throw new KitRefusal("kit_house_unavailable", "/");
|
|
1058
|
+
}
|
|
1059
|
+
if (response.status >= 500) {
|
|
1060
|
+
throw houseRefusal(response.status, await response.json().catch(() => null), "/");
|
|
1061
|
+
}
|
|
1062
|
+
let serialized;
|
|
1063
|
+
try {
|
|
1064
|
+
serialized = await response.text();
|
|
1065
|
+
} catch {
|
|
1066
|
+
if (signal?.aborted) throw new KitRefusal("kit_command_cancelled");
|
|
1067
|
+
if (!response.ok) throw new KitRefusal("kit_house_mcp_rejected", `${response.status}`);
|
|
1068
|
+
throw new KitRefusal("kit_house_unavailable", "/");
|
|
1069
|
+
}
|
|
1070
|
+
if (serialized.trim() === "") {
|
|
1071
|
+
if (!response.ok) throw new KitRefusal("kit_house_mcp_rejected", `${response.status}`);
|
|
1072
|
+
if (frame.id === undefined) return null;
|
|
1073
|
+
throw new KitRefusal("kit_house_unavailable", "/");
|
|
1074
|
+
}
|
|
1075
|
+
let answer;
|
|
1076
|
+
try {
|
|
1077
|
+
answer = JSON.parse(serialized);
|
|
1078
|
+
} catch {
|
|
1079
|
+
throw new KitRefusal("kit_house_mcp_rejected", `${response.status}`);
|
|
1080
|
+
}
|
|
1081
|
+
if (answer?.jsonrpc !== "2.0") {
|
|
1082
|
+
throw new KitRefusal("kit_house_mcp_rejected", `${response.status}`);
|
|
1083
|
+
}
|
|
1084
|
+
return answer;
|
|
1085
|
+
}
|