@agentshouse/kit 0.0.1 → 0.1.0-alpha.1

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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1696 -2
  3. package/bin/connect-linux.sh +127 -0
  4. package/bin/kit.js +1023 -0
  5. package/package.json +33 -3
  6. package/src/acp.js +99 -0
  7. package/src/adapter-mcp.js +44 -0
  8. package/src/adapter.js +252 -0
  9. package/src/agent-logins.js +65 -0
  10. package/src/agents.js +449 -0
  11. package/src/apply.js +29 -0
  12. package/src/attach.js +52 -0
  13. package/src/binding.js +90 -0
  14. package/src/blocks.js +156 -0
  15. package/src/bridge.js +449 -0
  16. package/src/bytes.js +89 -0
  17. package/src/channel.js +37 -0
  18. package/src/command.js +109 -0
  19. package/src/configuration.js +465 -0
  20. package/src/connector-login.js +91 -0
  21. package/src/connector.js +107 -0
  22. package/src/continuation.js +61 -0
  23. package/src/contract.js +363 -0
  24. package/src/conversation-originals.js +143 -0
  25. package/src/custom/authorization.js +63 -0
  26. package/src/declaration.js +35 -0
  27. package/src/delivery.js +191 -0
  28. package/src/directory.js +102 -0
  29. package/src/draft.js +89 -0
  30. package/src/google/calendar.js +177 -0
  31. package/src/google/declared.js +146 -0
  32. package/src/google/drive.js +435 -0
  33. package/src/google/gateway.js +166 -0
  34. package/src/google/gmail.js +249 -0
  35. package/src/google/id.js +1 -0
  36. package/src/google/oauth.js +273 -0
  37. package/src/google/source.js +25 -0
  38. package/src/hook.js +376 -0
  39. package/src/host.js +121 -0
  40. package/src/house-main.js +79 -0
  41. package/src/house.js +1085 -0
  42. package/src/inbound.js +175 -0
  43. package/src/instance/google.js +1 -0
  44. package/src/launcher-main.js +19 -0
  45. package/src/launcher.js +276 -0
  46. package/src/local-files.js +113 -0
  47. package/src/login.js +274 -0
  48. package/src/mcp.js +71 -0
  49. package/src/migrator.js +248 -0
  50. package/src/mount.js +582 -0
  51. package/src/operation.js +20 -0
  52. package/src/original.js +631 -0
  53. package/src/peer.js +124 -0
  54. package/src/plan.js +57 -0
  55. package/src/question.js +316 -0
  56. package/src/refusal.js +79 -0
  57. package/src/repository.js +521 -0
  58. package/src/session-bin/house +2 -0
  59. package/src/settlement.js +57 -0
  60. package/src/setup.js +185 -0
  61. package/src/source-originals.js +116 -0
  62. package/src/source.js +286 -0
  63. package/src/starter-set/grilling/LICENSE +21 -0
  64. package/src/starter-set/grilling/SKILL.md +30 -0
  65. package/src/starter-set/grilling/agents/openai.yaml +3 -0
  66. package/src/starter-set.js +17 -0
  67. package/src/state.js +157 -0
  68. package/src/sync-library.js +293 -0
  69. package/src/sync-state.js +116 -0
  70. package/src/sync.js +477 -0
  71. package/src/telegram.js +1045 -0
  72. package/src/terminal.js +57 -0
  73. package/src/tmux.js +119 -0
  74. package/src/worker.js +756 -0
  75. package/src/workspace.js +500 -0
package/src/login.js ADDED
@@ -0,0 +1,274 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { access, readFile } from "node:fs/promises";
3
+ import { createServer } from "node:http";
4
+ import { hostname } from "node:os";
5
+ import { join } from "node:path";
6
+ import { spawn } from "node:child_process";
7
+ import { createInterface } from "node:readline/promises";
8
+ import { stdin, stdout } from "node:process";
9
+ import { KIT_CREDENTIAL } from "./contract.js";
10
+ import { launchBase, prepareLaunchBase } from "./directory.js";
11
+ import { houseRequest } from "./house.js";
12
+ import { KitRefusal } from "./refusal.js";
13
+ import { writeText } from "./state.js";
14
+
15
+ const ENVIRONMENT =
16
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
17
+
18
+ function verifier() {
19
+ return randomBytes(32).toString("base64url");
20
+ }
21
+
22
+ function challenge(value) {
23
+ return createHash("sha256").update(value).digest("base64url");
24
+ }
25
+
26
+ async function waitFor(promise, signal) {
27
+ if (signal === undefined) return promise;
28
+ if (signal.aborted) throw new KitRefusal("kit_command_cancelled");
29
+ let cancel;
30
+ const cancellation = new Promise((_, reject) => {
31
+ cancel = () => reject(new KitRefusal("kit_command_cancelled"));
32
+ signal.addEventListener("abort", cancel, { once: true });
33
+ });
34
+ try {
35
+ return await Promise.race([promise, cancellation]);
36
+ } catch (error) {
37
+ if (signal.aborted) throw new KitRefusal("kit_command_cancelled");
38
+ throw error;
39
+ } finally {
40
+ signal.removeEventListener("abort", cancel);
41
+ }
42
+ }
43
+
44
+ async function request(options, dependencies, path, body) {
45
+ return waitFor(
46
+ houseRequest(options.house, null, path, body, dependencies, options.signal),
47
+ options.signal,
48
+ );
49
+ }
50
+
51
+ function startedLogin(value) {
52
+ if (
53
+ typeof value !== "object" ||
54
+ value === null ||
55
+ typeof value.start_url !== "string" ||
56
+ !URL.canParse(value.start_url)
57
+ ) {
58
+ throw new KitRefusal("kit_login_rejected");
59
+ }
60
+ return { startUrl: value.start_url };
61
+ }
62
+
63
+ function exchangedCredential(value, retained) {
64
+ if (
65
+ typeof value !== "object" ||
66
+ value === null ||
67
+ typeof value.credential !== "string" ||
68
+ !KIT_CREDENTIAL.test(value.credential) ||
69
+ typeof value.environment !== "string" ||
70
+ !ENVIRONMENT.test(value.environment) ||
71
+ (retained !== null && value.environment !== retained)
72
+ ) {
73
+ throw new KitRefusal("kit_login_rejected");
74
+ }
75
+ return { credential: value.credential, environment: value.environment };
76
+ }
77
+
78
+ export const ENROLLMENT_USAGE =
79
+ "kit login [--house <url>] [--environment <uuid>] [--manual]";
80
+
81
+ export async function storedEnvironment(directory) {
82
+ let value;
83
+ try {
84
+ value = (await readFile(join(directory, "environment"), "utf8")).trim();
85
+ } catch (error) {
86
+ if (error.code === "ENOENT") return null;
87
+ throw new KitRefusal("kit_environment_invalid", error.code ?? "unreadable");
88
+ }
89
+ if (!ENVIRONMENT.test(value)) throw new KitRefusal("kit_environment_invalid", "stored Environment id is malformed");
90
+ return value;
91
+ }
92
+
93
+ async function storedCredentialPresent(directory) {
94
+ try {
95
+ await access(join(directory, "credential"));
96
+ return true;
97
+ } catch (error) {
98
+ if (error.code === "ENOENT") return false;
99
+ throw new KitRefusal("kit_environment_invalid", error.code ?? "unreadable credential");
100
+ }
101
+ }
102
+
103
+ function houseOrigin(value) {
104
+ if (typeof value !== "string" || !URL.canParse(value)) {
105
+ throw new KitRefusal("kit_house_invalid", "House origin is malformed");
106
+ }
107
+ const origin = new URL(value);
108
+ if (origin.origin !== value || !(origin.protocol === "https:" ||
109
+ (origin.protocol === "http:" && ["127.0.0.1", "localhost"].includes(origin.hostname)))) {
110
+ throw new KitRefusal("kit_house_invalid", "House origin is malformed");
111
+ }
112
+ return value;
113
+ }
114
+
115
+ export async function storedHouse(directory, firstLogin = false, requested = undefined) {
116
+ let value;
117
+ try {
118
+ value = (await readFile(join(directory, "house"), "utf8")).trim();
119
+ } catch (error) {
120
+ if (error.code === "ENOENT") {
121
+ if ((firstLogin || requested !== undefined) &&
122
+ await storedEnvironment(directory) === null && !await storedCredentialPresent(directory)) {
123
+ return houseOrigin(requested ?? "https://agents.house");
124
+ }
125
+ }
126
+ throw new KitRefusal("kit_house_invalid", error.code ?? "unreadable");
127
+ }
128
+ houseOrigin(value);
129
+ if (requested !== undefined && requested !== value) {
130
+ throw new KitRefusal("kit_house_mismatch", "this Environment is bound to another House origin");
131
+ }
132
+ return value;
133
+ }
134
+
135
+ export function enrollmentAct(options, retained, host) {
136
+ if (options.label !== undefined || (options.environment !== undefined && !ENVIRONMENT.test(options.environment))) {
137
+ throw new KitRefusal("kit_login_rejected", ENROLLMENT_USAGE);
138
+ }
139
+ const environment = options.environment ?? retained;
140
+ if (environment === null) {
141
+ if (typeof host !== "string" || host.trim() === "") {
142
+ throw new KitRefusal("kit_login_rejected", "computer hostname is empty");
143
+ }
144
+ return { retained: null, body: { act: "new_environment", label: host } };
145
+ }
146
+ return {
147
+ retained: environment,
148
+ body: { act: "existing_environment", environment_id: environment },
149
+ };
150
+ }
151
+
152
+ export async function login(options, dependencies = {}) {
153
+ const open = dependencies.open ?? openBrowser;
154
+ const readCode = dependencies.readCode ?? manualCode;
155
+ const write = dependencies.write ?? ((line) => stdout.write(`${line}\n`));
156
+ const codeVerifier = verifier();
157
+ const retained = await storedEnvironment(options.directory);
158
+ if (retained === null && options.environment === undefined &&
159
+ await storedCredentialPresent(options.directory)) {
160
+ throw new KitRefusal("kit_environment_invalid", "credential exists without its Environment id");
161
+ }
162
+ const act = enrollmentAct(options, retained,
163
+ options.hostname ?? dependencies.hostname?.() ?? hostname());
164
+ const body = {
165
+ ...act.body,
166
+ code_challenge: challenge(codeVerifier),
167
+ presentation: options.presentation,
168
+ ...(options.loopbackUri ? { loopback_uri: options.loopbackUri } : {}),
169
+ };
170
+ const started = startedLogin(await request(options, dependencies, "/kit", body));
171
+ if (options.presentation === "manual" || options.hostBrowser === true) {
172
+ write(`Open ${started.startUrl}`);
173
+ } else {
174
+ await waitFor(Promise.resolve().then(() => open(started.startUrl)), options.signal);
175
+ }
176
+ const code = await waitFor(Promise.resolve().then(() => readCode(options.signal)), options.signal);
177
+ let settled;
178
+ try {
179
+ settled = await request(options, dependencies, "/kit/token", {
180
+ code,
181
+ code_verifier: codeVerifier,
182
+ });
183
+ } catch (error) {
184
+ if (error instanceof KitRefusal && error.code === "kit_house_unavailable") {
185
+ throw new KitRefusal(
186
+ "kit_credential_not_installed",
187
+ "House did not answer the exchange, so no credential was installed; " +
188
+ "run kit login again for this Environment to obtain a fresh owner-authorized credential",
189
+ );
190
+ }
191
+ throw error;
192
+ }
193
+ const exchanged = exchangedCredential(settled, act.retained);
194
+ await writeText(join(options.directory, "environment"), `${exchanged.environment}\n`);
195
+ await writeText(join(options.directory, "house"), `${options.house}\n`);
196
+ await writeText(
197
+ join(options.directory, "credential"),
198
+ `${exchanged.credential}\n`,
199
+ );
200
+ write(
201
+ act.retained === null
202
+ ? `House Kit Environment ${exchanged.environment} connected.`
203
+ : `House Kit Environment ${exchanged.environment} reconnected.`,
204
+ );
205
+ return { environment: exchanged.environment };
206
+ }
207
+
208
+ export async function loopback() {
209
+ const server = createServer();
210
+ await new Promise((resolve, reject) => {
211
+ server.once("error", reject);
212
+ server.listen(0, "127.0.0.1", resolve);
213
+ });
214
+ const address = server.address();
215
+ const code = new Promise((resolve, reject) => {
216
+ server.once("request", (request, response) => {
217
+ const received = new URL(
218
+ request.url ?? "/",
219
+ `http://127.0.0.1:${address.port}`,
220
+ ).searchParams.get("code");
221
+ response.writeHead(received ? 200 : 400, {
222
+ "content-type": "text/plain; charset=utf-8",
223
+ });
224
+ response.end(
225
+ received
226
+ ? "House Kit connected. You can close this window.\n"
227
+ : "Login failed.\n",
228
+ );
229
+ server.close();
230
+ if (received) resolve(received);
231
+ else reject(new KitRefusal("kit_login_rejected"));
232
+ });
233
+ });
234
+ return {
235
+ uri: `http://127.0.0.1:${address.port}/callback`,
236
+ code: () => code,
237
+ close: () => new Promise((resolve) => server.close(() => resolve())),
238
+ };
239
+ }
240
+
241
+ export async function manualCode(signal) {
242
+ const terminal = createInterface({ input: stdin, output: stdout });
243
+ try {
244
+ return (await terminal.question("One-time code: ", { signal })).trim();
245
+ } finally {
246
+ terminal.close();
247
+ }
248
+ }
249
+
250
+ export async function openBrowser(url) {
251
+ const command =
252
+ process.platform === "darwin"
253
+ ? "open"
254
+ : process.platform === "win32"
255
+ ? "cmd"
256
+ : "xdg-open";
257
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
258
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
259
+ child.unref();
260
+ }
261
+
262
+ export async function connectHouse(options, dependencies = {}) {
263
+ await prepareLaunchBase(launchBase(dependencies));
264
+ if (options.presentation === "manual") return login(options, dependencies);
265
+ const callback = await loopback();
266
+ try {
267
+ return await login(
268
+ { ...options, presentation: "loopback", loopbackUri: callback.uri },
269
+ { ...dependencies, readCode: callback.code },
270
+ );
271
+ } finally {
272
+ await callback.close().catch(() => undefined);
273
+ }
274
+ }
package/src/mcp.js ADDED
@@ -0,0 +1,71 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { bridgePeer } from "./peer.js";
3
+ import { KitRefusal } from "./refusal.js";
4
+
5
+ export function workerMcpPeer(options) {
6
+ const client = randomUUID();
7
+ const pending = new Map();
8
+ let bridge = null;
9
+ const peer = bridgePeer({
10
+ socket: options.socket,
11
+ nonce: options.nonce,
12
+ role: "mcp",
13
+ retryInitial: true,
14
+ hello: () => ({ client, bridge: [...pending.values()].find((request) => request.bridge !== null)?.bridge ?? null }),
15
+ greeted: (frame) => {
16
+ if (frame.type !== "ready" || typeof frame.bridge !== "string") return false;
17
+ if ([...pending.values()].some((request) => request.bridge !== null && request.bridge !== frame.bridge)) return false;
18
+ bridge = frame.bridge;
19
+ for (const retained of pending.values()) peer.send(retained.message);
20
+ return true;
21
+ },
22
+ onSend: (frame) => {
23
+ const retained = pending.get(frame.delivery);
24
+ if (frame.type === "mcp" && retained !== undefined) retained.bridge = bridge;
25
+ },
26
+ onFrame: (frame) => {
27
+ if (frame.type !== "mcp") {
28
+ options.onClosed?.(frame);
29
+ return;
30
+ }
31
+ if (pending.delete(frame.delivery) && frame.frame !== null) options.onFrame(frame.frame);
32
+ peer.send({ type: "ack", delivery: frame.delivery });
33
+ },
34
+ });
35
+ return {
36
+ open: peer.open,
37
+ refused: peer.refused,
38
+ send(frame) {
39
+ const delivery = randomUUID();
40
+ const retained = { type: "mcp", delivery, frame: structuredClone(frame) };
41
+ pending.set(delivery, { message: retained, bridge: null });
42
+ peer.send(retained);
43
+ },
44
+ end() {
45
+ pending.clear();
46
+ return peer.end();
47
+ },
48
+ };
49
+ }
50
+
51
+ export async function bridgeCall(bridge, request, signal = undefined) {
52
+ let accept;
53
+ let refuse;
54
+ const response = new Promise((resolve, reject) => { accept = resolve; refuse = reject; });
55
+ const peer = workerMcpPeer({
56
+ socket: bridge.socket,
57
+ nonce: bridge.nonce,
58
+ onFrame: accept,
59
+ onClosed: () => refuse(new KitRefusal("kit_bridge_refused")),
60
+ });
61
+ signal?.addEventListener("abort", () => refuse(signal.reason), { once: true });
62
+ void peer.open().then((greeting) => {
63
+ if (greeting?.type !== "ready") refuse(new KitRefusal("kit_bridge_refused"));
64
+ else peer.send({ jsonrpc: "2.0", id: 1, ...request });
65
+ }, refuse);
66
+ try {
67
+ return await Promise.race([response, peer.refused]);
68
+ } finally {
69
+ await peer.end();
70
+ }
71
+ }
@@ -0,0 +1,248 @@
1
+ import { lstat, readdir, readFile, stat } from "node:fs/promises";
2
+ import { createHash } from "node:crypto";
3
+ import { basename, join, resolve } from "node:path";
4
+ import { runCommand } from "./command.js";
5
+ import {
6
+ MIGRATOR_ATTACHMENTS_HOME,
7
+ MIGRATOR_ATTACHMENTS_MAX,
8
+ MIGRATOR_CATEGORY,
9
+ MIGRATOR_CONTENT_HOME,
10
+ MIGRATOR_CONTRACT,
11
+ MIGRATOR_DOWNLOAD_OUTCOMES,
12
+ MIGRATOR_MANIFEST,
13
+ MIGRATOR_MODEL_HOME,
14
+ MIGRATOR_OUTPUT_MAX_BYTES,
15
+ } from "./contract.js";
16
+ import { declarationReader } from "./declaration.js";
17
+ import { KitRefusal } from "./refusal.js";
18
+
19
+ const SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
20
+ const MARKDOWN = /\.md$/;
21
+ const NUL = "\u0000";
22
+
23
+ const { refuse, closed, text, optionalText, filled } = declarationReader(
24
+ "kit_migrator_manifest_invalid",
25
+ );
26
+
27
+ function packageRefusal(code, detail) {
28
+ return new KitRefusal(code, detail);
29
+ }
30
+
31
+ function relativePath(value, declaration) {
32
+ const path = text(value, declaration, 2048);
33
+ const segments = path.split("/");
34
+ if (segments.some((segment) => !SEGMENT.test(segment))) refuse(declaration);
35
+ return path;
36
+ }
37
+
38
+ function attachmentOf(value, position) {
39
+ const declaration = `attachments[${position}]`;
40
+ const entry = closed(
41
+ value,
42
+ ["path", "source", "name", "media_type", "download", "failure", "referrers"],
43
+ declaration,
44
+ );
45
+ if (!MIGRATOR_DOWNLOAD_OUTCOMES.includes(entry.download)) refuse(`${declaration}.download`);
46
+ const downloaded = entry.download === "downloaded";
47
+ if (downloaded === (entry.failure !== undefined)) refuse(`${declaration}.failure`);
48
+ if (downloaded !== (entry.path !== undefined)) refuse(`${declaration}.path`);
49
+ const referrers = filled(entry.referrers, `${declaration}.referrers`).map((referrer, index) =>
50
+ relativePath(referrer, `${declaration}.referrers[${index}]`),
51
+ );
52
+ if (referrers.some((referrer) => !MARKDOWN.test(referrer))) {
53
+ refuse(`${declaration}.referrers`);
54
+ }
55
+ const path = downloaded ? relativePath(entry.path, `${declaration}.path`) : null;
56
+ return {
57
+ path,
58
+ source: text(entry.source, `${declaration}.source`, 2048),
59
+ name:
60
+ optionalText(entry.name, `${declaration}.name`, 255) ??
61
+ (path === null ? null : basename(path)),
62
+ mediaType: optionalText(entry.media_type, `${declaration}.media_type`, 255),
63
+ download: entry.download,
64
+ failure: optionalText(entry.failure, `${declaration}.failure`, 255),
65
+ referrers,
66
+ };
67
+ }
68
+
69
+ export function parseManifest(document) {
70
+ const declared = closed(
71
+ document,
72
+ ["contract", "category", "migrator", "attachments"],
73
+ "manifest",
74
+ );
75
+ if (declared.contract !== MIGRATOR_CONTRACT) refuse("manifest.contract");
76
+ if (declared.category !== MIGRATOR_CATEGORY) refuse("manifest.category");
77
+ const attachments = declared.attachments === undefined ? [] : declared.attachments;
78
+ if (!Array.isArray(attachments) || attachments.length > MIGRATOR_ATTACHMENTS_MAX) {
79
+ refuse("manifest.attachments");
80
+ }
81
+ const parsed = attachments.map((entry, position) => attachmentOf(entry, position));
82
+ const paths = parsed.flatMap((entry) => (entry.path === null ? [] : [entry.path]));
83
+ if (new Set(paths).size !== paths.length) refuse("manifest.attachments");
84
+ return {
85
+ contract: declared.contract,
86
+ category: declared.category,
87
+ migrator: text(declared.migrator, "manifest.migrator", 128),
88
+ attachments: parsed,
89
+ };
90
+ }
91
+
92
+ async function listTree(root, prefix = "") {
93
+ let entries;
94
+ try {
95
+ if (!(await lstat(join(root, prefix))).isDirectory()) {
96
+ throw packageRefusal("kit_migrator_package_invalid", prefix || root);
97
+ }
98
+ entries = await readdir(join(root, prefix), { withFileTypes: true });
99
+ } catch (error) {
100
+ if (error.code === "ENOENT") return [];
101
+ throw error;
102
+ }
103
+ const found = [];
104
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
105
+ const relative = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
106
+ if (!SEGMENT.test(entry.name)) {
107
+ throw packageRefusal("kit_migrator_package_invalid", relative);
108
+ }
109
+ if (entry.isDirectory()) {
110
+ found.push(...(await listTree(root, relative)));
111
+ continue;
112
+ }
113
+ if (!entry.isFile()) throw packageRefusal("kit_migrator_package_invalid", relative);
114
+ found.push(relative);
115
+ }
116
+ return found;
117
+ }
118
+
119
+ async function readTextTree(root, home) {
120
+ const files = {};
121
+ for (const relative of await listTree(join(root, home))) {
122
+ let content;
123
+ try {
124
+ content = new TextDecoder("utf-8", { fatal: true }).decode(await readFile(join(root, home, relative)));
125
+ } catch {
126
+ throw packageRefusal("kit_migrator_binary_content", `${home}/${relative}`);
127
+ }
128
+ if (content.includes(NUL)) {
129
+ throw packageRefusal("kit_migrator_binary_content", `${home}/${relative}`);
130
+ }
131
+ files[relative] = content;
132
+ }
133
+ return files;
134
+ }
135
+
136
+ async function readAttachments(root, manifest) {
137
+ const declared = new Map(
138
+ manifest.attachments.flatMap((entry) => (entry.path === null ? [] : [[entry.path, entry]])),
139
+ );
140
+ const held = new Map();
141
+ for (const relative of await listTree(join(root, MIGRATOR_ATTACHMENTS_HOME))) {
142
+ if (!declared.has(relative)) {
143
+ throw packageRefusal("kit_migrator_attachment_undeclared", relative);
144
+ }
145
+ const absolute = join(root, MIGRATOR_ATTACHMENTS_HOME, relative);
146
+ const bytes = await readFile(absolute);
147
+ held.set(relative, {
148
+ path: relative,
149
+ file: absolute,
150
+ bytes: bytes.length,
151
+ sha256: createHash("sha256").update(bytes).digest("hex"),
152
+ });
153
+ }
154
+ for (const relative of declared.keys()) {
155
+ if (!held.has(relative)) throw packageRefusal("kit_migrator_attachment_missing", relative);
156
+ }
157
+ return held;
158
+ }
159
+
160
+ export async function readMigratorPackage(directory) {
161
+ const root = resolve(directory);
162
+ let document;
163
+ try {
164
+ document = JSON.parse(await readFile(join(root, MIGRATOR_MANIFEST), "utf8"));
165
+ } catch {
166
+ throw packageRefusal("kit_migrator_manifest_unreadable", MIGRATOR_MANIFEST);
167
+ }
168
+ const manifest = parseManifest(document);
169
+ if ((await listTree(root)).some((path) => path === "content/MIGRATION.md")) {
170
+ throw packageRefusal("kit_migrator_report_reserved", "content/MIGRATION.md");
171
+ }
172
+ const content = await readTextTree(root, MIGRATOR_CONTENT_HOME);
173
+ if (Object.keys(content).length === 0) {
174
+ throw packageRefusal("kit_migrator_content_empty", MIGRATOR_CONTENT_HOME);
175
+ }
176
+ for (const relative of Object.keys(content)) {
177
+ if (relative === "types" || relative.startsWith("types/")) {
178
+ throw packageRefusal("kit_migrator_types_alias", `${MIGRATOR_CONTENT_HOME}/${relative}`);
179
+ }
180
+ if (relative.split("/").includes("_provenance")) {
181
+ throw packageRefusal(
182
+ "kit_migrator_provenance_refused",
183
+ `${MIGRATOR_CONTENT_HOME}/${relative}`,
184
+ );
185
+ }
186
+ if (!MARKDOWN.test(relative)) {
187
+ throw packageRefusal("kit_migrator_content_invalid", `${MIGRATOR_CONTENT_HOME}/${relative}`);
188
+ }
189
+ }
190
+ const model = await readTextTree(root, MIGRATOR_MODEL_HOME);
191
+ for (const relative of Object.keys(model)) {
192
+ if (relative.includes("/") || !MARKDOWN.test(relative)) {
193
+ throw packageRefusal("kit_migrator_model_invalid", `${MIGRATOR_MODEL_HOME}/${relative}`);
194
+ }
195
+ }
196
+ for (const entry of manifest.attachments) {
197
+ for (const referrer of entry.referrers) {
198
+ if (content[referrer] === undefined) {
199
+ throw packageRefusal("kit_migrator_referrer_unknown", referrer);
200
+ }
201
+ }
202
+ }
203
+ return {
204
+ root,
205
+ manifest,
206
+ model,
207
+ content,
208
+ attachments: await readAttachments(root, manifest),
209
+ };
210
+ }
211
+
212
+ export function migratorRequest(declaration, directory) {
213
+ return `${JSON.stringify({
214
+ contract: MIGRATOR_CONTRACT,
215
+ migrator: declaration.id,
216
+ package: directory,
217
+ })}\n`;
218
+ }
219
+
220
+ export async function emitMigratorPackage(declaration, options) {
221
+ const directory = resolve(options.package);
222
+ if (await lstat(directory).catch(() => null)) {
223
+ throw packageRefusal("kit_migrator_output_exists", directory);
224
+ }
225
+ let run;
226
+ try {
227
+ run = await runCommand({
228
+ command: declaration.command,
229
+ environment: options.environment,
230
+ stdin: migratorRequest(declaration, directory),
231
+ timeoutMs: declaration.timeoutMs,
232
+ outputMaxBytes: MIGRATOR_OUTPUT_MAX_BYTES,
233
+ signal: options.signal,
234
+ });
235
+ } catch (error) {
236
+ throw new KitRefusal(error instanceof KitRefusal ? error.code : "kit_migrator_failed");
237
+ }
238
+ if (run.status !== 0) throw new KitRefusal("kit_migrator_failed", `${run.status}`);
239
+ const emitted = await stat(directory).catch(() => null);
240
+ if (emitted === null || !emitted.isDirectory()) {
241
+ throw packageRefusal("kit_migrator_package_missing", directory);
242
+ }
243
+ const held = await readMigratorPackage(directory);
244
+ if (held.manifest.migrator !== declaration.id) {
245
+ throw packageRefusal("kit_migrator_identity_mismatch", declaration.id);
246
+ }
247
+ return directory;
248
+ }