@agentshouse/kit 0.1.0-alpha.2 → 0.1.0-alpha.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +299 -26
- package/bin/connect-linux.sh +85 -13
- package/bin/kit.js +23 -3
- package/bootstrap.json +17 -0
- package/package.json +7 -2
- package/src/adapter-mcp.js +0 -13
- package/src/agents.js +63 -8
- package/src/binding.js +41 -0
- package/src/bridge.js +1 -1
- package/src/contract.js +21 -3
- package/src/git-main.js +138 -0
- package/src/git.js +62 -0
- package/src/host.js +6 -2
- package/src/house-main.js +65 -8
- package/src/house.js +345 -73
- package/src/inbound.js +2 -18
- package/src/kit-work-stream.js +88 -0
- package/src/launcher.js +9 -3
- package/src/local-files.js +3 -11
- package/src/login.js +2 -1
- package/src/mcp.js +13 -0
- package/src/mount.js +13 -21
- package/src/repository.js +3 -61
- package/src/session-bin/git +2 -0
- package/src/setup.js +7 -2
- package/src/source.js +44 -30
- package/src/sync-library.js +7 -11
- package/src/sync-state.js +4 -0
- package/src/sync.js +15 -10
- package/src/telegram.js +29 -11
- package/src/worker.js +529 -116
- package/src/working-copy.js +1210 -0
package/src/house.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { chmod, mkdir, readFile, rm } from "node:fs/promises";
|
|
3
|
+
import { connect, createServer } from "node:net";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
3
5
|
import { assertAcquiredCommandAdapter } from "./adapter.js";
|
|
6
|
+
import { readFrames, writeFrame } from "./channel.js";
|
|
4
7
|
import {
|
|
5
8
|
assistantDraftsPath,
|
|
6
9
|
assistantTextPath,
|
|
@@ -26,6 +29,7 @@ import {
|
|
|
26
29
|
MCP_PROTOCOL_VERSION,
|
|
27
30
|
WORKER_RUN_TOKEN_MARGIN_MS,
|
|
28
31
|
runOutcomePath,
|
|
32
|
+
runRequestPath,
|
|
29
33
|
secretInputPath,
|
|
30
34
|
SECRET_VALUE_MAX,
|
|
31
35
|
sessionInteractionsPath,
|
|
@@ -35,6 +39,10 @@ import { declarationReader } from "./declaration.js";
|
|
|
35
39
|
import { houseRefusal, KitRefusal, throwHouseAnswer } from "./refusal.js";
|
|
36
40
|
|
|
37
41
|
const LEASE = /^ahl_[A-Za-z0-9_-]+$/;
|
|
42
|
+
const RESIDENT = "Resident";
|
|
43
|
+
const LEASE_ACTIVE = "environment_control_lease_active";
|
|
44
|
+
const UPGRADE_REQUIRED = "kit_upgrade_required";
|
|
45
|
+
const UPDATE_DRAINING = "kit_update_draining";
|
|
38
46
|
const ASSISTANT_DRAFT_STATES = ["streamed", "unsupported"];
|
|
39
47
|
const ITEM_OUTCOMES = ["accepted", "duplicate", "rejected"];
|
|
40
48
|
|
|
@@ -49,6 +57,10 @@ async function readCredential(directory) {
|
|
|
49
57
|
return stored;
|
|
50
58
|
}
|
|
51
59
|
|
|
60
|
+
function latchRefusal(answer, dependencies) {
|
|
61
|
+
if (answer?.error?.code === UPGRADE_REQUIRED) dependencies.contractRefused?.();
|
|
62
|
+
}
|
|
63
|
+
|
|
52
64
|
async function houseAnswer(house, path, init, dependencies, signal) {
|
|
53
65
|
const fetcher = dependencies.fetch ?? fetch;
|
|
54
66
|
let response;
|
|
@@ -66,6 +78,7 @@ async function houseAnswer(house, path, init, dependencies, signal) {
|
|
|
66
78
|
} catch {
|
|
67
79
|
answer = null;
|
|
68
80
|
}
|
|
81
|
+
latchRefusal(answer, dependencies);
|
|
69
82
|
throwHouseAnswer(response.status, answer, path);
|
|
70
83
|
if (answer === null) throw new KitRefusal("kit_house_unavailable", path);
|
|
71
84
|
return answer;
|
|
@@ -105,7 +118,245 @@ function heldLease(answer) {
|
|
|
105
118
|
return { lease: answer.lease, expiresAt: Date.parse(answer.expires_at) };
|
|
106
119
|
}
|
|
107
120
|
|
|
121
|
+
function residentSocket(directory) {
|
|
122
|
+
return join(directory, "run", "resident.sock");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function updateDrain(directory) {
|
|
126
|
+
return join(directory, "state", "update-drain");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function refuseDrained(directory) {
|
|
130
|
+
if (existsSync(updateDrain(directory))) throw new KitRefusal(UPDATE_DRAINING);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function residentAnswer(status, code) {
|
|
134
|
+
const body = Buffer.from(JSON.stringify({ error: { code } })).toString("base64");
|
|
135
|
+
return { status, type: "application/json", body };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function newWork(path) {
|
|
139
|
+
return path === "/kit/telegram/updates" || (path.endsWith("/acquire") && path !== "/kit/commands/acquire");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function residentRelay(session, request, signal, draining) {
|
|
143
|
+
const path = typeof request.path === "string" ? request.path : "";
|
|
144
|
+
if (!path.startsWith("/kit/") || path.startsWith("/kit/lease")) {
|
|
145
|
+
return residentAnswer(403, "kit_resident_route_refused");
|
|
146
|
+
}
|
|
147
|
+
if (draining && newWork(path)) {
|
|
148
|
+
return residentAnswer(409, UPDATE_DRAINING);
|
|
149
|
+
}
|
|
150
|
+
let authorization;
|
|
151
|
+
try {
|
|
152
|
+
authorization = await heldControl(session, signal);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
return residentAnswer(error.status ?? 401, error.code ?? "kit_lease_rejected");
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
const response = await (session.dependencies.fetch ?? fetch)(`${session.house}${path}`, {
|
|
158
|
+
method: request.method,
|
|
159
|
+
headers: { ...request.headers, authorization },
|
|
160
|
+
body: request.text ?? (request.bytes === undefined ? undefined : Buffer.from(request.bytes, "base64")),
|
|
161
|
+
signal,
|
|
162
|
+
});
|
|
163
|
+
const body = Buffer.from(await response.arrayBuffer());
|
|
164
|
+
if (response.status === 409) latchRefusal(JSON.parse(body.toString("utf8")), session.dependencies);
|
|
165
|
+
return {
|
|
166
|
+
status: response.status,
|
|
167
|
+
type: response.headers.get("content-type"),
|
|
168
|
+
body: body.length === 0 ? null : body.toString("base64"),
|
|
169
|
+
};
|
|
170
|
+
} catch {
|
|
171
|
+
return residentAnswer(503, "kit_house_unavailable");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function serveResident(session, directory, signal, drain, push) {
|
|
176
|
+
const socket = residentSocket(directory);
|
|
177
|
+
await mkdir(dirname(socket), { recursive: true, mode: 0o700 });
|
|
178
|
+
await rm(socket, { force: true });
|
|
179
|
+
const connections = new Set();
|
|
180
|
+
let holder = null;
|
|
181
|
+
let draining = false;
|
|
182
|
+
function answerDrain(connection) {
|
|
183
|
+
draining = true;
|
|
184
|
+
drain(holder !== null).then(
|
|
185
|
+
(answer) => {
|
|
186
|
+
if (!connection.destroyed) writeFrame(connection, answer);
|
|
187
|
+
connection.end();
|
|
188
|
+
},
|
|
189
|
+
() => connection.destroy(),
|
|
190
|
+
);
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
function hold(connection) {
|
|
194
|
+
if (holder !== null || draining || existsSync(updateDrain(directory))) {
|
|
195
|
+
writeFrame(connection, { refused: draining || holder === null ? UPDATE_DRAINING : LEASE_ACTIVE });
|
|
196
|
+
connection.end();
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
holder = connection;
|
|
200
|
+
renewControl(session, signal).then(
|
|
201
|
+
() => writeFrame(connection, { held: true }),
|
|
202
|
+
(error) => {
|
|
203
|
+
writeFrame(connection, { refused: error.code ?? "kit_lease_rejected" });
|
|
204
|
+
connection.end();
|
|
205
|
+
},
|
|
206
|
+
);
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
const server = createServer((connection) => {
|
|
210
|
+
connections.add(connection);
|
|
211
|
+
connection.on("error", () => connection.destroy());
|
|
212
|
+
const relayed = new Map();
|
|
213
|
+
let held = null;
|
|
214
|
+
connection.on("close", () => {
|
|
215
|
+
connections.delete(connection);
|
|
216
|
+
if (holder === connection) holder = null;
|
|
217
|
+
for (const cancelled of relayed.values()) cancelled.abort();
|
|
218
|
+
});
|
|
219
|
+
readFrames(connection, {
|
|
220
|
+
onFrame: (request) => {
|
|
221
|
+
if (held === null) {
|
|
222
|
+
held = request.drain === true ? answerDrain(connection) : hold(connection);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (!held) return;
|
|
226
|
+
if (request.cancel === true) {
|
|
227
|
+
relayed.get(request.id)?.abort();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const cancelled = new AbortController();
|
|
231
|
+
relayed.set(request.id, cancelled);
|
|
232
|
+
const relaySignal = AbortSignal.any([signal, cancelled.signal]);
|
|
233
|
+
const answered =
|
|
234
|
+
request.push === undefined
|
|
235
|
+
? residentRelay(session, request, relaySignal, draining)
|
|
236
|
+
: push(request.push, relaySignal).then((pushed) => ({ pushed }));
|
|
237
|
+
void answered.then((answer) => {
|
|
238
|
+
relayed.delete(request.id);
|
|
239
|
+
if (!connection.destroyed) writeFrame(connection, { id: request.id, ...answer });
|
|
240
|
+
});
|
|
241
|
+
},
|
|
242
|
+
onRefusal: () => connection.destroy(),
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
await new Promise((resolve, reject) => {
|
|
246
|
+
server.once("error", reject);
|
|
247
|
+
server.listen(socket, resolve);
|
|
248
|
+
});
|
|
249
|
+
await chmod(socket, 0o600);
|
|
250
|
+
return {
|
|
251
|
+
serving: () => holder !== null,
|
|
252
|
+
async close() {
|
|
253
|
+
for (const connection of connections) connection.destroy();
|
|
254
|
+
await new Promise((resolve) => server.close(resolve));
|
|
255
|
+
await rm(socket, { force: true });
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function requestDrain(directory) {
|
|
261
|
+
const connection = connect(residentSocket(directory));
|
|
262
|
+
return new Promise((resolve, reject) => {
|
|
263
|
+
const unavailable = () => reject(new KitRefusal("kit_resident_unavailable"));
|
|
264
|
+
connection.on("error", unavailable);
|
|
265
|
+
connection.on("close", unavailable);
|
|
266
|
+
readFrames(connection, {
|
|
267
|
+
onFrame: (answer) => {
|
|
268
|
+
resolve(answer);
|
|
269
|
+
connection.end();
|
|
270
|
+
},
|
|
271
|
+
onRefusal: () => connection.destroy(),
|
|
272
|
+
});
|
|
273
|
+
writeFrame(connection, { drain: true });
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function residentDelegation(directory, fetcher) {
|
|
278
|
+
const connection = connect(residentSocket(directory));
|
|
279
|
+
const pending = new Map();
|
|
280
|
+
let greet;
|
|
281
|
+
const greeting = new Promise((resolve) => {
|
|
282
|
+
greet = resolve;
|
|
283
|
+
});
|
|
284
|
+
const unavailable = () => residentAnswer(503, "kit_resident_unavailable");
|
|
285
|
+
function settle(id, answer) {
|
|
286
|
+
pending.get(id)?.(answer);
|
|
287
|
+
pending.delete(id);
|
|
288
|
+
if (pending.size === 0) connection.unref();
|
|
289
|
+
}
|
|
290
|
+
connection.on("error", () => connection.destroy());
|
|
291
|
+
connection.on("close", () => {
|
|
292
|
+
greet(null);
|
|
293
|
+
for (const id of [...pending.keys()]) settle(id, unavailable());
|
|
294
|
+
});
|
|
295
|
+
readFrames(connection, {
|
|
296
|
+
onFrame: (frame) => (frame.id === undefined ? greet(frame) : settle(frame.id, frame)),
|
|
297
|
+
onRefusal: () => connection.destroy(),
|
|
298
|
+
});
|
|
299
|
+
writeFrame(connection, { hold: true });
|
|
300
|
+
const held = await greeting;
|
|
301
|
+
if (held === null) return null;
|
|
302
|
+
if (held.held !== true) {
|
|
303
|
+
connection.destroy();
|
|
304
|
+
throw new KitRefusal(held.refused);
|
|
305
|
+
}
|
|
306
|
+
connection.unref();
|
|
307
|
+
let next = 0;
|
|
308
|
+
function exchange(frame, signal) {
|
|
309
|
+
signal?.throwIfAborted();
|
|
310
|
+
if (connection.destroyed) return Promise.resolve(unavailable());
|
|
311
|
+
return new Promise((resolve, reject) => {
|
|
312
|
+
const id = (next += 1);
|
|
313
|
+
const cancel = () => {
|
|
314
|
+
pending.delete(id);
|
|
315
|
+
settle(id);
|
|
316
|
+
if (!connection.destroyed) writeFrame(connection, { id, cancel: true });
|
|
317
|
+
reject(signal.reason);
|
|
318
|
+
};
|
|
319
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
320
|
+
pending.set(id, (answer) => {
|
|
321
|
+
signal?.removeEventListener("abort", cancel);
|
|
322
|
+
resolve(answer);
|
|
323
|
+
});
|
|
324
|
+
connection.ref();
|
|
325
|
+
writeFrame(connection, { id, ...frame });
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
async function push(request, signal) {
|
|
329
|
+
const answer = await exchange({ push: request }, signal);
|
|
330
|
+
if (answer.pushed === undefined) throw new KitRefusal("kit_resident_unavailable");
|
|
331
|
+
return answer.pushed;
|
|
332
|
+
}
|
|
333
|
+
async function delegated(url, init = {}) {
|
|
334
|
+
const { authorization, ...headers } = init.headers ?? {};
|
|
335
|
+
if (authorization !== RESIDENT) return fetcher(url, init);
|
|
336
|
+
const address = new URL(url);
|
|
337
|
+
const answer = await exchange(
|
|
338
|
+
{
|
|
339
|
+
method: init.method ?? "GET",
|
|
340
|
+
path: `${address.pathname}${address.search}`,
|
|
341
|
+
headers,
|
|
342
|
+
...(typeof init.body === "string"
|
|
343
|
+
? { text: init.body }
|
|
344
|
+
: init.body === undefined
|
|
345
|
+
? {}
|
|
346
|
+
: { bytes: Buffer.from(init.body).toString("base64") }),
|
|
347
|
+
},
|
|
348
|
+
init.signal,
|
|
349
|
+
);
|
|
350
|
+
return new Response(answer.body === null ? null : Buffer.from(answer.body, "base64"), {
|
|
351
|
+
status: answer.status,
|
|
352
|
+
headers: answer.type === null ? {} : { "content-type": answer.type },
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
return { connection, fetch: delegated, push };
|
|
356
|
+
}
|
|
357
|
+
|
|
108
358
|
export async function openSession(options, dependencies = {}) {
|
|
359
|
+
if (options.resident !== true) refuseDrained(options.directory);
|
|
109
360
|
const session = {
|
|
110
361
|
house: options.house,
|
|
111
362
|
dependencies,
|
|
@@ -114,27 +365,40 @@ export async function openSession(options, dependencies = {}) {
|
|
|
114
365
|
expiresAt: 0,
|
|
115
366
|
signal: options.signal,
|
|
116
367
|
};
|
|
368
|
+
const resident =
|
|
369
|
+
options.acquire === true || options.resident === true
|
|
370
|
+
? null
|
|
371
|
+
: await residentDelegation(options.directory, dependencies.fetch ?? fetch);
|
|
372
|
+
if (resident !== null) {
|
|
373
|
+
session.resident = resident.connection;
|
|
374
|
+
session.push = resident.push;
|
|
375
|
+
session.dependencies = { ...dependencies, fetch: resident.fetch };
|
|
376
|
+
return session;
|
|
377
|
+
}
|
|
378
|
+
session.dependencies = {
|
|
379
|
+
...dependencies,
|
|
380
|
+
contractRefused: () => {
|
|
381
|
+
session.refused = true;
|
|
382
|
+
},
|
|
383
|
+
};
|
|
117
384
|
const credential = await readCredential(options.directory);
|
|
118
|
-
|
|
119
|
-
session,
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
);
|
|
385
|
+
const answer = await houseRequest(
|
|
386
|
+
session.house,
|
|
387
|
+
`Bearer ${credential}`,
|
|
388
|
+
"/kit/lease",
|
|
389
|
+
{},
|
|
390
|
+
session.dependencies,
|
|
391
|
+
session.signal,
|
|
392
|
+
).catch((error) => {
|
|
393
|
+
if (options.resident === true && session.refused) return null;
|
|
394
|
+
throw error;
|
|
395
|
+
});
|
|
396
|
+
if (answer !== null) Object.assign(session, heldLease(answer));
|
|
131
397
|
return session;
|
|
132
398
|
}
|
|
133
399
|
|
|
134
|
-
|
|
135
|
-
if (session.
|
|
136
|
-
return `Bearer ${session.lease}`;
|
|
137
|
-
}
|
|
400
|
+
async function renewControl(session, signal) {
|
|
401
|
+
if (session.refused) throw new KitRefusal(UPGRADE_REQUIRED);
|
|
138
402
|
Object.assign(
|
|
139
403
|
session,
|
|
140
404
|
heldLease(
|
|
@@ -148,10 +412,18 @@ export async function heldControl(session, signal = session.signal) {
|
|
|
148
412
|
),
|
|
149
413
|
),
|
|
150
414
|
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export async function heldControl(session, signal = session.signal) {
|
|
418
|
+
if (session.resident !== undefined) return RESIDENT;
|
|
419
|
+
if (session.refused) throw new KitRefusal(UPGRADE_REQUIRED);
|
|
420
|
+
if (session.expiresAt - session.now().getTime() <= LEASE_RENEWAL_MARGIN_MS) {
|
|
421
|
+
await renewControl(session, signal);
|
|
422
|
+
}
|
|
151
423
|
return `Bearer ${session.lease}`;
|
|
152
424
|
}
|
|
153
425
|
|
|
154
|
-
function controlSleep(milliseconds, signal) {
|
|
426
|
+
export function controlSleep(milliseconds, signal) {
|
|
155
427
|
return new Promise((resolve, reject) => {
|
|
156
428
|
if (signal.aborted) {
|
|
157
429
|
reject(new KitRefusal("kit_command_cancelled"));
|
|
@@ -195,9 +467,11 @@ export function mutatingCall(classification) {
|
|
|
195
467
|
}
|
|
196
468
|
|
|
197
469
|
export async function releaseControl(session, options = {}) {
|
|
470
|
+
session.resident?.end();
|
|
198
471
|
if (session.lease === null) return;
|
|
199
472
|
const held = session.lease;
|
|
200
473
|
try {
|
|
474
|
+
if (session.refused) throw new KitRefusal(UPGRADE_REQUIRED);
|
|
201
475
|
await houseRequest(
|
|
202
476
|
session.house,
|
|
203
477
|
`Bearer ${held}`,
|
|
@@ -229,6 +503,7 @@ export async function maintainControl(session, operation, signal, { release = tr
|
|
|
229
503
|
signal?.addEventListener("abort", cancelOperation, { once: true });
|
|
230
504
|
});
|
|
231
505
|
const renewal = (async () => {
|
|
506
|
+
if (session.resident !== undefined) return;
|
|
232
507
|
const sleep = session.dependencies.controlSleep ?? controlSleep;
|
|
233
508
|
while (!stopped) {
|
|
234
509
|
const delay = Math.max(
|
|
@@ -245,7 +520,7 @@ export async function maintainControl(session, operation, signal, { release = tr
|
|
|
245
520
|
throw new KitRefusal("kit_lease_rejected");
|
|
246
521
|
}
|
|
247
522
|
} catch (error) {
|
|
248
|
-
if (stopped) return;
|
|
523
|
+
if (stopped || session.refused) return;
|
|
249
524
|
rejectFailure(error);
|
|
250
525
|
maintenance.abort();
|
|
251
526
|
return;
|
|
@@ -253,20 +528,14 @@ export async function maintainControl(session, operation, signal, { release = tr
|
|
|
253
528
|
}
|
|
254
529
|
})();
|
|
255
530
|
const running = Promise.resolve().then(() => operation(operationSignal));
|
|
256
|
-
let ended = false;
|
|
257
531
|
try {
|
|
258
|
-
|
|
259
|
-
ended = true;
|
|
260
|
-
return answer;
|
|
261
|
-
} catch (error) {
|
|
262
|
-
ended = error instanceof KitRefusal && error.code === "kit_command_cancelled";
|
|
263
|
-
throw error;
|
|
532
|
+
return await Promise.race([running, failure, cancellation]);
|
|
264
533
|
} finally {
|
|
265
534
|
stopped = true;
|
|
266
535
|
maintenance.abort();
|
|
267
536
|
signal?.removeEventListener("abort", cancelOperation);
|
|
268
537
|
await Promise.all([renewal, running.catch(() => undefined)]);
|
|
269
|
-
if (
|
|
538
|
+
if (release) await releaseControl(session);
|
|
270
539
|
}
|
|
271
540
|
}
|
|
272
541
|
|
|
@@ -479,6 +748,7 @@ export async function reportLogin(session, login, outcome, signal = session.sign
|
|
|
479
748
|
}
|
|
480
749
|
|
|
481
750
|
const envelope = declarationReader("kit_acquire_rejected");
|
|
751
|
+
const input = declarationReader("kit_run_input_unreadable");
|
|
482
752
|
|
|
483
753
|
function acquiredRoute(value, declaration) {
|
|
484
754
|
const route = envelope.plain(value, declaration);
|
|
@@ -496,79 +766,82 @@ function acquiredRoute(value, declaration) {
|
|
|
496
766
|
|
|
497
767
|
function conversationText(value, declaration) {
|
|
498
768
|
if (value === null) return null;
|
|
499
|
-
if (typeof value !== "string" || value.length > 4096)
|
|
769
|
+
if (typeof value !== "string" || value.length > 4096) input.refuse(declaration);
|
|
500
770
|
return value;
|
|
501
771
|
}
|
|
502
772
|
|
|
503
773
|
function conversationMessage(value, declaration) {
|
|
504
|
-
const message =
|
|
774
|
+
const message = input.plain(value, declaration);
|
|
505
775
|
const text = conversationText(message.text, `${declaration}.text`);
|
|
506
776
|
const caption = conversationText(message.caption, `${declaration}.caption`);
|
|
507
|
-
if (!Array.isArray(message.files))
|
|
777
|
+
if (!Array.isArray(message.files)) input.refuse(`${declaration}.files`);
|
|
508
778
|
const files = message.files.map((value, index) => {
|
|
509
779
|
const name = `${declaration}.files[${index}]`;
|
|
510
|
-
const file =
|
|
780
|
+
const file = input.plain(value, name);
|
|
511
781
|
if (file.bytes !== null && (!Number.isSafeInteger(file.bytes) || file.bytes < 0)) {
|
|
512
|
-
|
|
782
|
+
input.refuse(`${name}.bytes`);
|
|
513
783
|
}
|
|
514
784
|
return {
|
|
515
|
-
path:
|
|
516
|
-
media_kind:
|
|
517
|
-
media_type: file.media_type === null ? null :
|
|
785
|
+
path: input.text(file.path, `${name}.path`, 2048),
|
|
786
|
+
media_kind: input.text(file.media_kind, `${name}.media_kind`, 128),
|
|
787
|
+
media_type: file.media_type === null ? null : input.text(file.media_type, `${name}.media_type`, 255),
|
|
518
788
|
bytes: file.bytes,
|
|
519
789
|
};
|
|
520
790
|
});
|
|
521
|
-
if (!text && !caption && files.length === 0)
|
|
791
|
+
if (!text && !caption && files.length === 0) input.refuse(declaration);
|
|
522
792
|
return {
|
|
523
|
-
message_id:
|
|
793
|
+
message_id: input.text(message.message_id, `${declaration}.message_id`, 128),
|
|
524
794
|
text, caption, files,
|
|
525
795
|
};
|
|
526
796
|
}
|
|
527
797
|
|
|
528
|
-
function
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
const turn =
|
|
798
|
+
function runRequest(answer) {
|
|
799
|
+
const request = input.plain(answer, "request");
|
|
800
|
+
if (request.kind === "conversation") {
|
|
801
|
+
if (request.origin !== "user") input.refuse("request.origin");
|
|
802
|
+
const turn = input.plain(request.turn, "request.turn");
|
|
533
803
|
return {
|
|
804
|
+
kind: "conversation",
|
|
534
805
|
origin: "user",
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
.map((message, index) => conversationMessage(message, `${declaration}.conversation.turn.messages[${index}]`)),
|
|
541
|
-
},
|
|
806
|
+
turn: {
|
|
807
|
+
id: input.text(turn.id, "request.turn.id", 128),
|
|
808
|
+
sequence: input.positiveCount(turn.sequence, "request.turn.sequence"),
|
|
809
|
+
messages: input.filled(turn.messages, "request.turn.messages")
|
|
810
|
+
.map((message, index) => conversationMessage(message, `request.turn.messages[${index}]`)),
|
|
542
811
|
},
|
|
543
812
|
};
|
|
544
813
|
}
|
|
545
|
-
if (
|
|
546
|
-
const
|
|
547
|
-
|
|
814
|
+
if (request.kind !== "routine") input.refuse("request.kind");
|
|
815
|
+
const routine = input.plain(request.routine, "request.routine");
|
|
816
|
+
if (typeof request.payload !== "string" || Array.from(request.payload).length > 20_000) input.refuse("request.payload");
|
|
817
|
+
if (!Array.isArray(request.references)) input.refuse("request.references");
|
|
548
818
|
return {
|
|
549
|
-
|
|
819
|
+
kind: "routine",
|
|
820
|
+
origin: input.text(request.origin, "request.origin", 32),
|
|
550
821
|
routine: {
|
|
551
|
-
name:
|
|
552
|
-
version:
|
|
822
|
+
name: input.text(routine.name, "request.routine.name", 256),
|
|
823
|
+
version: input.positiveCount(routine.version, "request.routine.version"),
|
|
553
824
|
},
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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),
|
|
825
|
+
instruction: input.text(request.instruction, "request.instruction", 256 * 1024),
|
|
826
|
+
payload: request.payload,
|
|
827
|
+
references: request.references.map((reference, index) => input.plain(reference, `request.references[${index}]`)),
|
|
828
|
+
requested_by: input.text(request.requested_by, "request.requested_by", 128),
|
|
829
|
+
requested_at: input.text(request.requested_at, "request.requested_at", 64),
|
|
830
|
+
deliver_at: request.deliver_at === null ? null : input.text(request.deliver_at, "request.deliver_at", 64),
|
|
569
831
|
};
|
|
570
832
|
}
|
|
571
833
|
|
|
834
|
+
export async function readRunRequest(session, run, token, signal = session.signal) {
|
|
835
|
+
return runRequest(await houseRequest(
|
|
836
|
+
session.house,
|
|
837
|
+
`Bearer ${token}`,
|
|
838
|
+
runRequestPath(run),
|
|
839
|
+
{},
|
|
840
|
+
session.dependencies,
|
|
841
|
+
signal,
|
|
842
|
+
));
|
|
843
|
+
}
|
|
844
|
+
|
|
572
845
|
function acquiredAssignment(value, declaration) {
|
|
573
846
|
const acquired = envelope.plain(value, declaration);
|
|
574
847
|
const bridge = envelope.plain(acquired.session, `${declaration}.session`);
|
|
@@ -597,7 +870,6 @@ function acquiredAssignment(value, declaration) {
|
|
|
597
870
|
token: envelope.text(bridge.bridge_token, `${declaration}.session.bridge_token`, 512),
|
|
598
871
|
expiresAt: Date.parse(expiresAt),
|
|
599
872
|
},
|
|
600
|
-
request: acquiredRequest(acquired, declaration),
|
|
601
873
|
};
|
|
602
874
|
}
|
|
603
875
|
|
package/src/inbound.js
CHANGED
|
@@ -134,13 +134,9 @@ export async function reconcileInbound(directory, session) {
|
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
export async function localRequest(directory, run, request) {
|
|
137
|
-
|
|
138
|
-
if (turn === null || turn === undefined) return request;
|
|
137
|
+
if (request.kind !== "conversation") return request;
|
|
139
138
|
const result = structuredClone(request);
|
|
140
|
-
const
|
|
141
|
-
if (!Array.isArray(localTurn.messages)) throw new KitRefusal("kit_run_input_unreadable");
|
|
142
|
-
for (const message of localTurn.messages) {
|
|
143
|
-
if (!Array.isArray(message.files)) throw new KitRefusal("kit_run_input_unreadable");
|
|
139
|
+
for (const message of result.turn.messages) {
|
|
144
140
|
for (const file of message.files) {
|
|
145
141
|
const parts = typeof file.path === "string" ? file.path.split("/") : [];
|
|
146
142
|
if (parts.length !== 4 || parts[0] !== ROOT || parts[1] !== run || !UUID.test(parts[1]) || !UUID.test(parts[2]) || !segment(parts[3])) {
|
|
@@ -161,15 +157,3 @@ export async function localRequest(directory, run, request) {
|
|
|
161
157
|
}
|
|
162
158
|
return result;
|
|
163
159
|
}
|
|
164
|
-
|
|
165
|
-
export async function localReply(directory, run, frame, reply) {
|
|
166
|
-
if (frame.method !== "tools/call" || frame.params?.name !== "session_get_request" || reply.error || reply.result?.isError) return reply;
|
|
167
|
-
const result = structuredClone(reply);
|
|
168
|
-
for (const item of result.result?.content ?? []) {
|
|
169
|
-
if (item.type !== "text") continue;
|
|
170
|
-
let input;
|
|
171
|
-
try { input = JSON.parse(item.text); } catch { throw new KitRefusal("kit_run_input_unreadable"); }
|
|
172
|
-
item.text = JSON.stringify(await localRequest(directory, run, input));
|
|
173
|
-
}
|
|
174
|
-
return result;
|
|
175
|
-
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2
|
+
import WebSocket from "ws";
|
|
3
|
+
import { KIT_CONTRACT_VERSION } from "./contract.js";
|
|
4
|
+
import { heldControl } from "./house.js";
|
|
5
|
+
import { KitRefusal } from "./refusal.js";
|
|
6
|
+
|
|
7
|
+
const PROTOCOL = "house.kit.stream.1";
|
|
8
|
+
const WATCHED_SUBJECTS = ["agents", "working_copy"];
|
|
9
|
+
|
|
10
|
+
function streamAddress(house) {
|
|
11
|
+
const url = new URL("/kit/stream", house);
|
|
12
|
+
if (url.protocol === "https:") url.protocol = "wss:";
|
|
13
|
+
else if (url.protocol === "http:") url.protocol = "ws:";
|
|
14
|
+
else throw new KitRefusal("kit_house_unavailable", "/kit/stream");
|
|
15
|
+
return url;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function connected(session, signal, available, dependencies) {
|
|
19
|
+
const authorization = await heldControl(session, signal);
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
const Socket = dependencies.workSocket ?? WebSocket;
|
|
22
|
+
const socket = new Socket(streamAddress(session.house), PROTOCOL, {
|
|
23
|
+
headers: { authorization, "x-house-kit-version": KIT_CONTRACT_VERSION },
|
|
24
|
+
handshakeTimeout: 10_000,
|
|
25
|
+
maxPayload: 4096,
|
|
26
|
+
perMessageDeflate: false,
|
|
27
|
+
});
|
|
28
|
+
let finished = false;
|
|
29
|
+
const finish = (error = null) => {
|
|
30
|
+
if (finished) return;
|
|
31
|
+
finished = true;
|
|
32
|
+
signal.removeEventListener("abort", stop);
|
|
33
|
+
if (error === null) resolve();
|
|
34
|
+
else reject(error);
|
|
35
|
+
};
|
|
36
|
+
const stop = () => {
|
|
37
|
+
socket.terminate();
|
|
38
|
+
finish();
|
|
39
|
+
};
|
|
40
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
41
|
+
socket.on("open", () => available(null));
|
|
42
|
+
socket.on("message", (data) => {
|
|
43
|
+
let frame;
|
|
44
|
+
try {
|
|
45
|
+
frame = JSON.parse(data.toString());
|
|
46
|
+
} catch {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (frame?.type === "work_available" && WATCHED_SUBJECTS.includes(frame.subject)) {
|
|
50
|
+
available(frame.subject);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
socket.on("unexpected-response", (_request, response) => {
|
|
54
|
+
response.resume();
|
|
55
|
+
if (response.statusCode === 409) session.dependencies.contractRefused?.();
|
|
56
|
+
const refusal =
|
|
57
|
+
response.statusCode === 409 ? new KitRefusal("kit_upgrade_required") : null;
|
|
58
|
+
socket.terminate();
|
|
59
|
+
finish(refusal);
|
|
60
|
+
});
|
|
61
|
+
socket.on("error", () => {
|
|
62
|
+
socket.terminate();
|
|
63
|
+
finish();
|
|
64
|
+
});
|
|
65
|
+
socket.on("close", () => finish());
|
|
66
|
+
if (signal.aborted) stop();
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function watchKitWork(session, signal, available, dependencies = {}) {
|
|
71
|
+
let retryMs = 500;
|
|
72
|
+
while (!signal.aborted) {
|
|
73
|
+
const began = Date.now();
|
|
74
|
+
try {
|
|
75
|
+
await connected(session, signal, available, dependencies);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error instanceof KitRefusal && error.code === "kit_upgrade_required") throw error;
|
|
78
|
+
if (signal.aborted) return;
|
|
79
|
+
}
|
|
80
|
+
if (signal.aborted) return;
|
|
81
|
+
retryMs = Date.now() - began >= 30_000 ? 1000 : Math.min(retryMs * 2, 30_000);
|
|
82
|
+
try {
|
|
83
|
+
await delay(dependencies.workReconnectMs ?? retryMs, undefined, { signal });
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (!signal.aborted) throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|