@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/src/house.js CHANGED
@@ -1,6 +1,8 @@
1
- import { readFile } from "node:fs/promises";
2
- import { join } from "node:path";
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
- 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
- }
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
- 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;
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 (ended && release) await releaseControl(session);
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: envelope.plain(request.payload, `${declaration}.request.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
- target.kind === "private" ? entry.kind === "private" : entry.room_handle === target.handle,
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: found.kind,
123
- authority: found.kind === "private" ? "private" : found.room_ref,
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, target, authority, plan, signal) {
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: target.kind === "room" ? authority.authority : null,
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, target, authority, plan, signal);
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
- const context = {
208
- instance,
207
+ return maintainControl(
209
208
  session,
210
- directory: options.directory,
211
- signal: options.signal,
212
- now: session.now,
213
- dependencies,
214
- googleOAuthClient,
215
- };
216
- const delivered = [];
217
- for (const service of services) delivered.push(await deliverService(context, service));
218
- return { instance: instance.id, services: delivered };
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
- if (outstanding.some((count) => count > 0)) context.session = await openSession(options, dependencies);
264
- const retried = [];
265
- for (const [position, service] of services.entries()) {
266
- retried.push({
267
- service: service.service,
268
- originals:
269
- outstanding[position] === 0
270
- ? []
271
- : await saveSourceOriginals(context, serviceBinding(context, service)),
272
- });
273
- }
274
- return { instance: instance.id, services: retried };
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
- instance: instance.id,
284
- services: await resetCheckpoints(options.directory, instance.id, options.service),
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 (found === undefined || !stringList(found.protected_paths) || operations === null) {
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: scope.manifest.scope === "private" ? "private" : "room",
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(authorityRecord(scope, discovery, files));
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),