@agentshouse/kit 0.1.0-alpha.1 → 0.1.0-alpha.11
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 +89 -10
- package/bin/connect-linux.sh +58 -12
- package/bin/kit.js +1 -1
- package/bootstrap.json +17 -0
- package/package.json +6 -2
- package/src/agents.js +63 -8
- package/src/contract.js +3 -3
- package/src/host.js +6 -2
- package/src/house.js +199 -15
- package/src/kit-work-stream.js +84 -0
- package/src/mount.js +7 -7
- package/src/setup.js +7 -2
- package/src/source.js +44 -30
- package/src/sync-state.js +4 -0
- package/src/sync.js +11 -6
- package/src/telegram.js +24 -8
- package/src/worker.js +197 -25
package/src/house.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import {
|
|
1
|
+
import { chmod, mkdir, readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { connect, createServer } from "node:net";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
3
4
|
import { assertAcquiredCommandAdapter } from "./adapter.js";
|
|
5
|
+
import { readFrames, writeFrame } from "./channel.js";
|
|
4
6
|
import {
|
|
5
7
|
assistantDraftsPath,
|
|
6
8
|
assistantTextPath,
|
|
@@ -35,6 +37,8 @@ import { declarationReader } from "./declaration.js";
|
|
|
35
37
|
import { houseRefusal, KitRefusal, throwHouseAnswer } from "./refusal.js";
|
|
36
38
|
|
|
37
39
|
const LEASE = /^ahl_[A-Za-z0-9_-]+$/;
|
|
40
|
+
const RESIDENT = "Resident";
|
|
41
|
+
const LEASE_ACTIVE = "environment_control_lease_active";
|
|
38
42
|
const ASSISTANT_DRAFT_STATES = ["streamed", "unsupported"];
|
|
39
43
|
const ITEM_OUTCOMES = ["accepted", "duplicate", "rejected"];
|
|
40
44
|
|
|
@@ -105,6 +109,172 @@ function heldLease(answer) {
|
|
|
105
109
|
return { lease: answer.lease, expiresAt: Date.parse(answer.expires_at) };
|
|
106
110
|
}
|
|
107
111
|
|
|
112
|
+
function residentSocket(directory) {
|
|
113
|
+
return join(directory, "run", "resident.sock");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function residentAnswer(status, code) {
|
|
117
|
+
const body = Buffer.from(JSON.stringify({ error: { code } })).toString("base64");
|
|
118
|
+
return { status, type: "application/json", body };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function residentRelay(session, request, signal) {
|
|
122
|
+
const path = typeof request.path === "string" ? request.path : "";
|
|
123
|
+
if (!path.startsWith("/kit/") || path.startsWith("/kit/lease")) {
|
|
124
|
+
return residentAnswer(403, "kit_resident_route_refused");
|
|
125
|
+
}
|
|
126
|
+
let authorization;
|
|
127
|
+
try {
|
|
128
|
+
authorization = await heldControl(session, signal);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
return residentAnswer(error.status ?? 401, error.code ?? "kit_lease_rejected");
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const response = await (session.dependencies.fetch ?? fetch)(`${session.house}${path}`, {
|
|
134
|
+
method: request.method,
|
|
135
|
+
headers: { ...request.headers, authorization },
|
|
136
|
+
body: request.text ?? (request.bytes === undefined ? undefined : Buffer.from(request.bytes, "base64")),
|
|
137
|
+
signal,
|
|
138
|
+
});
|
|
139
|
+
const body = Buffer.from(await response.arrayBuffer());
|
|
140
|
+
return {
|
|
141
|
+
status: response.status,
|
|
142
|
+
type: response.headers.get("content-type"),
|
|
143
|
+
body: body.length === 0 ? null : body.toString("base64"),
|
|
144
|
+
};
|
|
145
|
+
} catch {
|
|
146
|
+
return residentAnswer(503, "kit_house_unavailable");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function serveResident(session, directory, signal) {
|
|
151
|
+
const socket = residentSocket(directory);
|
|
152
|
+
await mkdir(dirname(socket), { recursive: true, mode: 0o700 });
|
|
153
|
+
await rm(socket, { force: true });
|
|
154
|
+
let holder = null;
|
|
155
|
+
const server = createServer((connection) => {
|
|
156
|
+
connection.on("error", () => connection.destroy());
|
|
157
|
+
if (holder !== null) {
|
|
158
|
+
writeFrame(connection, { refused: LEASE_ACTIVE });
|
|
159
|
+
connection.end();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
holder = connection;
|
|
163
|
+
const relayed = new Map();
|
|
164
|
+
connection.on("close", () => {
|
|
165
|
+
if (holder === connection) holder = null;
|
|
166
|
+
for (const cancelled of relayed.values()) cancelled.abort();
|
|
167
|
+
});
|
|
168
|
+
renewControl(session, signal).then(
|
|
169
|
+
() => {
|
|
170
|
+
writeFrame(connection, { held: true });
|
|
171
|
+
readFrames(connection, {
|
|
172
|
+
onFrame: (request) => {
|
|
173
|
+
if (request.cancel === true) {
|
|
174
|
+
relayed.get(request.id)?.abort();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const cancelled = new AbortController();
|
|
178
|
+
relayed.set(request.id, cancelled);
|
|
179
|
+
void residentRelay(session, request, AbortSignal.any([signal, cancelled.signal])).then(
|
|
180
|
+
(answer) => {
|
|
181
|
+
relayed.delete(request.id);
|
|
182
|
+
if (!connection.destroyed) writeFrame(connection, { id: request.id, ...answer });
|
|
183
|
+
},
|
|
184
|
+
);
|
|
185
|
+
},
|
|
186
|
+
onRefusal: () => connection.destroy(),
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
(error) => {
|
|
190
|
+
writeFrame(connection, { refused: error.code ?? "kit_lease_rejected" });
|
|
191
|
+
connection.end();
|
|
192
|
+
},
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
await new Promise((resolve, reject) => {
|
|
196
|
+
server.once("error", reject);
|
|
197
|
+
server.listen(socket, resolve);
|
|
198
|
+
});
|
|
199
|
+
await chmod(socket, 0o600);
|
|
200
|
+
return async () => {
|
|
201
|
+
holder?.destroy();
|
|
202
|
+
await new Promise((resolve) => server.close(resolve));
|
|
203
|
+
await rm(socket, { force: true });
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function residentDelegation(directory, fetcher) {
|
|
208
|
+
const connection = connect(residentSocket(directory));
|
|
209
|
+
const pending = new Map();
|
|
210
|
+
let greet;
|
|
211
|
+
const greeting = new Promise((resolve) => {
|
|
212
|
+
greet = resolve;
|
|
213
|
+
});
|
|
214
|
+
const unavailable = () => residentAnswer(503, "kit_resident_unavailable");
|
|
215
|
+
function settle(id, answer) {
|
|
216
|
+
pending.get(id)?.(answer);
|
|
217
|
+
pending.delete(id);
|
|
218
|
+
if (pending.size === 0) connection.unref();
|
|
219
|
+
}
|
|
220
|
+
connection.on("error", () => connection.destroy());
|
|
221
|
+
connection.on("close", () => {
|
|
222
|
+
greet(null);
|
|
223
|
+
for (const id of [...pending.keys()]) settle(id, unavailable());
|
|
224
|
+
});
|
|
225
|
+
readFrames(connection, {
|
|
226
|
+
onFrame: (frame) => (frame.id === undefined ? greet(frame) : settle(frame.id, frame)),
|
|
227
|
+
onRefusal: () => connection.destroy(),
|
|
228
|
+
});
|
|
229
|
+
const held = await greeting;
|
|
230
|
+
if (held === null) return null;
|
|
231
|
+
if (held.held !== true) {
|
|
232
|
+
connection.destroy();
|
|
233
|
+
throw new KitRefusal(held.refused);
|
|
234
|
+
}
|
|
235
|
+
connection.unref();
|
|
236
|
+
let next = 0;
|
|
237
|
+
async function delegated(url, init = {}) {
|
|
238
|
+
const { authorization, ...headers } = init.headers ?? {};
|
|
239
|
+
if (authorization !== RESIDENT) return fetcher(url, init);
|
|
240
|
+
init.signal?.throwIfAborted();
|
|
241
|
+
const address = new URL(url);
|
|
242
|
+
const answer = connection.destroyed
|
|
243
|
+
? unavailable()
|
|
244
|
+
: await new Promise((resolve, reject) => {
|
|
245
|
+
const id = (next += 1);
|
|
246
|
+
const cancel = () => {
|
|
247
|
+
pending.delete(id);
|
|
248
|
+
settle(id);
|
|
249
|
+
if (!connection.destroyed) writeFrame(connection, { id, cancel: true });
|
|
250
|
+
reject(init.signal.reason);
|
|
251
|
+
};
|
|
252
|
+
init.signal?.addEventListener("abort", cancel, { once: true });
|
|
253
|
+
pending.set(id, (answer) => {
|
|
254
|
+
init.signal?.removeEventListener("abort", cancel);
|
|
255
|
+
resolve(answer);
|
|
256
|
+
});
|
|
257
|
+
connection.ref();
|
|
258
|
+
writeFrame(connection, {
|
|
259
|
+
id,
|
|
260
|
+
method: init.method ?? "GET",
|
|
261
|
+
path: `${address.pathname}${address.search}`,
|
|
262
|
+
headers,
|
|
263
|
+
...(typeof init.body === "string"
|
|
264
|
+
? { text: init.body }
|
|
265
|
+
: init.body === undefined
|
|
266
|
+
? {}
|
|
267
|
+
: { bytes: Buffer.from(init.body).toString("base64") }),
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
return new Response(answer.body === null ? null : Buffer.from(answer.body, "base64"), {
|
|
271
|
+
status: answer.status,
|
|
272
|
+
headers: answer.type === null ? {} : { "content-type": answer.type },
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
return { connection, fetch: delegated };
|
|
276
|
+
}
|
|
277
|
+
|
|
108
278
|
export async function openSession(options, dependencies = {}) {
|
|
109
279
|
const session = {
|
|
110
280
|
house: options.house,
|
|
@@ -114,6 +284,15 @@ export async function openSession(options, dependencies = {}) {
|
|
|
114
284
|
expiresAt: 0,
|
|
115
285
|
signal: options.signal,
|
|
116
286
|
};
|
|
287
|
+
const resident =
|
|
288
|
+
options.acquire === true
|
|
289
|
+
? null
|
|
290
|
+
: await residentDelegation(options.directory, dependencies.fetch ?? fetch);
|
|
291
|
+
if (resident !== null) {
|
|
292
|
+
session.resident = resident.connection;
|
|
293
|
+
session.dependencies = { ...dependencies, fetch: resident.fetch };
|
|
294
|
+
return session;
|
|
295
|
+
}
|
|
117
296
|
const credential = await readCredential(options.directory);
|
|
118
297
|
Object.assign(
|
|
119
298
|
session,
|
|
@@ -131,10 +310,7 @@ export async function openSession(options, dependencies = {}) {
|
|
|
131
310
|
return session;
|
|
132
311
|
}
|
|
133
312
|
|
|
134
|
-
|
|
135
|
-
if (session.expiresAt - session.now().getTime() > LEASE_RENEWAL_MARGIN_MS) {
|
|
136
|
-
return `Bearer ${session.lease}`;
|
|
137
|
-
}
|
|
313
|
+
async function renewControl(session, signal) {
|
|
138
314
|
Object.assign(
|
|
139
315
|
session,
|
|
140
316
|
heldLease(
|
|
@@ -148,6 +324,13 @@ export async function heldControl(session, signal = session.signal) {
|
|
|
148
324
|
),
|
|
149
325
|
),
|
|
150
326
|
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export async function heldControl(session, signal = session.signal) {
|
|
330
|
+
if (session.resident !== undefined) return RESIDENT;
|
|
331
|
+
if (session.expiresAt - session.now().getTime() <= LEASE_RENEWAL_MARGIN_MS) {
|
|
332
|
+
await renewControl(session, signal);
|
|
333
|
+
}
|
|
151
334
|
return `Bearer ${session.lease}`;
|
|
152
335
|
}
|
|
153
336
|
|
|
@@ -195,6 +378,7 @@ export function mutatingCall(classification) {
|
|
|
195
378
|
}
|
|
196
379
|
|
|
197
380
|
export async function releaseControl(session, options = {}) {
|
|
381
|
+
session.resident?.end();
|
|
198
382
|
if (session.lease === null) return;
|
|
199
383
|
const held = session.lease;
|
|
200
384
|
try {
|
|
@@ -229,6 +413,7 @@ export async function maintainControl(session, operation, signal, { release = tr
|
|
|
229
413
|
signal?.addEventListener("abort", cancelOperation, { once: true });
|
|
230
414
|
});
|
|
231
415
|
const renewal = (async () => {
|
|
416
|
+
if (session.resident !== undefined) return;
|
|
232
417
|
const sleep = session.dependencies.controlSleep ?? controlSleep;
|
|
233
418
|
while (!stopped) {
|
|
234
419
|
const delay = Math.max(
|
|
@@ -253,20 +438,14 @@ export async function maintainControl(session, operation, signal, { release = tr
|
|
|
253
438
|
}
|
|
254
439
|
})();
|
|
255
440
|
const running = Promise.resolve().then(() => operation(operationSignal));
|
|
256
|
-
let ended = false;
|
|
257
441
|
try {
|
|
258
|
-
|
|
259
|
-
ended = true;
|
|
260
|
-
return answer;
|
|
261
|
-
} catch (error) {
|
|
262
|
-
ended = error instanceof KitRefusal && error.code === "kit_command_cancelled";
|
|
263
|
-
throw error;
|
|
442
|
+
return await Promise.race([running, failure, cancellation]);
|
|
264
443
|
} finally {
|
|
265
444
|
stopped = true;
|
|
266
445
|
maintenance.abort();
|
|
267
446
|
signal?.removeEventListener("abort", cancelOperation);
|
|
268
447
|
await Promise.all([renewal, running.catch(() => undefined)]);
|
|
269
|
-
if (
|
|
448
|
+
if (release) await releaseControl(session);
|
|
270
449
|
}
|
|
271
450
|
}
|
|
272
451
|
|
|
@@ -480,6 +659,11 @@ export async function reportLogin(session, login, outcome, signal = session.sign
|
|
|
480
659
|
|
|
481
660
|
const envelope = declarationReader("kit_acquire_rejected");
|
|
482
661
|
|
|
662
|
+
function routineInput(value, declaration) {
|
|
663
|
+
if (typeof value !== "string" || Array.from(value).length > 20_000) envelope.refuse(declaration);
|
|
664
|
+
return value;
|
|
665
|
+
}
|
|
666
|
+
|
|
483
667
|
function acquiredRoute(value, declaration) {
|
|
484
668
|
const route = envelope.plain(value, declaration);
|
|
485
669
|
if (!Array.isArray(route.arguments)) envelope.refuse(`${declaration}.arguments`);
|
|
@@ -551,7 +735,7 @@ function acquiredRequest(acquired, declaration) {
|
|
|
551
735
|
name: envelope.text(routine.name, `${declaration}.routine.name`, 256),
|
|
552
736
|
version: envelope.positiveCount(routine.version, `${declaration}.routine.version`),
|
|
553
737
|
},
|
|
554
|
-
payload:
|
|
738
|
+
payload: routineInput(request.payload, `${declaration}.request.payload`),
|
|
555
739
|
requested_by: envelope.text(
|
|
556
740
|
request.requested_by,
|
|
557
741
|
`${declaration}.request.requested_by`,
|
|
@@ -0,0 +1,84 @@
|
|
|
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
|
+
|
|
9
|
+
function streamAddress(house) {
|
|
10
|
+
const url = new URL("/kit/stream", house);
|
|
11
|
+
if (url.protocol === "https:") url.protocol = "wss:";
|
|
12
|
+
else if (url.protocol === "http:") url.protocol = "ws:";
|
|
13
|
+
else throw new KitRefusal("kit_house_unavailable", "/kit/stream");
|
|
14
|
+
return url;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function connected(session, signal, available, dependencies) {
|
|
18
|
+
const authorization = await heldControl(session, signal);
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const Socket = dependencies.workSocket ?? WebSocket;
|
|
21
|
+
const socket = new Socket(streamAddress(session.house), PROTOCOL, {
|
|
22
|
+
headers: { authorization, "x-house-kit-version": KIT_CONTRACT_VERSION },
|
|
23
|
+
handshakeTimeout: 10_000,
|
|
24
|
+
maxPayload: 4096,
|
|
25
|
+
perMessageDeflate: false,
|
|
26
|
+
});
|
|
27
|
+
let finished = false;
|
|
28
|
+
const finish = (error = null) => {
|
|
29
|
+
if (finished) return;
|
|
30
|
+
finished = true;
|
|
31
|
+
signal.removeEventListener("abort", stop);
|
|
32
|
+
if (error === null) resolve();
|
|
33
|
+
else reject(error);
|
|
34
|
+
};
|
|
35
|
+
const stop = () => {
|
|
36
|
+
socket.terminate();
|
|
37
|
+
finish();
|
|
38
|
+
};
|
|
39
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
40
|
+
socket.on("open", available);
|
|
41
|
+
socket.on("message", (data) => {
|
|
42
|
+
let frame;
|
|
43
|
+
try {
|
|
44
|
+
frame = JSON.parse(data.toString());
|
|
45
|
+
} catch {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (frame?.type === "work_available" && frame.subject === "agents") available();
|
|
49
|
+
});
|
|
50
|
+
socket.on("unexpected-response", (_request, response) => {
|
|
51
|
+
response.resume();
|
|
52
|
+
const refusal =
|
|
53
|
+
response.statusCode === 409 ? new KitRefusal("kit_upgrade_required") : null;
|
|
54
|
+
socket.terminate();
|
|
55
|
+
finish(refusal);
|
|
56
|
+
});
|
|
57
|
+
socket.on("error", () => {
|
|
58
|
+
socket.terminate();
|
|
59
|
+
finish();
|
|
60
|
+
});
|
|
61
|
+
socket.on("close", () => finish());
|
|
62
|
+
if (signal.aborted) stop();
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function watchKitAgentWork(session, signal, available, dependencies = {}) {
|
|
67
|
+
let retryMs = 500;
|
|
68
|
+
while (!signal.aborted) {
|
|
69
|
+
const began = Date.now();
|
|
70
|
+
try {
|
|
71
|
+
await connected(session, signal, available, dependencies);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (error instanceof KitRefusal && error.code === "kit_upgrade_required") throw error;
|
|
74
|
+
if (signal.aborted) return;
|
|
75
|
+
}
|
|
76
|
+
if (signal.aborted) return;
|
|
77
|
+
retryMs = Date.now() - began >= 30_000 ? 1000 : Math.min(retryMs * 2, 30_000);
|
|
78
|
+
try {
|
|
79
|
+
await delay(dependencies.workReconnectMs ?? retryMs, undefined, { signal });
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (!signal.aborted) throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
package/src/mount.js
CHANGED
|
@@ -84,7 +84,6 @@ export function mountTarget(subtree) {
|
|
|
84
84
|
refuse("kit_mount_subtree_invalid", subtree);
|
|
85
85
|
}
|
|
86
86
|
return {
|
|
87
|
-
kind: own ? "private" : "room",
|
|
88
87
|
handle: own ? null : segments[1],
|
|
89
88
|
root: own ? "/private" : `/rooms/${segments[1]}`,
|
|
90
89
|
subtree,
|
|
@@ -107,10 +106,11 @@ function discoveryOf(value) {
|
|
|
107
106
|
|
|
108
107
|
function authorityFor(discovery, target) {
|
|
109
108
|
const found = discovery.authorities.find((entry) =>
|
|
110
|
-
|
|
109
|
+
entry.kind === "room" && entry.room_handle === target.handle,
|
|
111
110
|
);
|
|
112
111
|
if (found === undefined) refuse("kit_mount_authority_unknown", target.root);
|
|
113
112
|
if (
|
|
113
|
+
typeof found.room_ref !== "string" ||
|
|
114
114
|
!Array.isArray(found.protected_paths) ||
|
|
115
115
|
found.operations === null ||
|
|
116
116
|
typeof found.operations !== "object" ||
|
|
@@ -119,8 +119,8 @@ function authorityFor(discovery, target) {
|
|
|
119
119
|
refuse("kit_mount_rejected", "discover");
|
|
120
120
|
}
|
|
121
121
|
return {
|
|
122
|
-
kind:
|
|
123
|
-
authority: found.
|
|
122
|
+
kind: "room",
|
|
123
|
+
authority: found.room_ref,
|
|
124
124
|
path: target.root,
|
|
125
125
|
protected_paths: found.protected_paths,
|
|
126
126
|
operations: found.operations,
|
|
@@ -326,7 +326,7 @@ function pendingTransfer(held, save) {
|
|
|
326
326
|
};
|
|
327
327
|
}
|
|
328
328
|
|
|
329
|
-
async function submitAttachments(session, held,
|
|
329
|
+
async function submitAttachments(session, held, authority, plan, signal) {
|
|
330
330
|
const transfers = [];
|
|
331
331
|
for (const save of plan.saves) {
|
|
332
332
|
const local = held.attachments.get(save.path);
|
|
@@ -341,7 +341,7 @@ async function submitAttachments(session, held, target, authority, plan, signal)
|
|
|
341
341
|
version: save.version,
|
|
342
342
|
name: declared.name,
|
|
343
343
|
mediaType: declared.mediaType ?? undefined,
|
|
344
|
-
room:
|
|
344
|
+
room: authority.authority,
|
|
345
345
|
body,
|
|
346
346
|
signal,
|
|
347
347
|
});
|
|
@@ -471,7 +471,7 @@ export async function mountPackage(held, options, dependencies = {}) {
|
|
|
471
471
|
};
|
|
472
472
|
const localReport = join(held.root, "MIGRATION.json");
|
|
473
473
|
await writeFile(localReport, `${JSON.stringify(result, null, 2)}\n`);
|
|
474
|
-
result.transfers = await submitAttachments(session, held,
|
|
474
|
+
result.transfers = await submitAttachments(session, held, authority, plan, signal);
|
|
475
475
|
|
|
476
476
|
const report = `${target.subtree}/${MOUNT_REPORT_NAME}`;
|
|
477
477
|
await writeFile(localReport, `${JSON.stringify(result, null, 2)}\n`);
|
package/src/setup.js
CHANGED
|
@@ -114,14 +114,14 @@ async function signedIn(module) {
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
async function setUpModule(context, module) {
|
|
117
|
+
async function setUpModule(context, module, install = true) {
|
|
118
118
|
const summary = (status, reason = null) => ({
|
|
119
119
|
category: module.category,
|
|
120
120
|
name: module.name,
|
|
121
121
|
status,
|
|
122
122
|
reason,
|
|
123
123
|
});
|
|
124
|
-
if (module.install !== null) {
|
|
124
|
+
if (install && module.install !== null) {
|
|
125
125
|
try {
|
|
126
126
|
await module.install();
|
|
127
127
|
} catch (error) {
|
|
@@ -144,10 +144,15 @@ async function setUpModule(context, module) {
|
|
|
144
144
|
return summary("ready");
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
export function signInWantedAgent(context, kind) {
|
|
148
|
+
return setUpModule(context, agentModule(context, kind), false);
|
|
149
|
+
}
|
|
150
|
+
|
|
147
151
|
export async function runSetup(context, request) {
|
|
148
152
|
await prepareLaunchBase(launchBase(context.dependencies));
|
|
149
153
|
const modules = [];
|
|
150
154
|
for (const kind of context.configuration.agents) {
|
|
155
|
+
await context.waitAgentLogin?.(kind);
|
|
151
156
|
modules.push(await setUpModule(context, agentModule(context, kind)));
|
|
152
157
|
}
|
|
153
158
|
for (const instance of context.configuration.instances) {
|
package/src/source.js
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
newCheckpoint,
|
|
13
13
|
stallOf,
|
|
14
14
|
} from "./delivery.js";
|
|
15
|
-
import { captureBatch, openSession } from "./house.js";
|
|
15
|
+
import { captureBatch, maintainControl, openSession } from "./house.js";
|
|
16
16
|
import { googlePage } from "./google/source.js";
|
|
17
17
|
import { houseAnswered, houseAnsweredFailure, houseFailed, KitRefusal } from "./refusal.js";
|
|
18
18
|
import { recordAcceptedOriginals, sameReference, saveSourceOriginals } from "./source-originals.js";
|
|
@@ -204,18 +204,24 @@ export async function captureSource(options, dependencies = {}) {
|
|
|
204
204
|
const googleOAuthClient =
|
|
205
205
|
instance.preset === "google" ? dependencies.googleOAuthClient() : null;
|
|
206
206
|
const session = await openSession(options, dependencies);
|
|
207
|
-
|
|
208
|
-
instance,
|
|
207
|
+
return maintainControl(
|
|
209
208
|
session,
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
209
|
+
async (signal) => {
|
|
210
|
+
const context = {
|
|
211
|
+
instance,
|
|
212
|
+
session,
|
|
213
|
+
directory: options.directory,
|
|
214
|
+
signal,
|
|
215
|
+
now: session.now,
|
|
216
|
+
dependencies,
|
|
217
|
+
googleOAuthClient,
|
|
218
|
+
};
|
|
219
|
+
const delivered = [];
|
|
220
|
+
for (const service of services) delivered.push(await deliverService(context, service));
|
|
221
|
+
return { instance: instance.id, services: delivered };
|
|
222
|
+
},
|
|
223
|
+
options.signal,
|
|
224
|
+
);
|
|
219
225
|
}
|
|
220
226
|
|
|
221
227
|
export async function sourceStatus(options) {
|
|
@@ -255,32 +261,40 @@ export async function retrySourceOriginals(options, dependencies = {}) {
|
|
|
255
261
|
const configuration = await readConfiguration(options.configuration);
|
|
256
262
|
const instance = sourceInstance(configuration, options.instance);
|
|
257
263
|
const services = declaredServices(instance, options.service ?? null);
|
|
258
|
-
const context = { instance, directory: options.directory, signal: options.signal };
|
|
259
264
|
const outstanding = [];
|
|
260
265
|
for (const service of services) {
|
|
261
266
|
outstanding.push((await readSourceOriginals(options.directory, instance.id, service.service)).length);
|
|
262
267
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
268
|
+
const retry = async (context) => {
|
|
269
|
+
const retried = [];
|
|
270
|
+
for (const [position, service] of services.entries()) {
|
|
271
|
+
retried.push({
|
|
272
|
+
service: service.service,
|
|
273
|
+
originals:
|
|
274
|
+
outstanding[position] === 0
|
|
275
|
+
? []
|
|
276
|
+
: await saveSourceOriginals(context, serviceBinding(context, service)),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
return { instance: instance.id, services: retried };
|
|
280
|
+
};
|
|
281
|
+
const context = { instance, directory: options.directory, signal: options.signal };
|
|
282
|
+
if (outstanding.every((count) => count === 0)) return retry(context);
|
|
283
|
+
const session = await openSession(options, dependencies);
|
|
284
|
+
return maintainControl(session, (signal) => retry({ ...context, session, signal }), options.signal);
|
|
275
285
|
}
|
|
276
286
|
|
|
277
287
|
export async function resetSource(options, dependencies = {}) {
|
|
278
288
|
const configuration = await readConfiguration(options.configuration);
|
|
279
289
|
const instance = sourceInstance(configuration, options.instance);
|
|
280
290
|
declaredServices(instance, options.service);
|
|
281
|
-
await openSession(options, dependencies);
|
|
282
|
-
return
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
291
|
+
const session = await openSession(options, dependencies);
|
|
292
|
+
return maintainControl(
|
|
293
|
+
session,
|
|
294
|
+
async () => ({
|
|
295
|
+
instance: instance.id,
|
|
296
|
+
services: await resetCheckpoints(options.directory, instance.id, options.service),
|
|
297
|
+
}),
|
|
298
|
+
options.signal,
|
|
299
|
+
);
|
|
286
300
|
}
|
package/src/sync-state.js
CHANGED
|
@@ -26,6 +26,9 @@ function syncFile(directory, key, kind = "") {
|
|
|
26
26
|
function recordedAuthority(authority) {
|
|
27
27
|
const operations = authority?.operations;
|
|
28
28
|
return (
|
|
29
|
+
authority?.kind === "room" &&
|
|
30
|
+
typeof authority.authority === "string" &&
|
|
31
|
+
typeof authority.path === "string" &&
|
|
29
32
|
typeof authority?.log_epoch === "string" &&
|
|
30
33
|
operations !== null &&
|
|
31
34
|
typeof operations === "object" &&
|
|
@@ -43,6 +46,7 @@ export async function readReplicaState(directory, replica) {
|
|
|
43
46
|
}
|
|
44
47
|
const state = await readNote(syncFile(directory, replicaKey(canonical)), null);
|
|
45
48
|
if (state === null) return null;
|
|
49
|
+
if (state.version !== EXPLICIT_SYNC_VERSION) return null;
|
|
46
50
|
if (!Array.isArray(state.authorities) || !state.authorities.every(recordedAuthority)) return null;
|
|
47
51
|
return state;
|
|
48
52
|
}
|
package/src/sync.js
CHANGED
|
@@ -105,9 +105,6 @@ async function bootstrapBundle(session, prefixes, signal) {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
function matchedDiscovery(discovery, scope) {
|
|
108
|
-
if (scope.manifest?.scope === "private") {
|
|
109
|
-
return discovery.authorities.find((entry) => entry.kind === "private");
|
|
110
|
-
}
|
|
111
108
|
return discovery.authorities.find(
|
|
112
109
|
(entry) => entry.kind === "room" && entry.room_ref === scope.manifest?.anchor,
|
|
113
110
|
);
|
|
@@ -173,11 +170,17 @@ function operationsOf(value) {
|
|
|
173
170
|
function authorityRecord(scope, discovery, files) {
|
|
174
171
|
const found = matchedDiscovery(discovery, scope);
|
|
175
172
|
const operations = found === undefined ? null : operationsOf(found.operations);
|
|
176
|
-
if (
|
|
173
|
+
if (
|
|
174
|
+
scope.manifest?.scope !== "room" ||
|
|
175
|
+
typeof scope.manifest.anchor !== "string" ||
|
|
176
|
+
found === undefined ||
|
|
177
|
+
!stringList(found.protected_paths) ||
|
|
178
|
+
operations === null
|
|
179
|
+
) {
|
|
177
180
|
throw new KitRefusal("kit_bootstrap_rejected");
|
|
178
181
|
}
|
|
179
182
|
return {
|
|
180
|
-
kind:
|
|
183
|
+
kind: "room",
|
|
181
184
|
authority: scope.manifest.anchor,
|
|
182
185
|
room_handle: scope.manifest.room_handle,
|
|
183
186
|
path: scope.manifest.path,
|
|
@@ -192,9 +195,10 @@ function authorityRecord(scope, discovery, files) {
|
|
|
192
195
|
async function materialize(options, discovery, bundle, signal, previous = null) {
|
|
193
196
|
const authorities = [];
|
|
194
197
|
for (const scope of bundle.scopes) {
|
|
198
|
+
const record = authorityRecord(scope, discovery, []);
|
|
195
199
|
const held = previous?.authorities.find((entry) => entry.authority === scope.manifest?.anchor);
|
|
196
200
|
const files = await replaceScope(options.replica, held, scope, signal);
|
|
197
|
-
authorities.push(
|
|
201
|
+
authorities.push({ ...record, files });
|
|
198
202
|
}
|
|
199
203
|
const state = {
|
|
200
204
|
version: EXPLICIT_SYNC_VERSION,
|
|
@@ -409,6 +413,7 @@ export async function bootstrap(options, dependencies = {}) {
|
|
|
409
413
|
const canonical = await canonicalReplica(options.replica);
|
|
410
414
|
const existing = await readReplicaState(options.directory, canonical);
|
|
411
415
|
if (existing !== null) throw new KitRefusal("kit_sync_already_bootstrapped", canonical);
|
|
416
|
+
await clearPending(options.directory, canonical);
|
|
412
417
|
return withSession(options, dependencies, async (session, signal) => {
|
|
413
418
|
const discovery = discoveryOf(
|
|
414
419
|
await door(session, KIT_DOOR_DISCOVER_PATH, { prefixes: options.prefixes }, signal),
|