@agentshouse/kit 0.0.1 → 0.1.0-alpha.10

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 (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1776 -2
  3. package/bin/connect-linux.sh +173 -0
  4. package/bin/kit.js +1023 -0
  5. package/bootstrap.json +17 -0
  6. package/package.json +37 -3
  7. package/src/acp.js +99 -0
  8. package/src/adapter-mcp.js +44 -0
  9. package/src/adapter.js +252 -0
  10. package/src/agent-logins.js +65 -0
  11. package/src/agents.js +504 -0
  12. package/src/apply.js +29 -0
  13. package/src/attach.js +52 -0
  14. package/src/binding.js +90 -0
  15. package/src/blocks.js +156 -0
  16. package/src/bridge.js +449 -0
  17. package/src/bytes.js +89 -0
  18. package/src/channel.js +37 -0
  19. package/src/command.js +109 -0
  20. package/src/configuration.js +465 -0
  21. package/src/connector-login.js +91 -0
  22. package/src/connector.js +107 -0
  23. package/src/continuation.js +61 -0
  24. package/src/contract.js +363 -0
  25. package/src/conversation-originals.js +143 -0
  26. package/src/custom/authorization.js +63 -0
  27. package/src/declaration.js +35 -0
  28. package/src/delivery.js +191 -0
  29. package/src/directory.js +102 -0
  30. package/src/draft.js +89 -0
  31. package/src/google/calendar.js +177 -0
  32. package/src/google/declared.js +146 -0
  33. package/src/google/drive.js +435 -0
  34. package/src/google/gateway.js +166 -0
  35. package/src/google/gmail.js +249 -0
  36. package/src/google/id.js +1 -0
  37. package/src/google/oauth.js +273 -0
  38. package/src/google/source.js +25 -0
  39. package/src/hook.js +376 -0
  40. package/src/host.js +121 -0
  41. package/src/house-main.js +79 -0
  42. package/src/house.js +1275 -0
  43. package/src/inbound.js +175 -0
  44. package/src/instance/google.js +1 -0
  45. package/src/kit-work-stream.js +84 -0
  46. package/src/launcher-main.js +19 -0
  47. package/src/launcher.js +276 -0
  48. package/src/local-files.js +113 -0
  49. package/src/login.js +274 -0
  50. package/src/mcp.js +71 -0
  51. package/src/migrator.js +248 -0
  52. package/src/mount.js +582 -0
  53. package/src/operation.js +20 -0
  54. package/src/original.js +631 -0
  55. package/src/peer.js +124 -0
  56. package/src/plan.js +57 -0
  57. package/src/question.js +316 -0
  58. package/src/refusal.js +79 -0
  59. package/src/repository.js +521 -0
  60. package/src/session-bin/house +2 -0
  61. package/src/settlement.js +57 -0
  62. package/src/setup.js +190 -0
  63. package/src/source-originals.js +116 -0
  64. package/src/source.js +290 -0
  65. package/src/starter-set/grilling/LICENSE +21 -0
  66. package/src/starter-set/grilling/SKILL.md +30 -0
  67. package/src/starter-set/grilling/agents/openai.yaml +3 -0
  68. package/src/starter-set.js +17 -0
  69. package/src/state.js +157 -0
  70. package/src/sync-library.js +293 -0
  71. package/src/sync-state.js +120 -0
  72. package/src/sync.js +482 -0
  73. package/src/telegram.js +1061 -0
  74. package/src/terminal.js +57 -0
  75. package/src/tmux.js +119 -0
  76. package/src/worker.js +928 -0
  77. package/src/workspace.js +500 -0
package/src/channel.js ADDED
@@ -0,0 +1,37 @@
1
+ import { BRIDGE_LINE_MAX_BYTES } from "./contract.js";
2
+ import { KitRefusal } from "./refusal.js";
3
+
4
+ export function writeFrame(stream, frame) {
5
+ stream.write(`${JSON.stringify(frame)}\n`);
6
+ }
7
+
8
+ export function readFrames(stream, handlers) {
9
+ const maximum = handlers.maxBytes ?? BRIDGE_LINE_MAX_BYTES;
10
+ let buffered = "";
11
+ stream.setEncoding("utf8");
12
+ stream.on("data", (chunk) => {
13
+ buffered += chunk;
14
+ for (;;) {
15
+ const boundary = buffered.indexOf("\n");
16
+ if (boundary < 0) break;
17
+ const line = buffered.slice(0, boundary);
18
+ buffered = buffered.slice(boundary + 1);
19
+ if (line.trim() === "") continue;
20
+ let frame;
21
+ try {
22
+ frame = JSON.parse(line);
23
+ } catch {
24
+ handlers.onRefusal(new KitRefusal("kit_channel_frame_invalid"));
25
+ return;
26
+ }
27
+ if (typeof frame !== "object" || frame === null || Array.isArray(frame)) {
28
+ handlers.onRefusal(new KitRefusal("kit_channel_frame_invalid"));
29
+ return;
30
+ }
31
+ handlers.onFrame(frame);
32
+ }
33
+ if (Buffer.byteLength(buffered, "utf8") <= maximum) return;
34
+ buffered = "";
35
+ handlers.onRefusal(new KitRefusal("kit_channel_frame_exceeded"));
36
+ });
37
+ }
package/src/command.js ADDED
@@ -0,0 +1,109 @@
1
+ import { spawn } from "node:child_process";
2
+ import { readFile } from "node:fs/promises";
3
+ import { COMMAND_INPUT_MAX_BYTES } from "./contract.js";
4
+ import { KitRefusal } from "./refusal.js";
5
+
6
+ const INHERITED_ENVIRONMENT = ["PATH"];
7
+ const HOUSE_CREDENTIAL = /^(ahk_|ahl_)/;
8
+ const STDERR_KEPT_BYTES = 8 * 1024;
9
+
10
+ export function withoutHouseCredentials(environment) {
11
+ for (const [name, value] of Object.entries(environment)) {
12
+ if (HOUSE_CREDENTIAL.test(value)) throw new KitRefusal("kit_house_credential_refused", name);
13
+ }
14
+ return environment;
15
+ }
16
+
17
+ export function inheritedEnvironment(names) {
18
+ const inherited = {};
19
+ for (const name of names) {
20
+ const value = process.env[name];
21
+ if (value !== undefined) inherited[name] = value;
22
+ }
23
+ return inherited;
24
+ }
25
+
26
+ function childEnvironment(environment) {
27
+ return {
28
+ ...inheritedEnvironment(INHERITED_ENVIRONMENT),
29
+ ...withoutHouseCredentials(environment),
30
+ };
31
+ }
32
+
33
+ async function resolvedSecret(name, reference) {
34
+ if (reference.environment !== undefined) {
35
+ return process.env[reference.environment] ?? "";
36
+ }
37
+ try {
38
+ return await readFile(reference.file, "utf8");
39
+ } catch {
40
+ throw new KitRefusal("kit_secret_unresolved", name);
41
+ }
42
+ }
43
+
44
+ export async function resolveEnvironment(declaration) {
45
+ const environment = { ...declaration.environment };
46
+ for (const [name, reference] of Object.entries(declaration.secrets ?? {})) {
47
+ const value = (await resolvedSecret(name, reference)).trim();
48
+ if (value === "") throw new KitRefusal("kit_secret_unresolved", name);
49
+ environment[name] = value;
50
+ }
51
+ return environment;
52
+ }
53
+
54
+ export async function runCommand(options) {
55
+ const environment = childEnvironment(options.environment);
56
+ const stdin = Buffer.from(options.stdin, "utf8");
57
+ if (stdin.length > COMMAND_INPUT_MAX_BYTES) {
58
+ throw new KitRefusal("kit_command_input_exceeded");
59
+ }
60
+ const [program, ...args] = options.command;
61
+ return new Promise((resolve, reject) => {
62
+ const child = spawn(program, args, { env: environment, stdio: ["pipe", "pipe", "pipe"] });
63
+ const output = [];
64
+ const errors = [];
65
+ let outputBytes = 0;
66
+ let errorBytes = 0;
67
+ let refusal = null;
68
+ const abandon = (code) => {
69
+ refusal ??= code;
70
+ child.kill("SIGKILL");
71
+ };
72
+ const timer = setTimeout(() => abandon("kit_command_timeout"), options.timeoutMs);
73
+ const cancel = () => abandon("kit_command_cancelled");
74
+ if (options.signal?.aborted) abandon("kit_command_cancelled");
75
+ options.signal?.addEventListener("abort", cancel, { once: true });
76
+ const settled = () => {
77
+ clearTimeout(timer);
78
+ options.signal?.removeEventListener("abort", cancel);
79
+ };
80
+ child.stdout.on("data", (chunk) => {
81
+ outputBytes += chunk.length;
82
+ if (options.outputMaxBytes !== undefined && outputBytes > options.outputMaxBytes) {
83
+ abandon("kit_command_output_exceeded");
84
+ return;
85
+ }
86
+ output.push(chunk);
87
+ });
88
+ child.stderr.on("data", (chunk) => {
89
+ if (errorBytes >= STDERR_KEPT_BYTES) return;
90
+ errorBytes += chunk.length;
91
+ errors.push(chunk);
92
+ });
93
+ child.on("error", () => {
94
+ settled();
95
+ reject(new KitRefusal("kit_command_unavailable", program));
96
+ });
97
+ child.on("close", (status) => {
98
+ settled();
99
+ const stderr = Buffer.concat(errors).toString("utf8").slice(0, STDERR_KEPT_BYTES);
100
+ if (refusal !== null) {
101
+ reject(new KitRefusal(refusal, stderr.trim() === "" ? null : stderr.trim()));
102
+ return;
103
+ }
104
+ resolve({ status, stdout: Buffer.concat(output).toString("utf8"), stderr });
105
+ });
106
+ child.stdin.on("error", () => undefined);
107
+ child.stdin.end(stdin);
108
+ });
109
+ }
@@ -0,0 +1,465 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import {
3
+ CONNECTOR_TIMEOUT_MS,
4
+ DECLARED_ID,
5
+ EXECUTION_AUTHORITIES,
6
+ MIGRATOR_CATEGORY,
7
+ MIGRATOR_TIMEOUT_MS,
8
+ } from "./contract.js";
9
+ import { declarationReader } from "./declaration.js";
10
+ import { GOOGLE_DRIVE_ID } from "./google/id.js";
11
+ import { KitRefusal } from "./refusal.js";
12
+
13
+ const PRESETS = new Set(["custom", "google"]);
14
+ const AGENT_CLIENTS = new Set(["claude-code"]);
15
+ const GOOGLE_CURATED_SERVICES = new Set(["gmail", "calendar", "drive"]);
16
+ const ENVIRONMENT_VALUE_MAX = 4096;
17
+ const GOOGLE_API_HOST = /^[a-z0-9-]+(?:\.[a-z0-9-]+)*\.googleapis\.com$/;
18
+ const GOOGLE_API_PATH = /^\/[A-Za-z0-9._~!$&'()*+,;=:@%/-]+$/;
19
+ const GOOGLE_API_SCOPE = /^https:\/\/www\.googleapis\.com\/auth\/[A-Za-z0-9._-]+$/;
20
+ const EXTRACTION_PATH = /^(?:[A-Za-z_][A-Za-z0-9_-]*|\d+)(?:\.(?:[A-Za-z_][A-Za-z0-9_-]*|\d+))*$/;
21
+ const QUERY_NAME = /^[A-Za-z][A-Za-z0-9_.-]{0,127}$/;
22
+
23
+ const { refuse, plain, closed, text, positiveCount, filled } = declarationReader(
24
+ "kit_configuration_invalid",
25
+ );
26
+
27
+ function declaredId(value, declaration) {
28
+ if (typeof value !== "string" || !DECLARED_ID.test(value)) refuse(declaration);
29
+ return value;
30
+ }
31
+
32
+ function environmentOf(value, declaration) {
33
+ if (value === undefined) return {};
34
+ const environment = {};
35
+ for (const [name, entry] of Object.entries(plain(value, declaration))) {
36
+ if (typeof entry !== "string" || entry.length > ENVIRONMENT_VALUE_MAX) {
37
+ refuse(`${declaration}.${name}`);
38
+ }
39
+ environment[name] = entry;
40
+ }
41
+ return environment;
42
+ }
43
+
44
+ function secretReference(value, declaration) {
45
+ const reference = closed(value, ["environment", "file"], declaration);
46
+ const named = Object.keys(reference);
47
+ if (named.length !== 1) refuse(declaration);
48
+ return { [named[0]]: text(reference[named[0]], declaration, 4096) };
49
+ }
50
+
51
+ function secretsOf(value, declaration, environment) {
52
+ if (value === undefined) return {};
53
+ const secrets = {};
54
+ for (const [name, entry] of Object.entries(plain(value, declaration))) {
55
+ if (name in environment) refuse(`${declaration}.${name}`);
56
+ secrets[name] = secretReference(entry, `${declaration}.${name}`);
57
+ }
58
+ return secrets;
59
+ }
60
+
61
+ function connectorOf(value, declaration) {
62
+ if (value === undefined) refuse(declaration);
63
+ const connector = closed(
64
+ value,
65
+ ["command", "timeout_ms", "environment", "secrets"],
66
+ declaration,
67
+ );
68
+ const command = filled(connector.command, `${declaration}.command`);
69
+ const environment = environmentOf(connector.environment, `${declaration}.environment`);
70
+ return {
71
+ command: command.map((argument, position) =>
72
+ text(argument, `${declaration}.command[${position}]`, 4096),
73
+ ),
74
+ timeoutMs:
75
+ connector.timeout_ms === undefined
76
+ ? CONNECTOR_TIMEOUT_MS
77
+ : positiveCount(connector.timeout_ms, `${declaration}.timeout_ms`, CONNECTOR_TIMEOUT_MS),
78
+ environment,
79
+ secrets: secretsOf(connector.secrets, `${declaration}.secrets`, environment),
80
+ };
81
+ }
82
+
83
+ function migratorOf(value, declaration) {
84
+ const declared = closed(
85
+ value,
86
+ ["id", "category", "command", "timeout_ms", "environment", "secrets"],
87
+ declaration,
88
+ );
89
+ if (declared.category !== MIGRATOR_CATEGORY) refuse(`${declaration}.category`);
90
+ const command = filled(declared.command, `${declaration}.command`);
91
+ const environment = environmentOf(declared.environment, `${declaration}.environment`);
92
+ return {
93
+ id: declaredId(declared.id, `${declaration}.id`),
94
+ category: declared.category,
95
+ command: command.map((argument, position) =>
96
+ text(argument, `${declaration}.command[${position}]`, 4096),
97
+ ),
98
+ timeoutMs:
99
+ declared.timeout_ms === undefined
100
+ ? MIGRATOR_TIMEOUT_MS
101
+ : positiveCount(declared.timeout_ms, `${declaration}.timeout_ms`, MIGRATOR_TIMEOUT_MS),
102
+ environment,
103
+ secrets: secretsOf(declared.secrets, `${declaration}.secrets`, environment),
104
+ };
105
+ }
106
+
107
+ function backfillOf(value, declaration) {
108
+ if (value === undefined) refuse(declaration);
109
+ const backfill = closed(value, ["days", "count", "all"], declaration);
110
+ const named = Object.keys(backfill);
111
+ if (named.length !== 1) refuse(declaration);
112
+ if (named[0] === "all") {
113
+ if (backfill.all !== true) refuse(declaration);
114
+ return { kind: "all" };
115
+ }
116
+ return { kind: named[0], bound: positiveCount(backfill[named[0]], declaration) };
117
+ }
118
+
119
+ function textList(value, declaration) {
120
+ return filled(value, declaration).map((entry, position) =>
121
+ text(entry, `${declaration}[${position}]`, 512),
122
+ );
123
+ }
124
+
125
+ function textArray(value, declaration) {
126
+ if (!Array.isArray(value)) refuse(declaration);
127
+ return value.map((entry, position) => text(entry, `${declaration}[${position}]`, 512));
128
+ }
129
+
130
+ function googleSelectionOf(value, declaration, service) {
131
+ if (service === "gmail") {
132
+ const selection = closed(value, ["labels"], declaration);
133
+ return { labels: textList(selection.labels, `${declaration}.labels`) };
134
+ }
135
+ if (service === "calendar") {
136
+ const selection = closed(value, ["calendars"], declaration);
137
+ return { calendars: textList(selection.calendars, `${declaration}.calendars`) };
138
+ }
139
+ if (service === "drive") {
140
+ const selection = closed(value, ["files", "folders"], declaration);
141
+ const files =
142
+ selection.files === undefined ? [] : textArray(selection.files, `${declaration}.files`);
143
+ const folders =
144
+ selection.folders === undefined ? [] : textArray(selection.folders, `${declaration}.folders`);
145
+ if (files.length + folders.length === 0) refuse(declaration);
146
+ files.forEach((id, position) => {
147
+ if (!GOOGLE_DRIVE_ID.test(id)) refuse(`${declaration}.files[${position}]`);
148
+ });
149
+ folders.forEach((id, position) => {
150
+ if (!GOOGLE_DRIVE_ID.test(id)) refuse(`${declaration}.folders[${position}]`);
151
+ });
152
+ return { files, folders };
153
+ }
154
+ refuse(declaration);
155
+ }
156
+
157
+ function extractionPath(value, declaration) {
158
+ const path = text(value, declaration, 512);
159
+ if (!EXTRACTION_PATH.test(path)) refuse(declaration);
160
+ return path;
161
+ }
162
+
163
+ function queryOf(value, declaration) {
164
+ const query = plain(value, declaration);
165
+ const parsed = {};
166
+ for (const [name, entry] of Object.entries(query)) {
167
+ if (!QUERY_NAME.test(name) || typeof entry !== "string" || entry.length > 2048) {
168
+ refuse(`${declaration}.${name}`);
169
+ }
170
+ parsed[name] = entry;
171
+ }
172
+ return parsed;
173
+ }
174
+
175
+ function declaredServiceOf(value, declaration) {
176
+ const declared = closed(
177
+ value,
178
+ ["host", "path", "query", "page_size", "page_token", "items", "fields", "scopes"],
179
+ declaration,
180
+ );
181
+ const host = text(declared.host, `${declaration}.host`, 253);
182
+ if (!GOOGLE_API_HOST.test(host)) refuse(`${declaration}.host`);
183
+ const path = text(declared.path, `${declaration}.path`, 2048);
184
+ if (!GOOGLE_API_PATH.test(path) || path.includes("..") || path.includes("//")) {
185
+ refuse(`${declaration}.path`);
186
+ }
187
+ const tokens = closed(
188
+ declared.page_token,
189
+ ["request", "response", "checkpoint_request", "checkpoint_response"],
190
+ `${declaration}.page_token`,
191
+ );
192
+ const pageToken = {
193
+ request: text(tokens.request, `${declaration}.page_token.request`, 128),
194
+ response: extractionPath(tokens.response, `${declaration}.page_token.response`),
195
+ checkpointRequest: text(
196
+ tokens.checkpoint_request,
197
+ `${declaration}.page_token.checkpoint_request`,
198
+ 128,
199
+ ),
200
+ checkpointResponse: extractionPath(
201
+ tokens.checkpoint_response,
202
+ `${declaration}.page_token.checkpoint_response`,
203
+ ),
204
+ };
205
+ if (
206
+ !QUERY_NAME.test(pageToken.request) ||
207
+ !QUERY_NAME.test(pageToken.checkpointRequest) ||
208
+ pageToken.request === pageToken.checkpointRequest
209
+ ) {
210
+ refuse(`${declaration}.page_token`);
211
+ }
212
+ const fields = closed(
213
+ declared.fields,
214
+ [
215
+ "native_id",
216
+ "native_version",
217
+ "provider_modified_at",
218
+ "observed_at",
219
+ "body",
220
+ "container",
221
+ "author",
222
+ "url",
223
+ ],
224
+ `${declaration}.fields`,
225
+ );
226
+ const pageSize = text(declared.page_size, `${declaration}.page_size`, 128);
227
+ if (!QUERY_NAME.test(pageSize)) refuse(`${declaration}.page_size`);
228
+ const scopes = textList(declared.scopes, `${declaration}.scopes`).map((scope, position) => {
229
+ if (!GOOGLE_API_SCOPE.test(scope)) refuse(`${declaration}.scopes[${position}]`);
230
+ return scope;
231
+ });
232
+ return {
233
+ host,
234
+ path,
235
+ query: queryOf(declared.query, `${declaration}.query`),
236
+ pageSize,
237
+ scopes: [...new Set(scopes)],
238
+ pageToken,
239
+ items: extractionPath(declared.items, `${declaration}.items`),
240
+ fields: {
241
+ nativeId: extractionPath(fields.native_id, `${declaration}.fields.native_id`),
242
+ nativeVersion:
243
+ fields.native_version === undefined
244
+ ? null
245
+ : extractionPath(fields.native_version, `${declaration}.fields.native_version`),
246
+ providerModifiedAt:
247
+ fields.provider_modified_at === undefined
248
+ ? null
249
+ : extractionPath(
250
+ fields.provider_modified_at,
251
+ `${declaration}.fields.provider_modified_at`,
252
+ ),
253
+ observedAt: extractionPath(fields.observed_at, `${declaration}.fields.observed_at`),
254
+ body: textList(fields.body, `${declaration}.fields.body`).map((path, position) =>
255
+ extractionPath(path, `${declaration}.fields.body[${position}]`),
256
+ ),
257
+ container: extractionPath(fields.container, `${declaration}.fields.container`),
258
+ author:
259
+ fields.author === undefined
260
+ ? null
261
+ : extractionPath(fields.author, `${declaration}.fields.author`),
262
+ url:
263
+ fields.url === undefined ? null : extractionPath(fields.url, `${declaration}.fields.url`),
264
+ },
265
+ };
266
+ }
267
+
268
+ function serviceOf(value, declaration, preset) {
269
+ const service = closed(
270
+ value,
271
+ ["service", "source_ref", "rooms", "backfill", "connector", "selection", "declaration"],
272
+ declaration,
273
+ );
274
+ const name = declaredId(service.service, `${declaration}.service`);
275
+ if (preset === "custom" && service.connector === undefined) refuse(`${declaration}.connector`);
276
+ if (preset === "custom" && service.selection !== undefined) refuse(`${declaration}.selection`);
277
+ if (preset === "custom" && service.declaration !== undefined) refuse(`${declaration}.declaration`);
278
+ if (preset === "google" && service.connector !== undefined) refuse(`${declaration}.connector`);
279
+ if (preset === "google" && GOOGLE_CURATED_SERVICES.has(name) && service.declaration !== undefined) {
280
+ refuse(`${declaration}.declaration`);
281
+ }
282
+ if (preset === "google" && !GOOGLE_CURATED_SERVICES.has(name)) {
283
+ if (service.selection !== undefined) refuse(`${declaration}.selection`);
284
+ if (service.declaration === undefined) refuse(`${declaration}.declaration`);
285
+ }
286
+ return {
287
+ service: name,
288
+ sourceRef: text(service.source_ref, `${declaration}.source_ref`, 512),
289
+ rooms: filled(service.rooms, `${declaration}.rooms`).map((room, position) =>
290
+ text(room, `${declaration}.rooms[${position}]`, 512),
291
+ ),
292
+ backfill: backfillOf(service.backfill, `${declaration}.backfill`),
293
+ selection:
294
+ preset === "google" && GOOGLE_CURATED_SERVICES.has(name)
295
+ ? googleSelectionOf(service.selection, `${declaration}.selection`, name)
296
+ : null,
297
+ connector: preset === "custom" ? connectorOf(service.connector, `${declaration}.connector`) : null,
298
+ declaration:
299
+ preset === "google" && !GOOGLE_CURATED_SERVICES.has(name)
300
+ ? declaredServiceOf(service.declaration, `${declaration}.declaration`)
301
+ : null,
302
+ };
303
+ }
304
+
305
+ function instanceOf(value, declaration) {
306
+ const instance = closed(value, ["id", "preset", "services", "scopes"], declaration);
307
+ const preset = declaredId(instance.preset, `${declaration}.preset`);
308
+ if (!PRESETS.has(preset)) refuse(`${declaration}.preset`);
309
+ if (preset !== "google" && instance.scopes !== undefined) refuse(`${declaration}.scopes`);
310
+ const scopes =
311
+ instance.scopes === undefined
312
+ ? []
313
+ : textList(instance.scopes, `${declaration}.scopes`).map((scope, position) => {
314
+ if (!GOOGLE_API_SCOPE.test(scope)) refuse(`${declaration}.scopes[${position}]`);
315
+ return scope;
316
+ });
317
+ const services = filled(instance.services, `${declaration}.services`).map((service, position) =>
318
+ serviceOf(service, `${declaration}.services[${position}]`, preset),
319
+ );
320
+ services.forEach((service, position) => {
321
+ const first = services.findIndex((candidate) => candidate.service === service.service);
322
+ if (first !== position) refuse(`${declaration}.services[${position}].service`);
323
+ });
324
+ services.forEach((service, position) => {
325
+ if (
326
+ service.declaration !== null &&
327
+ service.declaration.scopes.some((scope) => !scopes.includes(scope))
328
+ ) {
329
+ refuse(`${declaration}.services[${position}].declaration.scopes`);
330
+ }
331
+ });
332
+ return {
333
+ id: declaredId(instance.id, `${declaration}.id`),
334
+ preset,
335
+ services,
336
+ ...(preset === "google" ? { scopes: [...new Set(scopes)] } : {}),
337
+ };
338
+ }
339
+
340
+ function absolutePath(value, declaration) {
341
+ const path = text(value, declaration, 2048);
342
+ if (!path.startsWith("/")) refuse(declaration);
343
+ return path;
344
+ }
345
+
346
+ function ceilingOf(value, declaration) {
347
+ return value === undefined ? null : positiveCount(value, declaration);
348
+ }
349
+
350
+ function maximaOf(value, declaration) {
351
+ if (value === null) return null;
352
+ const maxima = closed(value, ["cpu_millicores", "memory_bytes", "disk_bytes"], declaration);
353
+ if (Object.keys(maxima).length === 0) refuse(declaration);
354
+ return {
355
+ cpuMillicores: ceilingOf(maxima.cpu_millicores, `${declaration}.cpu_millicores`),
356
+ memoryBytes: ceilingOf(maxima.memory_bytes, `${declaration}.memory_bytes`),
357
+ diskBytes: ceilingOf(maxima.disk_bytes, `${declaration}.disk_bytes`),
358
+ };
359
+ }
360
+
361
+ function hostOf(value, declaration) {
362
+ if (value === null) return null;
363
+ const host = closed(value, ["workspace", "maxima", "image"], declaration);
364
+ if (host.maxima === undefined) refuse(`${declaration}.maxima`);
365
+ return {
366
+ workspace: absolutePath(host.workspace, `${declaration}.workspace`),
367
+ maxima: maximaOf(host.maxima, `${declaration}.maxima`),
368
+ image: host.image === undefined || host.image === null ? null : text(host.image, `${declaration}.image`, 255),
369
+ };
370
+ }
371
+
372
+ function clientOf(value, declaration) {
373
+ const client = closed(value, ["kind", "settings"], declaration);
374
+ const kind = text(client.kind, `${declaration}.kind`, 64);
375
+ if (!AGENT_CLIENTS.has(kind)) refuse(`${declaration}.kind`);
376
+ return { kind, settings: absolutePath(client.settings, `${declaration}.settings`) };
377
+ }
378
+
379
+ export const AVAILABILITY_MODES = ["intermittent", "continuous"];
380
+
381
+ export function parseConfiguration(document) {
382
+ const declared = closed(
383
+ document,
384
+ [
385
+ "availability",
386
+ "execution_authority",
387
+ "instances",
388
+ "migrators",
389
+ "clients",
390
+ "host",
391
+ ],
392
+ "configuration",
393
+ );
394
+ if (!AVAILABILITY_MODES.includes(declared.availability)) {
395
+ refuse("availability");
396
+ }
397
+ if (!EXECUTION_AUTHORITIES.includes(declared.execution_authority)) {
398
+ refuse("execution_authority");
399
+ }
400
+ if (!Array.isArray(declared.instances)) refuse("instances");
401
+ if (!Array.isArray(declared.migrators)) refuse("migrators");
402
+ const instances = declared.instances.map((instance, position) =>
403
+ instanceOf(instance, `instances[${position}]`),
404
+ );
405
+ instances.forEach((instance, position) => {
406
+ const first = instances.findIndex((candidate) => candidate.id === instance.id);
407
+ if (first !== position) refuse(`instances[${position}].id`);
408
+ });
409
+ const migrators = declared.migrators.map((migrator, position) =>
410
+ migratorOf(migrator, `migrators[${position}]`),
411
+ );
412
+ migrators.forEach((migrator, position) => {
413
+ const first = migrators.findIndex((candidate) => candidate.id === migrator.id);
414
+ if (first !== position) refuse(`migrators[${position}].id`);
415
+ });
416
+ if (!Array.isArray(declared.clients)) refuse("clients");
417
+ const clients = declared.clients.map((client, position) =>
418
+ clientOf(client, `clients[${position}]`),
419
+ );
420
+ clients.forEach((client, position) => {
421
+ const first = clients.findIndex((candidate) => candidate.settings === client.settings);
422
+ if (first !== position) refuse(`clients[${position}].settings`);
423
+ });
424
+ return {
425
+ availability: declared.availability,
426
+ executionAuthority: declared.execution_authority,
427
+ instances,
428
+ migrators,
429
+ clients,
430
+ host: hostOf(declared.host, "host"),
431
+ };
432
+ }
433
+
434
+ export async function readConfiguration(path) {
435
+ let document;
436
+ try {
437
+ document = JSON.parse(await readFile(path, "utf8"));
438
+ } catch {
439
+ throw new KitRefusal("kit_configuration_invalid", path);
440
+ }
441
+ return parseConfiguration(document);
442
+ }
443
+
444
+ export function appliedDocument(configuration) {
445
+ return { availability: configuration.availability };
446
+ }
447
+
448
+ export function sourceInstance(configuration, id) {
449
+ const instance = configuration.instances.find((candidate) => candidate.id === id);
450
+ if (instance === undefined) throw new KitRefusal("kit_instance_unknown", id);
451
+ return instance;
452
+ }
453
+
454
+ export function declaredMigrator(configuration, id) {
455
+ const migrator = configuration.migrators.find((candidate) => candidate.id === id);
456
+ if (migrator === undefined) throw new KitRefusal("kit_migrator_undeclared", id);
457
+ return migrator;
458
+ }
459
+
460
+ export function declaredServices(instance, service) {
461
+ if (service === null) return instance.services;
462
+ const named = instance.services.find((candidate) => candidate.service === service);
463
+ if (named === undefined) throw new KitRefusal("kit_service_unknown", service);
464
+ return [named];
465
+ }
@@ -0,0 +1,91 @@
1
+ import { readConfiguration, sourceInstance } from "./configuration.js";
2
+ import { authorizeCustom, customAuthorizationStatus } from "./custom/authorization.js";
3
+ import { googleAuthorization, authorizeGoogle, GOOGLE_SCOPES } from "./google/oauth.js";
4
+ import { KitRefusal } from "./refusal.js";
5
+
6
+ function googleServiceScopes(service) {
7
+ const curated = GOOGLE_SCOPES[service.service];
8
+ return curated === undefined ? service.declaration.scopes : [curated];
9
+ }
10
+
11
+ function googleServiceAuthorized(authorization, scopes) {
12
+ return authorization.grants.some((grant) =>
13
+ scopes.every((scope) => grant.scopes.includes(scope)),
14
+ );
15
+ }
16
+
17
+ function googleConsent(service, context) {
18
+ return async ({ url }) => {
19
+ const collected = await context.present(
20
+ {
21
+ steps: [
22
+ { kind: "visit", label: `Sign in to Google and allow access for ${service.service}`, url },
23
+ {
24
+ kind: "collect",
25
+ label: "The address your browser ended on",
26
+ name: "address",
27
+ description:
28
+ "After you allow access, Google sends your browser to a 127.0.0.1 address that does not open. Copy that whole address from the address bar and paste it here.",
29
+ },
30
+ ],
31
+ },
32
+ context.signal,
33
+ );
34
+ return collected.address;
35
+ };
36
+ }
37
+
38
+ const CONNECTOR_LOGINS = {
39
+ custom: {
40
+ status: customAuthorizationStatus,
41
+ login: authorizeCustom,
42
+ },
43
+ google: {
44
+ status: async (instance, context) => {
45
+ const authorization = await googleAuthorization(context.directory, instance.id);
46
+ if (authorization === null) return false;
47
+ return instance.services.every((service) =>
48
+ googleServiceAuthorized(authorization, googleServiceScopes(service)),
49
+ );
50
+ },
51
+ guided: async (instance, context) => {
52
+ for (const service of instance.services) {
53
+ try {
54
+ await authorizeGoogle(
55
+ {
56
+ directory: context.directory,
57
+ instance: instance.id,
58
+ client: context.dependencies.googleOAuthClient(),
59
+ scopes: googleServiceScopes(service),
60
+ approve: googleConsent(service, context),
61
+ },
62
+ context.dependencies.googleDependencies ?? {},
63
+ );
64
+ } catch (error) {
65
+ if (context.signal.aborted) return "cancelled";
66
+ if (error instanceof KitRefusal && error.code === "google_authorization_declined") {
67
+ return "cancelled";
68
+ }
69
+ return "failed";
70
+ }
71
+ }
72
+ return "ready";
73
+ },
74
+ },
75
+ };
76
+
77
+ export function connectorLogin(instance, dependencies = {}) {
78
+ return { ...CONNECTOR_LOGINS, ...(dependencies.connectorLogins ?? {}) }[instance.preset];
79
+ }
80
+
81
+ export async function signInConnector(options, dependencies = {}) {
82
+ const configuration = await readConfiguration(options.configuration);
83
+ const instance = sourceInstance(configuration, options.instance);
84
+ const gateway = connectorLogin(instance, dependencies);
85
+ if (gateway.login === undefined) {
86
+ throw new KitRefusal("kit_connector_login_guided", `${instance.id} signs in through House setup`);
87
+ }
88
+ const context = { directory: options.directory, signal: options.signal };
89
+ if (await gateway.status(instance, context)) return "skipped";
90
+ return (await gateway.login(instance, context)) ? "ready" : "needs_attention";
91
+ }