@agentshouse/kit 0.1.0-alpha.4 → 0.1.0-alpha.40

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 (150) hide show
  1. package/README.md +41 -1671
  2. package/bin/connect-linux.sh +501 -53
  3. package/bin/connect-windows.ps1 +430 -0
  4. package/bin/kit.js +200 -159
  5. package/bootstrap.json +28 -0
  6. package/docs/configuration.md +104 -0
  7. package/docs/google.md +33 -0
  8. package/docs/linux.md +77 -0
  9. package/docs/macos.md +26 -0
  10. package/docs/telegram.md +29 -0
  11. package/docs/windows.md +36 -0
  12. package/package.json +7 -2
  13. package/src/adapter-mcp.js +92 -12
  14. package/src/agents.js +63 -8
  15. package/src/binding.js +52 -0
  16. package/src/blocks.js +2 -26
  17. package/src/bridge.js +28 -14
  18. package/src/bytes.js +19 -8
  19. package/src/configuration.js +14 -211
  20. package/src/connector-login.js +3 -84
  21. package/src/contract.js +44 -12
  22. package/src/conversation-originals.js +1 -3
  23. package/src/delivery.js +4 -7
  24. package/src/directory.js +6 -26
  25. package/src/draft.js +4 -82
  26. package/src/git-main.js +138 -0
  27. package/src/git.js +62 -0
  28. package/src/google/calendar.js +39 -45
  29. package/src/google/drive.js +61 -186
  30. package/src/google/entries.js +349 -0
  31. package/src/google/gateway.js +3 -6
  32. package/src/google/gmail.js +33 -23
  33. package/src/google/oauth.js +106 -66
  34. package/src/google/source.js +21 -23
  35. package/src/host.js +6 -2
  36. package/src/house-main.js +65 -8
  37. package/src/house.js +446 -127
  38. package/src/how-we-work.md +25 -0
  39. package/src/inbound.js +2 -18
  40. package/src/kit-work-stream.js +120 -0
  41. package/src/launcher.js +46 -10
  42. package/src/local-files.js +3 -11
  43. package/src/login.js +27 -14
  44. package/src/mcp.js +18 -0
  45. package/src/mount.js +9 -18
  46. package/src/original.js +4 -4
  47. package/src/pacing.js +41 -0
  48. package/src/plan.js +37 -45
  49. package/src/repository.js +3 -61
  50. package/src/session-bin/git +2 -0
  51. package/src/setup.js +14 -15
  52. package/src/skill-set/ask-matt/PHASE-BOUNDARIES.md +55 -0
  53. package/src/skill-set/ask-matt/SKILL.md +91 -0
  54. package/src/skill-set/ask-matt/agents/openai.yaml +4 -0
  55. package/src/skill-set/code-review/LICENSE +21 -0
  56. package/src/skill-set/code-review/SKILL.md +89 -0
  57. package/src/skill-set/code-review/agents/openai.yaml +3 -0
  58. package/src/skill-set/codebase-design/DEEPENING.md +37 -0
  59. package/src/skill-set/codebase-design/DESIGN-IT-TWICE.md +44 -0
  60. package/src/skill-set/codebase-design/LICENSE +21 -0
  61. package/src/skill-set/codebase-design/SKILL.md +116 -0
  62. package/src/skill-set/codebase-design/agents/openai.yaml +3 -0
  63. package/src/skill-set/diagnosing-bugs/LICENSE +21 -0
  64. package/src/skill-set/diagnosing-bugs/SKILL.md +140 -0
  65. package/src/skill-set/diagnosing-bugs/agents/openai.yaml +3 -0
  66. package/src/skill-set/diagnosing-bugs/scripts/hitl-loop.template.sh +44 -0
  67. package/src/skill-set/domain-modeling/ADR-FORMAT.md +47 -0
  68. package/src/skill-set/domain-modeling/CONTEXT-FORMAT.md +60 -0
  69. package/src/skill-set/domain-modeling/LICENSE +21 -0
  70. package/src/skill-set/domain-modeling/SKILL.md +76 -0
  71. package/src/skill-set/domain-modeling/agents/openai.yaml +3 -0
  72. package/src/skill-set/grill-me/LICENSE +21 -0
  73. package/src/skill-set/grill-me/SKILL.md +8 -0
  74. package/src/skill-set/grill-me/agents/openai.yaml +4 -0
  75. package/src/skill-set/grill-with-docs/LICENSE +21 -0
  76. package/src/skill-set/grill-with-docs/SKILL.md +8 -0
  77. package/src/skill-set/grill-with-docs/agents/openai.yaml +4 -0
  78. package/src/skill-set/grilling/LICENSE +21 -0
  79. package/src/{starter-set → skill-set}/grilling/SKILL.md +1 -1
  80. package/src/skill-set/handoff/LICENSE +21 -0
  81. package/src/skill-set/handoff/SKILL.md +17 -0
  82. package/src/skill-set/handoff/agents/openai.yaml +4 -0
  83. package/src/skill-set/implement/LICENSE +21 -0
  84. package/src/skill-set/implement/SKILL.md +16 -0
  85. package/src/skill-set/implement/agents/openai.yaml +4 -0
  86. package/src/skill-set/improve-codebase-architecture/HTML-REPORT.md +123 -0
  87. package/src/skill-set/improve-codebase-architecture/LICENSE +21 -0
  88. package/src/skill-set/improve-codebase-architecture/SKILL.md +72 -0
  89. package/src/skill-set/improve-codebase-architecture/agents/openai.yaml +4 -0
  90. package/src/skill-set/prototype/LICENSE +21 -0
  91. package/src/skill-set/prototype/LOGIC.md +67 -0
  92. package/src/skill-set/prototype/SKILL.md +28 -0
  93. package/src/skill-set/prototype/UI.md +112 -0
  94. package/src/skill-set/prototype/agents/openai.yaml +3 -0
  95. package/src/skill-set/research/LICENSE +21 -0
  96. package/src/skill-set/research/SKILL.md +14 -0
  97. package/src/skill-set/research/agents/openai.yaml +3 -0
  98. package/src/skill-set/resolving-merge-conflicts/LICENSE +21 -0
  99. package/src/skill-set/resolving-merge-conflicts/SKILL.md +16 -0
  100. package/src/skill-set/resolving-merge-conflicts/agents/openai.yaml +3 -0
  101. package/src/skill-set/tdd/LICENSE +21 -0
  102. package/src/skill-set/tdd/SKILL.md +40 -0
  103. package/src/skill-set/tdd/agents/openai.yaml +3 -0
  104. package/src/skill-set/tdd/mocking.md +59 -0
  105. package/src/skill-set/tdd/tests.md +77 -0
  106. package/src/skill-set/teach/GLOSSARY-FORMAT.md +35 -0
  107. package/src/skill-set/teach/LEARNING-RECORD-FORMAT.md +46 -0
  108. package/src/skill-set/teach/LICENSE +21 -0
  109. package/src/skill-set/teach/MISSION-FORMAT.md +31 -0
  110. package/src/skill-set/teach/RESOURCES-FORMAT.md +32 -0
  111. package/src/skill-set/teach/SKILL.md +141 -0
  112. package/src/skill-set/teach/agents/openai.yaml +4 -0
  113. package/src/skill-set/to-questionnaire/LICENSE +21 -0
  114. package/src/skill-set/to-questionnaire/SKILL.md +55 -0
  115. package/src/skill-set/to-questionnaire/agents/openai.yaml +4 -0
  116. package/src/skill-set/to-spec/LICENSE +21 -0
  117. package/src/skill-set/to-spec/SKILL.md +76 -0
  118. package/src/skill-set/to-spec/agents/openai.yaml +4 -0
  119. package/src/skill-set/to-tickets/LICENSE +21 -0
  120. package/src/skill-set/to-tickets/SKILL.md +106 -0
  121. package/src/skill-set/to-tickets/agents/openai.yaml +4 -0
  122. package/src/skill-set/wait-what/LICENSE +21 -0
  123. package/src/skill-set/wait-what/SKILL.md +8 -0
  124. package/src/skill-set/wait-what/agents/openai.yaml +4 -0
  125. package/src/skill-set/wayfinder/LICENSE +21 -0
  126. package/src/skill-set/wayfinder/SKILL.md +129 -0
  127. package/src/skill-set/wayfinder/agents/openai.yaml +4 -0
  128. package/src/skill-set/writing-for-agents/LICENSE +21 -0
  129. package/src/skill-set/writing-for-agents/SKILL-MECHANICS.md +22 -0
  130. package/src/skill-set/writing-for-agents/SKILL.md +83 -0
  131. package/src/skill-set/writing-for-agents/agents/openai.yaml +3 -0
  132. package/src/skill-set.js +85 -0
  133. package/src/source.js +93 -54
  134. package/src/state.js +19 -1
  135. package/src/sync-library.js +7 -11
  136. package/src/sync-state.js +12 -3
  137. package/src/sync.js +142 -43
  138. package/src/tasks.js +92 -0
  139. package/src/telegram-view.js +217 -0
  140. package/src/telegram.js +204 -69
  141. package/src/turn-frames.js +69 -0
  142. package/src/worker.js +738 -189
  143. package/src/working-copy.js +1274 -0
  144. package/src/workspace.js +299 -88
  145. package/src/google/declared.js +0 -146
  146. package/src/google/id.js +0 -1
  147. package/src/instance/google.js +0 -1
  148. package/src/starter-set.js +0 -17
  149. /package/src/{starter-set/grilling → skill-set/ask-matt}/LICENSE +0 -0
  150. /package/src/{starter-set → skill-set}/grilling/agents/openai.yaml +0 -0
package/src/blocks.js CHANGED
@@ -1,8 +1,4 @@
1
- import {
2
- ASSISTANT_BLOCKS_MAX,
3
- ASSISTANT_TABLE_COLUMNS_MAX,
4
- ASSISTANT_TEXT_MAX_BYTES,
5
- } from "./contract.js";
1
+ import { ASSISTANT_TABLE_COLUMNS_MAX } from "./contract.js";
6
2
 
7
3
  const HEADING = /^(#{1,6})\s+(.+)$/;
8
4
  const BULLET = /^ {0,3}[-*+]\s+(.+)$/;
@@ -132,25 +128,5 @@ export function assistantBlocks(text) {
132
128
  index += 1;
133
129
  }
134
130
  endParagraph();
135
- if (blocks.length === 0) return null;
136
- if (blocks.length <= ASSISTANT_BLOCKS_MAX) return blocks;
137
- const whole = text.trim();
138
- return whole === "" ? null : [{ type: "paragraph", text: whole }];
139
- }
140
-
141
- export function blocksText(blocks) {
142
- return blocks
143
- .map((block) => {
144
- if (block.type === "formula") return block.expression;
145
- if (block.type === "list") return block.items.join("\n");
146
- if (block.type === "table") {
147
- return [block.headers, ...block.rows].map((row) => row.join("\t")).join("\n");
148
- }
149
- return block.text;
150
- })
151
- .join("\n\n");
152
- }
153
-
154
- export function withinAssistantBound(text) {
155
- return Buffer.byteLength(text, "utf8") <= ASSISTANT_TEXT_MAX_BYTES;
131
+ return blocks.length === 0 ? null : blocks;
156
132
  }
package/src/bridge.js CHANGED
@@ -16,9 +16,14 @@ import {
16
16
  TURN_OUTCOMES,
17
17
  } from "./contract.js";
18
18
  import { operationId } from "./operation.js";
19
+ import { planSnapshot } from "./plan.js";
19
20
  import { houseFailed } from "./refusal.js";
20
21
 
21
- function bridgedFrame(request, operation) {
22
+ function sequenced(value) {
23
+ return Number.isSafeInteger(value) && value > 0;
24
+ }
25
+
26
+ export function bridgedFrame(request, operation) {
22
27
  if (typeof request.method !== "string") return request;
23
28
  const params = typeof request.params === "object" && request.params !== null ? request.params : {};
24
29
  const declared = typeof params._meta === "object" && params._meta !== null ? params._meta : {};
@@ -50,6 +55,7 @@ export async function openBridge(options) {
50
55
  closed: false,
51
56
  questions: new Map(),
52
57
  interrupts: new Set(),
58
+ plan: null,
53
59
  };
54
60
  const connections = new Set();
55
61
  const clients = new Map();
@@ -201,38 +207,45 @@ export async function openBridge(options) {
201
207
  return;
202
208
  }
203
209
  if (frame.type === "draft") {
204
- if (typeof frame.text === "string" && frame.text !== "") options.draft?.(frame.text);
210
+ if (typeof frame.text === "string" && frame.text !== "" && sequenced(frame.sequence)) {
211
+ options.draft?.({ text: frame.text, sequence: frame.sequence });
212
+ }
205
213
  return;
206
214
  }
207
- if (
208
- frame.type === "plan" &&
209
- typeof frame.provider_turn === "string" &&
210
- Array.isArray(frame.steps) &&
211
- typeof frame.terminal === "boolean"
212
- ) {
213
- void options.plan?.({
214
- provider_turn: frame.provider_turn,
215
- steps: frame.steps,
216
- terminal: frame.terminal,
217
- });
215
+ if (frame.type === "plan") {
216
+ const snapshot = planSnapshot(frame);
217
+ if (snapshot === null || !sequenced(frame.sequence)) return;
218
+ state.plan = snapshot;
219
+ options.plan?.({ ...snapshot, sequence: frame.sequence });
218
220
  return;
219
221
  }
222
+ const carried = planSnapshot(frame.plan);
223
+ if (carried !== null) state.plan = carried;
220
224
  if (
221
225
  frame.type === "interrupt_applied" &&
222
226
  typeof frame.interrupt === "string" &&
223
227
  ["interrupted", "nothing_to_interrupt"].includes(frame.outcome) &&
224
228
  state.interrupts.has(frame.interrupt)
225
229
  ) {
226
- const reported = options.interrupt?.(frame.interrupt, frame.outcome);
230
+ const reported = options.interrupt?.(
231
+ frame.interrupt,
232
+ frame.outcome,
233
+ frame.outcome === "interrupted" ? state.plan : null,
234
+ );
227
235
  if (reported === undefined) state.interrupts.delete(frame.interrupt);
228
236
  else void Promise.resolve(reported).finally(() => state.interrupts.delete(frame.interrupt));
229
237
  return;
230
238
  }
239
+ if (frame.type === "how_we_work") {
240
+ void options.howWeWork?.();
241
+ return;
242
+ }
231
243
  if (frame.type === "turn_ended") {
232
244
  state.stop = {
233
245
  providerContinuation: frame.provider_continuation === true,
234
246
  providerSessionId: typeof frame.provider_session_id === "string" && frame.provider_session_id.length > 0 && frame.provider_session_id.length <= 512 ? frame.provider_session_id : null,
235
247
  stopOutcome: TURN_OUTCOMES.includes(frame.stop_outcome) ? frame.stop_outcome : null,
248
+ plan: carried,
236
249
  };
237
250
  return;
238
251
  }
@@ -432,6 +445,7 @@ export async function openBridge(options) {
432
445
  .filter((interaction) => interaction !== null),
433
446
  assistant: () => [...state.assistant],
434
447
  stop: () => state.stop,
448
+ plan: () => state.plan,
435
449
  failure: () => state.failure,
436
450
  stopRecovery: () => lifetime.abort(),
437
451
  async close() {
package/src/bytes.js CHANGED
@@ -31,6 +31,11 @@ async function downloadBody(response, signal) {
31
31
  return Buffer.concat(chunks, size);
32
32
  }
33
33
 
34
+ function framedBody(request) {
35
+ if (request.method !== "POST" || !Buffer.isBuffer(request.body)) throw new KitRefusal("kit_byte_body_invalid");
36
+ return Buffer.concat([Buffer.from(`${request.operation}\n`, "utf8"), request.body]);
37
+ }
38
+
34
39
  export async function transferBytes(request, dependencies = {}) {
35
40
  if (request.method !== "GET" && request.method !== "POST") {
36
41
  throw new KitRefusal("kit_byte_method_invalid", request.method);
@@ -38,26 +43,32 @@ export async function transferBytes(request, dependencies = {}) {
38
43
  if (typeof request.url !== "string" || request.url.length === 0) {
39
44
  throw new KitRefusal("kit_byte_url_invalid");
40
45
  }
41
- if (typeof request.operation !== "string" || request.operation.length === 0) {
46
+ if (request.method === "POST" && (typeof request.operation !== "string" || request.operation.length === 0)) {
42
47
  throw new KitRefusal("kit_byte_operation_invalid");
43
48
  }
49
+ const framed = request.framed === true;
50
+ const body = framed ? framedBody(request) : request.body;
44
51
  if (request.method === "POST") {
45
- if (!Buffer.isBuffer(request.body)) throw new KitRefusal("kit_byte_body_invalid");
46
- if (request.body.length > BYTE_GRANT_POST_BYTES_MAX) {
52
+ if (!Buffer.isBuffer(body)) throw new KitRefusal("kit_byte_body_invalid");
53
+ if (body.length > BYTE_GRANT_POST_BYTES_MAX) {
47
54
  throw new KitRefusal("original_bytes_exceeded");
48
55
  }
49
56
  }
50
57
  const fetcher = dependencies.fetch ?? fetch;
51
- const headers = {
52
- [BYTE_GRANT_OPERATION_HEADER]: request.operation,
53
- };
54
- if (request.method === "POST") headers["content-type"] = "application/octet-stream";
58
+ const headers = framed
59
+ ? { "content-type": "text/plain; charset=utf-8" }
60
+ : request.method === "POST"
61
+ ? {
62
+ [BYTE_GRANT_OPERATION_HEADER]: request.operation,
63
+ "content-type": "application/octet-stream",
64
+ }
65
+ : {};
55
66
  let response;
56
67
  try {
57
68
  response = await fetcher(request.url, {
58
69
  method: request.method,
59
70
  headers,
60
- body: request.method === "POST" ? request.body : undefined,
71
+ body: request.method === "POST" ? body : undefined,
61
72
  redirect: "manual",
62
73
  signal: request.signal,
63
74
  });
@@ -7,18 +7,10 @@ import {
7
7
  MIGRATOR_TIMEOUT_MS,
8
8
  } from "./contract.js";
9
9
  import { declarationReader } from "./declaration.js";
10
- import { GOOGLE_DRIVE_ID } from "./google/id.js";
11
10
  import { KitRefusal } from "./refusal.js";
12
11
 
13
- const PRESETS = new Set(["custom", "google"]);
14
12
  const AGENT_CLIENTS = new Set(["claude-code"]);
15
- const GOOGLE_CURATED_SERVICES = new Set(["gmail", "calendar", "drive"]);
16
13
  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
14
 
23
15
  const { refuse, plain, closed, text, positiveCount, filled } = declarationReader(
24
16
  "kit_configuration_invalid",
@@ -116,225 +108,33 @@ function backfillOf(value, declaration) {
116
108
  return { kind: named[0], bound: positiveCount(backfill[named[0]], declaration) };
117
109
  }
118
110
 
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) {
111
+ function serviceOf(value, declaration) {
269
112
  const service = closed(
270
113
  value,
271
- ["service", "source_ref", "rooms", "backfill", "connector", "selection", "declaration"],
114
+ ["service", "source_ref", "rooms", "backfill", "connector"],
272
115
  declaration,
273
116
  );
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
117
  return {
287
- service: name,
118
+ service: declaredId(service.service, `${declaration}.service`),
288
119
  sourceRef: text(service.source_ref, `${declaration}.source_ref`, 512),
289
120
  rooms: filled(service.rooms, `${declaration}.rooms`).map((room, position) =>
290
121
  text(room, `${declaration}.rooms[${position}]`, 512),
291
122
  ),
292
123
  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,
124
+ connector: connectorOf(service.connector, `${declaration}.connector`),
302
125
  };
303
126
  }
304
127
 
305
128
  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
- });
129
+ const instance = closed(value, ["id", "preset", "services"], declaration);
317
130
  const services = filled(instance.services, `${declaration}.services`).map((service, position) =>
318
- serviceOf(service, `${declaration}.services[${position}]`, preset),
131
+ serviceOf(service, `${declaration}.services[${position}]`),
319
132
  );
320
133
  services.forEach((service, position) => {
321
134
  const first = services.findIndex((candidate) => candidate.service === service.service);
322
135
  if (first !== position) refuse(`${declaration}.services[${position}].service`);
323
136
  });
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
- };
137
+ return { id: declaredId(instance.id, `${declaration}.id`), preset: instance.preset, services };
338
138
  }
339
139
 
340
140
  function absolutePath(value, declaration) {
@@ -360,12 +160,11 @@ function maximaOf(value, declaration) {
360
160
 
361
161
  function hostOf(value, declaration) {
362
162
  if (value === null) return null;
363
- const host = closed(value, ["workspace", "maxima", "image"], declaration);
163
+ const host = closed(value, ["workspace", "maxima"], declaration);
364
164
  if (host.maxima === undefined) refuse(`${declaration}.maxima`);
365
165
  return {
366
166
  workspace: absolutePath(host.workspace, `${declaration}.workspace`),
367
167
  maxima: maximaOf(host.maxima, `${declaration}.maxima`),
368
- image: host.image === undefined || host.image === null ? null : text(host.image, `${declaration}.image`, 255),
369
168
  };
370
169
  }
371
170
 
@@ -388,6 +187,7 @@ export function parseConfiguration(document) {
388
187
  "migrators",
389
188
  "clients",
390
189
  "host",
190
+ "skills",
391
191
  ],
392
192
  "configuration",
393
193
  );
@@ -397,10 +197,12 @@ export function parseConfiguration(document) {
397
197
  if (!EXECUTION_AUTHORITIES.includes(declared.execution_authority)) {
398
198
  refuse("execution_authority");
399
199
  }
200
+ const skills = declared.skills === undefined ? true : declared.skills;
201
+ if (typeof skills !== "boolean") refuse("skills");
400
202
  if (!Array.isArray(declared.instances)) refuse("instances");
401
203
  if (!Array.isArray(declared.migrators)) refuse("migrators");
402
- const instances = declared.instances.map((instance, position) =>
403
- instanceOf(instance, `instances[${position}]`),
204
+ const instances = declared.instances.flatMap((instance, position) =>
205
+ instance?.preset === "custom" ? [instanceOf(instance, `instances[${position}]`)] : [],
404
206
  );
405
207
  instances.forEach((instance, position) => {
406
208
  const first = instances.findIndex((candidate) => candidate.id === instance.id);
@@ -428,6 +230,7 @@ export function parseConfiguration(document) {
428
230
  migrators,
429
231
  clients,
430
232
  host: hostOf(declared.host, "host"),
233
+ skills,
431
234
  };
432
235
  }
433
236
 
@@ -1,91 +1,10 @@
1
1
  import { readConfiguration, sourceInstance } from "./configuration.js";
2
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
3
 
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 = {}) {
4
+ export async function signInConnector(options) {
82
5
  const configuration = await readConfiguration(options.configuration);
83
6
  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
7
  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";
8
+ if (await customAuthorizationStatus(instance, context)) return "skipped";
9
+ return (await authorizeCustom(instance, context)) ? "ready" : "needs_attention";
91
10
  }