@agentshouse/kit 0.1.0-alpha.9 → 0.2.1-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 (93) hide show
  1. package/README.md +14 -1740
  2. package/dist/acp.js +120 -0
  3. package/dist/agents.js +90 -0
  4. package/dist/api.js +45 -0
  5. package/dist/blocks.js +51 -0
  6. package/dist/bridge.js +124 -0
  7. package/dist/clis.js +110 -0
  8. package/dist/conversations.js +538 -0
  9. package/dist/files.js +21 -0
  10. package/dist/home.js +24 -0
  11. package/dist/house-main.js +92 -0
  12. package/dist/instructions.js +28 -0
  13. package/dist/kit-main.js +16 -0
  14. package/dist/login.js +112 -0
  15. package/dist/resident.js +66 -0
  16. package/dist/secret-input.js +16 -0
  17. package/dist/sign-in.js +83 -0
  18. package/dist/stream.js +47 -0
  19. package/package.json +34 -10
  20. package/bin/connect-linux.sh +0 -134
  21. package/bin/kit.js +0 -1023
  22. package/bootstrap.json +0 -17
  23. package/src/acp.js +0 -99
  24. package/src/adapter-mcp.js +0 -44
  25. package/src/adapter.js +0 -252
  26. package/src/agent-logins.js +0 -65
  27. package/src/agents.js +0 -504
  28. package/src/apply.js +0 -29
  29. package/src/attach.js +0 -52
  30. package/src/binding.js +0 -90
  31. package/src/blocks.js +0 -156
  32. package/src/bridge.js +0 -449
  33. package/src/bytes.js +0 -89
  34. package/src/channel.js +0 -37
  35. package/src/command.js +0 -109
  36. package/src/configuration.js +0 -465
  37. package/src/connector-login.js +0 -91
  38. package/src/connector.js +0 -107
  39. package/src/continuation.js +0 -61
  40. package/src/contract.js +0 -363
  41. package/src/conversation-originals.js +0 -143
  42. package/src/custom/authorization.js +0 -63
  43. package/src/declaration.js +0 -35
  44. package/src/delivery.js +0 -191
  45. package/src/directory.js +0 -102
  46. package/src/draft.js +0 -89
  47. package/src/google/calendar.js +0 -177
  48. package/src/google/declared.js +0 -146
  49. package/src/google/drive.js +0 -435
  50. package/src/google/gateway.js +0 -166
  51. package/src/google/gmail.js +0 -249
  52. package/src/google/id.js +0 -1
  53. package/src/google/oauth.js +0 -273
  54. package/src/google/source.js +0 -25
  55. package/src/hook.js +0 -376
  56. package/src/host.js +0 -121
  57. package/src/house-main.js +0 -79
  58. package/src/house.js +0 -1090
  59. package/src/inbound.js +0 -175
  60. package/src/instance/google.js +0 -1
  61. package/src/kit-work-stream.js +0 -84
  62. package/src/launcher-main.js +0 -19
  63. package/src/launcher.js +0 -276
  64. package/src/local-files.js +0 -113
  65. package/src/login.js +0 -274
  66. package/src/mcp.js +0 -71
  67. package/src/migrator.js +0 -248
  68. package/src/mount.js +0 -582
  69. package/src/operation.js +0 -20
  70. package/src/original.js +0 -631
  71. package/src/peer.js +0 -124
  72. package/src/plan.js +0 -57
  73. package/src/question.js +0 -316
  74. package/src/refusal.js +0 -79
  75. package/src/repository.js +0 -521
  76. package/src/session-bin/house +0 -2
  77. package/src/settlement.js +0 -57
  78. package/src/setup.js +0 -190
  79. package/src/source-originals.js +0 -116
  80. package/src/source.js +0 -286
  81. package/src/starter-set/grilling/LICENSE +0 -21
  82. package/src/starter-set/grilling/SKILL.md +0 -30
  83. package/src/starter-set/grilling/agents/openai.yaml +0 -3
  84. package/src/starter-set.js +0 -17
  85. package/src/state.js +0 -157
  86. package/src/sync-library.js +0 -293
  87. package/src/sync-state.js +0 -120
  88. package/src/sync.js +0 -482
  89. package/src/telegram.js +0 -1045
  90. package/src/terminal.js +0 -57
  91. package/src/tmux.js +0 -119
  92. package/src/worker.js +0 -925
  93. package/src/workspace.js +0 -500
package/src/channel.js DELETED
@@ -1,37 +0,0 @@
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 DELETED
@@ -1,109 +0,0 @@
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
- }
@@ -1,465 +0,0 @@
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
- }
@@ -1,91 +0,0 @@
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
- }