@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/src/launcher.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { commandHouseInstructions, commandHouseRequest } from "./adapter-mcp.js";
2
+ import { commandHouseInstructions } from "./adapter-mcp.js";
3
3
  import {
4
4
  compiledContract,
5
5
  initializeCommandAdapter,
@@ -129,6 +129,12 @@ async function configureSession(acp, contract, sessionId, adapter) {
129
129
  }
130
130
  }
131
131
 
132
+ function workingCopyContext(copies) {
133
+ if (!Array.isArray(copies) || copies.length === 0) return [];
134
+ const lines = copies.map((copy) => `${copy.path}: ${copy.repository}`);
135
+ return [{ type: "text", text: ["Local Git working copies of House paths:", ...lines].join("\n") }];
136
+ }
137
+
132
138
  async function runTurn(channel, start, bridge, turn) {
133
139
  const contract = compiledContract(start.adapter?.kind);
134
140
  const adapter = startCommandAdapter(
@@ -185,14 +191,14 @@ async function runTurn(channel, start, bridge, turn) {
185
191
  loading = false;
186
192
  }
187
193
  await configureSession(acp, contract, providerSessionId, start.adapter);
188
- const request = start.request === null ? await commandHouseRequest(bridge) : start.request;
189
194
  turn.adapterSession = true;
190
195
  turn.cancel = () => acp.cancel({ sessionId: providerSessionId });
191
196
  const prompted = await acp.request("session/prompt", {
192
197
  sessionId: providerSessionId,
193
198
  prompt: [
194
- { type: "text", text: JSON.stringify(request) },
199
+ { type: "text", text: JSON.stringify(start.request) },
195
200
  { type: "text", text: instructions },
201
+ ...workingCopyContext(start.working_copies),
196
202
  ],
197
203
  });
198
204
  await new Promise((resolve) => setImmediate(resolve));
@@ -1,6 +1,7 @@
1
1
  import { isAbsolute } from "node:path";
2
- import { parse, stringify } from "yaml";
2
+ import { stringify } from "yaml";
3
3
  import { AGENT_OPERATION_META, ATTACHMENT_FILE_META } from "./contract.js";
4
+ import { answerText, answerYaml } from "./mcp.js";
4
5
  import { derivedOperationId } from "./operation.js";
5
6
  import { localAttachment, sendLocalConversationFile, uploadLocalAttachment } from "./original.js";
6
7
  import { KitRefusal } from "./refusal.js";
@@ -12,18 +13,9 @@ class HouseRefused extends Error {
12
13
  }
13
14
  }
14
15
 
15
- function parsed(text) {
16
- try {
17
- return parse(text);
18
- } catch {
19
- return null;
20
- }
21
- }
22
-
23
16
  function answerOf(frame) {
24
17
  if (frame.error !== undefined || frame.result?.isError === true) throw new HouseRefused(frame);
25
- const text = (frame.result?.content ?? []).filter((item) => item.type === "text").map((item) => item.text).join("\n");
26
- const answer = parsed(text);
18
+ const answer = answerYaml(answerText(frame.result));
27
19
  if (answer === null || typeof answer !== "object") throw new KitRefusal("kit_house_answer_unreadable");
28
20
  return answer;
29
21
  }
package/src/login.js CHANGED
@@ -8,7 +8,7 @@ import { createInterface } from "node:readline/promises";
8
8
  import { stdin, stdout } from "node:process";
9
9
  import { KIT_CREDENTIAL } from "./contract.js";
10
10
  import { launchBase, prepareLaunchBase } from "./directory.js";
11
- import { houseRequest } from "./house.js";
11
+ import { houseRequest, refuseDrained } from "./house.js";
12
12
  import { KitRefusal } from "./refusal.js";
13
13
  import { writeText } from "./state.js";
14
14
 
@@ -260,6 +260,7 @@ export async function openBrowser(url) {
260
260
  }
261
261
 
262
262
  export async function connectHouse(options, dependencies = {}) {
263
+ refuseDrained(options.directory);
263
264
  await prepareLaunchBase(launchBase(dependencies));
264
265
  if (options.presentation === "manual") return login(options, dependencies);
265
266
  const callback = await loopback();
package/src/mcp.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { parse } from "yaml";
2
3
  import { bridgePeer } from "./peer.js";
3
4
  import { KitRefusal } from "./refusal.js";
4
5
 
@@ -69,3 +70,15 @@ export async function bridgeCall(bridge, request, signal = undefined) {
69
70
  await peer.end();
70
71
  }
71
72
  }
73
+
74
+ export function answerText(result) {
75
+ return (result?.content ?? []).filter((item) => item.type === "text").map((item) => item.text).join("\n");
76
+ }
77
+
78
+ export function answerYaml(text) {
79
+ try {
80
+ return parse(text);
81
+ } catch {
82
+ return null;
83
+ }
84
+ }
package/src/mount.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { join } from "node:path";
3
3
  import { readFile, writeFile } from "node:fs/promises";
4
- import { apply, links, parse, serialize } from "@agentshouse/mdmodel";
4
+ import { apply, links, parse, retarget, serialize } from "@agentshouse/mdmodel";
5
5
  import {
6
6
  KIT_DOOR_BATCH_PATH,
7
7
  KIT_DOOR_BOOTSTRAP_PATH,
@@ -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,
@@ -128,16 +128,11 @@ function authorityFor(discovery, target) {
128
128
  };
129
129
  }
130
130
 
131
- function hrefSuffix(href) {
132
- const match = /[?#]/.exec(href);
133
- return match === null ? "" : href.slice(match.index);
134
- }
135
-
136
131
  function replaceLink(document, href, destination) {
137
132
  const marker = `house-kit-link:${operationId()}`;
138
133
  const located = apply(document, { kind: "link", href, to: marker });
139
134
  if (located.outcome !== "applied") refuse("kit_mount_link_unwritable", href);
140
- const cleared = parse(serialize(located.document).replaceAll(`${marker}${hrefSuffix(href)}`, marker));
135
+ const cleared = parse(serialize(located.document).replaceAll(retarget(href, marker), marker));
141
136
  if (!cleared.ok) refuse("kit_mount_link_unwritable", href);
142
137
  const changed = apply(cleared.document, { kind: "link", href: marker, to: destination });
143
138
  if (changed.outcome !== "applied") refuse("kit_mount_link_unwritable", href);
@@ -178,11 +173,8 @@ export function rewriteAttachmentLinks(held, mintVersion) {
178
173
  refuse("kit_migrator_referrer_undeclared", `${relative} -> ${path}`);
179
174
  }
180
175
  const version = mintVersion(path);
181
- const applied = apply(document, {
182
- kind: "link",
183
- href: link.href,
184
- to: `${PENDING_LINK_SCHEME}:${version}`,
185
- });
176
+ const pending = `${PENDING_LINK_SCHEME}:${version}`;
177
+ const applied = apply(document, { kind: "link", href: link.href, to: pending });
186
178
  if (applied.outcome !== "applied") refuse("kit_mount_link_unwritable", link.href);
187
179
  document = applied.document;
188
180
  let save = saves.find((entry) => entry.path === path);
@@ -191,8 +183,8 @@ export function rewriteAttachmentLinks(held, mintVersion) {
191
183
  saves.push(save);
192
184
  }
193
185
  if (!save.documents.includes(relative)) save.documents.push(relative);
194
- const suffix = hrefSuffix(link.href);
195
- const href = `${PENDING_LINK_SCHEME}:${version}${suffix}`;
186
+ const suffix = retarget(link.href, "");
187
+ const href = retarget(link.href, pending);
196
188
  if (!save.hrefs.some((entry) => entry.document === relative && entry.href === href)) {
197
189
  save.hrefs.push({ document: relative, href, suffix });
198
190
  }
@@ -326,7 +318,7 @@ function pendingTransfer(held, save) {
326
318
  };
327
319
  }
328
320
 
329
- async function submitAttachments(session, held, target, authority, plan, signal) {
321
+ async function submitAttachments(session, held, authority, plan, signal) {
330
322
  const transfers = [];
331
323
  for (const save of plan.saves) {
332
324
  const local = held.attachments.get(save.path);
@@ -341,7 +333,7 @@ async function submitAttachments(session, held, target, authority, plan, signal)
341
333
  version: save.version,
342
334
  name: declared.name,
343
335
  mediaType: declared.mediaType ?? undefined,
344
- room: target.kind === "room" ? authority.authority : null,
336
+ room: authority.authority,
345
337
  body,
346
338
  signal,
347
339
  });
@@ -471,7 +463,7 @@ export async function mountPackage(held, options, dependencies = {}) {
471
463
  };
472
464
  const localReport = join(held.root, "MIGRATION.json");
473
465
  await writeFile(localReport, `${JSON.stringify(result, null, 2)}\n`);
474
- result.transfers = await submitAttachments(session, held, target, authority, plan, signal);
466
+ result.transfers = await submitAttachments(session, held, authority, plan, signal);
475
467
 
476
468
  const report = `${target.subtree}/${MOUNT_REPORT_NAME}`;
477
469
  await writeFile(localReport, `${JSON.stringify(result, null, 2)}\n`);
package/src/repository.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { createHash } from "node:crypto";
2
- import { spawn } from "node:child_process";
3
2
  import { constants } from "node:fs";
4
3
  import { open, readlink, realpath } from "node:fs/promises";
5
4
  import { join } from "node:path";
5
+ import { runGit } from "./git.js";
6
6
  import { deliverPublicationChunk, maintainControl, openPublicationManifest, openSession } from "./house.js";
7
7
  import { KitRefusal } from "./refusal.js";
8
8
  import { readRepositoryState, writeRepositoryState } from "./state.js";
@@ -258,69 +258,11 @@ async function* chunkArchive(checkout, entries, signal) {
258
258
  yield Buffer.alloc(BLOCK_BYTES * 2);
259
259
  }
260
260
 
261
- function runGit(checkout, args, dependencies, capture, signal) {
262
- active(signal);
263
- const launcher = dependencies.spawn ?? spawn;
264
- return new Promise((resolve, reject) => {
265
- let child;
266
- try {
267
- child = launcher("git", args, {
268
- cwd: checkout,
269
- env: process.env,
270
- stdio: capture ? ["ignore", "pipe", "inherit"] : "inherit",
271
- signal,
272
- killSignal: "SIGKILL",
273
- });
274
- } catch (error) {
275
- reject(
276
- signal?.aborted === true || error?.name === "AbortError"
277
- ? new KitRefusal("kit_command_cancelled")
278
- : new KitRefusal("kit_git_unavailable"),
279
- );
280
- return;
281
- }
282
- const output = [];
283
- let settled = false;
284
- const finish = (operation) => {
285
- if (settled) return;
286
- settled = true;
287
- operation();
288
- };
289
- child.stdout?.on("data", (chunk) => output.push(Buffer.from(chunk)));
290
- child.once("error", (error) =>
291
- finish(() =>
292
- reject(
293
- signal?.aborted === true || error?.name === "AbortError"
294
- ? new KitRefusal("kit_command_cancelled")
295
- : new KitRefusal("kit_git_unavailable"),
296
- ),
297
- ),
298
- );
299
- child.once("close", (status) => {
300
- finish(() => {
301
- if (signal?.aborted === true) {
302
- reject(new KitRefusal("kit_command_cancelled"));
303
- return;
304
- }
305
- if (status !== 0) {
306
- reject(
307
- new KitRefusal(args[0] === "pull" ? "kit_git_pull_failed" : "kit_git_selection_failed"),
308
- );
309
- return;
310
- }
311
- resolve(Buffer.concat(output));
312
- });
313
- });
314
- });
315
- }
316
-
317
261
  async function selectedPaths(checkout, dependencies, signal) {
318
262
  const output = await runGit(
319
263
  checkout,
320
264
  ["ls-files", "--cached", "--others", "--exclude-standard", "-z"],
321
- dependencies,
322
- true,
323
- signal,
265
+ { dependencies, capture: true, signal, failure: "kit_git_selection_failed" },
324
266
  );
325
267
  const paths = [];
326
268
  let offset = 0;
@@ -469,7 +411,7 @@ export async function captureRepository(options, dependencies = {}) {
469
411
  session,
470
412
  async (signal) => {
471
413
  const state = stateOf(await readRepositoryState(options.directory, key), identity);
472
- await runGit(identity.checkout, ["pull"], dependencies, false, signal);
414
+ await runGit(identity.checkout, ["pull"], { dependencies, signal, failure: "kit_git_pull_failed" });
473
415
  const now = dependencies.now ?? (() => new Date());
474
416
  let sourceRef = state.source_ref;
475
417
  let publication = null;
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "../git-main.js";
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
  }
@@ -100,13 +100,12 @@ function recordedMap(authority) {
100
100
  return new Map(authority.files.map((file) => [file.path, file]));
101
101
  }
102
102
 
103
- function structuredOrReplace(root, relative, recorded, content) {
103
+ export function structuredOrReplace(root, relative, recorded, content) {
104
104
  const path = housePath(root, relative);
105
+ const whole = [{ op: "replace", path, base: recorded.revision, content }];
105
106
  const before = parse(recorded.content, relative);
106
107
  const after = parse(content, relative);
107
- if (!before.ok || !after.ok) {
108
- return [{ op: "replace", path, base: recorded.revision, content }];
109
- }
108
+ if (!before.ok || !after.ok) return whole;
110
109
  const ops = [];
111
110
  if (before.document.preamble !== after.document.preamble) {
112
111
  ops.push({
@@ -124,9 +123,7 @@ function structuredOrReplace(root, relative, recorded, content) {
124
123
  if (JSON.stringify(before.document.fields[name]) === JSON.stringify(after.document.fields[name])) {
125
124
  continue;
126
125
  }
127
- if (after.document.fields[name] === undefined) {
128
- return [{ op: "replace", path, base: recorded.revision, content }];
129
- }
126
+ if (after.document.fields[name] === undefined) return whole;
130
127
  const change = { op: "set_field", path, field: name, value: after.document.fields[name] };
131
128
  if (before.document.fields[name] !== undefined) change.base = before.document.fields[name];
132
129
  ops.push(change);
@@ -139,12 +136,11 @@ function structuredOrReplace(root, relative, recorded, content) {
139
136
  const prior = before.document.sections[name];
140
137
  const next = after.document.sections[name];
141
138
  if (prior === next) continue;
142
- if (prior === undefined || next === undefined) {
143
- return [{ op: "replace", path, base: recorded.revision, content }];
144
- }
139
+ if (prior === undefined || next === undefined) return whole;
145
140
  ops.push({ op: "replace_section", path, section: name, base: prior, content: next });
146
141
  }
147
- return ops.length === 0 ? [{ op: "replace", path, base: recorded.revision, content }] : ops;
142
+ if (ops.length === 0) return whole;
143
+ return applyChanges({ [relative]: recorded.content }, ops, root)[relative] === content ? ops : whole;
148
144
  }
149
145
 
150
146
  export function deriveChanges(state, authority, localFiles) {
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
@@ -41,12 +41,12 @@ function sessionDependencies(options, dependencies) {
41
41
  };
42
42
  }
43
43
 
44
- async function withSession(options, dependencies, operation) {
44
+ export async function withSession(options, dependencies, operation) {
45
45
  const session = await openSession(options, sessionDependencies(options, dependencies));
46
46
  return maintainControl(session, (signal) => operation(session, signal), options.signal);
47
47
  }
48
48
 
49
- async function door(session, path, body, signal) {
49
+ export async function door(session, path, body, signal) {
50
50
  return houseRequest(
51
51
  session.house,
52
52
  await heldControl(session, signal),
@@ -83,7 +83,7 @@ function changePosition(value, declaration) {
83
83
  return { position: value.position, log_epoch: value.log_epoch };
84
84
  }
85
85
 
86
- function discoveryOf(value) {
86
+ export function discoveryOf(value) {
87
87
  if (typeof value?.md_model_version !== "string" || !Array.isArray(value.authorities)) {
88
88
  throw new KitRefusal("kit_sync_rejected", "discover");
89
89
  }
@@ -93,7 +93,7 @@ function discoveryOf(value) {
93
93
  return value;
94
94
  }
95
95
 
96
- async function bootstrapBundle(session, prefixes, signal) {
96
+ export async function bootstrapBundle(session, prefixes, signal) {
97
97
  const answer = await door(session, KIT_DOOR_BOOTSTRAP_PATH, { prefixes }, signal);
98
98
  if (typeof answer?.transfer?.url === "string") {
99
99
  const granted = await houseGetJson(answer.transfer.url, session.dependencies, signal);
@@ -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),