@agentshouse/kit 0.1.0-alpha.25 → 0.1.0-alpha.27

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 (117) hide show
  1. package/README.md +172 -158
  2. package/bin/connect-linux.sh +15 -4
  3. package/bin/kit.js +48 -46
  4. package/bootstrap.json +3 -3
  5. package/package.json +1 -1
  6. package/src/adapter-mcp.js +23 -1
  7. package/src/bridge.js +4 -0
  8. package/src/configuration.js +13 -209
  9. package/src/connector-login.js +3 -84
  10. package/src/contract.js +4 -0
  11. package/src/delivery.js +4 -7
  12. package/src/directory.js +6 -26
  13. package/src/google/calendar.js +39 -45
  14. package/src/google/drive.js +61 -186
  15. package/src/google/entries.js +346 -0
  16. package/src/google/gateway.js +3 -6
  17. package/src/google/gmail.js +33 -23
  18. package/src/google/oauth.js +106 -66
  19. package/src/google/source.js +21 -23
  20. package/src/house.js +43 -5
  21. package/src/how-we-work.md +25 -0
  22. package/src/kit-work-stream.js +1 -1
  23. package/src/launcher.js +11 -4
  24. package/src/mcp.js +5 -0
  25. package/src/setup.js +8 -14
  26. package/src/skill-set/ask-matt/PHASE-BOUNDARIES.md +55 -0
  27. package/src/skill-set/ask-matt/SKILL.md +91 -0
  28. package/src/skill-set/ask-matt/agents/openai.yaml +4 -0
  29. package/src/skill-set/code-review/LICENSE +21 -0
  30. package/src/skill-set/code-review/SKILL.md +89 -0
  31. package/src/skill-set/code-review/agents/openai.yaml +3 -0
  32. package/src/skill-set/codebase-design/DEEPENING.md +37 -0
  33. package/src/skill-set/codebase-design/DESIGN-IT-TWICE.md +44 -0
  34. package/src/skill-set/codebase-design/LICENSE +21 -0
  35. package/src/skill-set/codebase-design/SKILL.md +116 -0
  36. package/src/skill-set/codebase-design/agents/openai.yaml +3 -0
  37. package/src/skill-set/diagnosing-bugs/LICENSE +21 -0
  38. package/src/skill-set/diagnosing-bugs/SKILL.md +140 -0
  39. package/src/skill-set/diagnosing-bugs/agents/openai.yaml +3 -0
  40. package/src/skill-set/diagnosing-bugs/scripts/hitl-loop.template.sh +44 -0
  41. package/src/skill-set/domain-modeling/ADR-FORMAT.md +47 -0
  42. package/src/skill-set/domain-modeling/CONTEXT-FORMAT.md +60 -0
  43. package/src/skill-set/domain-modeling/LICENSE +21 -0
  44. package/src/skill-set/domain-modeling/SKILL.md +76 -0
  45. package/src/skill-set/domain-modeling/agents/openai.yaml +3 -0
  46. package/src/skill-set/grill-me/LICENSE +21 -0
  47. package/src/skill-set/grill-me/SKILL.md +8 -0
  48. package/src/skill-set/grill-me/agents/openai.yaml +4 -0
  49. package/src/skill-set/grill-with-docs/LICENSE +21 -0
  50. package/src/skill-set/grill-with-docs/SKILL.md +8 -0
  51. package/src/skill-set/grill-with-docs/agents/openai.yaml +4 -0
  52. package/src/skill-set/grilling/LICENSE +21 -0
  53. package/src/{starter-set → skill-set}/grilling/SKILL.md +1 -1
  54. package/src/skill-set/handoff/LICENSE +21 -0
  55. package/src/skill-set/handoff/SKILL.md +17 -0
  56. package/src/skill-set/handoff/agents/openai.yaml +4 -0
  57. package/src/skill-set/implement/LICENSE +21 -0
  58. package/src/skill-set/implement/SKILL.md +16 -0
  59. package/src/skill-set/implement/agents/openai.yaml +4 -0
  60. package/src/skill-set/improve-codebase-architecture/HTML-REPORT.md +123 -0
  61. package/src/skill-set/improve-codebase-architecture/LICENSE +21 -0
  62. package/src/skill-set/improve-codebase-architecture/SKILL.md +72 -0
  63. package/src/skill-set/improve-codebase-architecture/agents/openai.yaml +4 -0
  64. package/src/skill-set/prototype/LICENSE +21 -0
  65. package/src/skill-set/prototype/LOGIC.md +67 -0
  66. package/src/skill-set/prototype/SKILL.md +28 -0
  67. package/src/skill-set/prototype/UI.md +112 -0
  68. package/src/skill-set/prototype/agents/openai.yaml +3 -0
  69. package/src/skill-set/research/LICENSE +21 -0
  70. package/src/skill-set/research/SKILL.md +14 -0
  71. package/src/skill-set/research/agents/openai.yaml +3 -0
  72. package/src/skill-set/resolving-merge-conflicts/LICENSE +21 -0
  73. package/src/skill-set/resolving-merge-conflicts/SKILL.md +16 -0
  74. package/src/skill-set/resolving-merge-conflicts/agents/openai.yaml +3 -0
  75. package/src/skill-set/tdd/LICENSE +21 -0
  76. package/src/skill-set/tdd/SKILL.md +40 -0
  77. package/src/skill-set/tdd/agents/openai.yaml +3 -0
  78. package/src/skill-set/tdd/mocking.md +59 -0
  79. package/src/skill-set/tdd/tests.md +77 -0
  80. package/src/skill-set/teach/GLOSSARY-FORMAT.md +35 -0
  81. package/src/skill-set/teach/LEARNING-RECORD-FORMAT.md +46 -0
  82. package/src/skill-set/teach/LICENSE +21 -0
  83. package/src/skill-set/teach/MISSION-FORMAT.md +31 -0
  84. package/src/skill-set/teach/RESOURCES-FORMAT.md +32 -0
  85. package/src/skill-set/teach/SKILL.md +141 -0
  86. package/src/skill-set/teach/agents/openai.yaml +4 -0
  87. package/src/skill-set/to-questionnaire/LICENSE +21 -0
  88. package/src/skill-set/to-questionnaire/SKILL.md +55 -0
  89. package/src/skill-set/to-questionnaire/agents/openai.yaml +4 -0
  90. package/src/skill-set/to-spec/LICENSE +21 -0
  91. package/src/skill-set/to-spec/SKILL.md +76 -0
  92. package/src/skill-set/to-spec/agents/openai.yaml +4 -0
  93. package/src/skill-set/to-tickets/LICENSE +21 -0
  94. package/src/skill-set/to-tickets/SKILL.md +106 -0
  95. package/src/skill-set/to-tickets/agents/openai.yaml +4 -0
  96. package/src/skill-set/wait-what/LICENSE +21 -0
  97. package/src/skill-set/wait-what/SKILL.md +8 -0
  98. package/src/skill-set/wait-what/agents/openai.yaml +4 -0
  99. package/src/skill-set/wayfinder/LICENSE +21 -0
  100. package/src/skill-set/wayfinder/SKILL.md +129 -0
  101. package/src/skill-set/wayfinder/agents/openai.yaml +4 -0
  102. package/src/skill-set/writing-for-agents/LICENSE +21 -0
  103. package/src/skill-set/writing-for-agents/SKILL-MECHANICS.md +22 -0
  104. package/src/skill-set/writing-for-agents/SKILL.md +83 -0
  105. package/src/skill-set/writing-for-agents/agents/openai.yaml +3 -0
  106. package/src/skill-set.js +85 -0
  107. package/src/source.js +51 -26
  108. package/src/state.js +19 -1
  109. package/src/tasks.js +72 -0
  110. package/src/worker.js +46 -68
  111. package/src/working-copy.js +1 -15
  112. package/src/google/declared.js +0 -146
  113. package/src/google/id.js +0 -1
  114. package/src/instance/google.js +0 -1
  115. package/src/starter-set.js +0 -17
  116. /package/src/{starter-set/grilling → skill-set/ask-matt}/LICENSE +0 -0
  117. /package/src/{starter-set → skill-set}/grilling/agents/openai.yaml +0 -0
package/bin/kit.js CHANGED
@@ -5,12 +5,12 @@ import { join, resolve } from "node:path";
5
5
  import { connectHouse, loginHouse, storedEnvironment, storedHouse } from "../src/login.js";
6
6
  import { apply } from "../src/apply.js";
7
7
  import { captureSource, resetSource, retrySourceOriginals, sourceStatus } from "../src/source.js";
8
+ import { accountEntries, accountStatus, captureAccount } from "../src/google/entries.js";
8
9
  import { setupEnvironment } from "../src/setup.js";
9
10
  import { signInConnector } from "../src/connector-login.js";
10
11
  import { attachSession } from "../src/attach.js";
11
12
  import { runResident, runWorkers } from "../src/worker.js";
12
13
  import { openSession, releaseControl, requestDrain } from "../src/house.js";
13
- import { GOOGLE_OAUTH_CLIENT } from "../src/instance/google.js";
14
14
  import { reportHost } from "../src/host.js";
15
15
  import { signInAgent, updateAgents } from "../src/agents.js";
16
16
  import { serveWorkspaces } from "../src/workspace.js";
@@ -37,8 +37,8 @@ const AGENTS_USAGE =
37
37
  "usage: kit agents update [--config <path>] | kit agents sign-in <kind> [--config <path>]";
38
38
  const WORKSPACE_USAGE = "usage: kit workspace host [--config <path>] [--passes <count>]";
39
39
  const SOURCE_USAGE =
40
- "usage: kit source <capture|reset|retry-files> <instance> [service] [--config <path>]\n" +
41
- " kit source status <instance> [service] [--config <path>]\n" +
40
+ "usage: kit source <capture|status> <account|instance> [service] [--config <path>]\n" +
41
+ " kit source <reset|retry-files> <instance> [service] [--config <path>]\n" +
42
42
  " kit source sign-in <instance> [--config <path>]";
43
43
  const SETUP_USAGE = "usage: kit setup [--config <path>]";
44
44
  const REPOSITORY_USAGE =
@@ -108,13 +108,6 @@ function write(line) {
108
108
  process.stdout.write(`${line}\n`);
109
109
  }
110
110
 
111
- function builtGoogleOAuthClient() {
112
- if (GOOGLE_OAUTH_CLIENT === null) {
113
- throw new KitRefusal("google_oauth_client_unconfigured", "run npm run build");
114
- }
115
- return GOOGLE_OAUTH_CLIENT;
116
- }
117
-
118
111
  async function runLogin(args, directory) {
119
112
  const { values } = declared(args, ["--house", "--manual", "--host-browser", "--environment"], 0, { flags: ["--host-browser"] });
120
113
  if (values["--manual"] && values["--host-browser"]) throw new Error(LOGIN_USAGE);
@@ -188,15 +181,12 @@ async function runSetup(args, directory) {
188
181
  process.once("SIGINT", cancel);
189
182
  let setup;
190
183
  try {
191
- setup = await setupEnvironment(
192
- {
193
- house,
194
- directory,
195
- configuration: values["--config"] ?? join(directory, "kit.json"),
196
- signal: cancellation.signal,
197
- },
198
- { googleOAuthClient: builtGoogleOAuthClient },
199
- );
184
+ setup = await setupEnvironment({
185
+ house,
186
+ directory,
187
+ configuration: values["--config"] ?? join(directory, "kit.json"),
188
+ signal: cancellation.signal,
189
+ });
200
190
  } finally {
201
191
  process.removeListener("SIGINT", cancel);
202
192
  }
@@ -268,22 +258,27 @@ async function runSourceCapture(args, directory) {
268
258
  const house = await storedHouse(directory);
269
259
  const cancellation = new AbortController();
270
260
  process.once("SIGINT", () => cancellation.abort());
271
- const result = await captureSource(
272
- {
273
- house,
274
- directory,
275
- configuration: values["--config"] ?? join(directory, "kit.json"),
276
- instance: rest[0],
277
- service: rest[1] ?? null,
278
- signal: cancellation.signal,
279
- },
280
- { googleOAuthClient: builtGoogleOAuthClient },
281
- );
261
+ const entries = await accountEntries(directory, rest[0]);
262
+ const options = {
263
+ house,
264
+ directory,
265
+ service: rest[1] ?? null,
266
+ signal: cancellation.signal,
267
+ };
268
+ const result =
269
+ entries.length > 0
270
+ ? await captureAccount({ ...options, account: rest[0], entries })
271
+ : await captureSource({
272
+ ...options,
273
+ configuration: values["--config"] ?? join(directory, "kit.json"),
274
+ instance: rest[0],
275
+ });
282
276
  for (const delivered of result.services) {
283
277
  const category =
284
278
  delivered.health.category === null ? "" : ` (${delivered.health.category})`;
279
+ const cause = delivered.health.cause === undefined ? "" : `, cause ${delivered.health.cause}`;
285
280
  write(
286
- `Delivered ${delivered.delivered} items for ${result.instance}/${delivered.service} in ${delivered.batches} batches; health ${delivered.health.state}${category}.`,
281
+ `Delivered ${delivered.delivered} items for ${result.instance}/${delivered.service} in ${delivered.batches} batches; health ${delivered.health.state}${category}${cause}.`,
287
282
  );
288
283
  if (delivered.refused !== null) {
289
284
  write(
@@ -332,22 +327,32 @@ async function runSourceRetryFiles(args, directory) {
332
327
  async function runSourceStatus(args, directory) {
333
328
  const { values, rest } = declared(args, ["--config"], 2);
334
329
  if (rest.length === 0) throw new Error(SOURCE_USAGE);
335
- const result = await sourceStatus({
336
- directory,
337
- configuration: values["--config"] ?? join(directory, "kit.json"),
338
- instance: rest[0],
339
- service: rest[1] ?? null,
340
- });
330
+ const entries = await accountEntries(directory, rest[0]);
331
+ const result =
332
+ entries.length > 0
333
+ ? await accountStatus({ directory, account: rest[0], entries, service: rest[1] ?? null })
334
+ : await sourceStatus({
335
+ directory,
336
+ configuration: values["--config"] ?? join(directory, "kit.json"),
337
+ instance: rest[0],
338
+ service: rest[1] ?? null,
339
+ });
341
340
  for (const reported of result.services) {
342
341
  const category = reported.health.category === null ? "" : ` (${reported.health.category})`;
342
+ const cause = reported.health.cause === undefined ? "" : `, cause ${reported.health.cause}`;
343
343
  const observed =
344
344
  reported.health.observed_at === null ? "never observed" : reported.health.observed_at;
345
+ const signedIn =
346
+ reported.signed_in === undefined ? "" : reported.signed_in ? "signed in; " : "signed out; ";
345
347
  write(
346
348
  `${result.instance}/${reported.service} -> ${reported.source_ref} in ${reported.rooms.join(", ")}: ` +
347
- `${reported.health.state}${category} at ${observed}; ` +
349
+ `${signedIn}${reported.health.state}${category}${cause} at ${observed}; ` +
348
350
  `cursor ${reported.checkpoint?.cursor ?? "none"}, ${reported.gaps.length} gaps, ` +
349
351
  `${reported.dropped.length} dropped items, ${reported.originals.length} unsaved originals.`,
350
352
  );
353
+ if (reported.enable !== null && reported.enable !== undefined) {
354
+ write(`Enable the ${reported.enable} in the Google Cloud project of ${result.instance}'s OAuth client.`);
355
+ }
351
356
  }
352
357
  if (result.services.some((reported) => reported.health.state !== "ok")) process.exitCode = 1;
353
358
  }
@@ -425,14 +430,11 @@ function writeConversationFiles(supervised) {
425
430
  async function runWorkerRun(args, directory) {
426
431
  const { values } = declared(args, ["--config"], 0);
427
432
  const house = await storedHouse(directory);
428
- const supervised = await runWorkers(
429
- {
430
- house,
431
- directory,
432
- configuration: values["--config"] ?? join(directory, "kit.json"),
433
- },
434
- { googleOAuthClient: builtGoogleOAuthClient },
435
- );
433
+ const supervised = await runWorkers({
434
+ house,
435
+ directory,
436
+ configuration: values["--config"] ?? join(directory, "kit.json"),
437
+ });
436
438
  write(`Worker runtime ready on tmux ${supervised.tmux} in namespace ${supervised.namespace}.`);
437
439
  if (supervised.epochReport !== null) {
438
440
  write(`House started a new log epoch; reported ${supervised.epochReport.length} held Runs and House settled each.`);
package/bootstrap.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.1.0-alpha.25",
3
+ "version": "0.1.0-alpha.27",
4
4
  "hosts": {
5
5
  "linux": {
6
- "command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.25/connect-linux.sh)",
6
+ "command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.27/connect-linux.sh)",
7
7
  "asset": {
8
8
  "name": "connect-linux.sh",
9
- "url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.25/connect-linux.sh"
9
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.27/connect-linux.sh"
10
10
  },
11
11
  "workspace": {
12
12
  "argument": "--workspace",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentshouse/kit",
3
- "version": "0.1.0-alpha.25",
3
+ "version": "0.1.0-alpha.27",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,12 +1,15 @@
1
+ import { readFileSync } from "node:fs";
1
2
  import {
2
3
  COMMAND_ADAPTER_INITIALIZE_TIMEOUT_MS,
3
4
  HOW_WE_WORK_PATH,
4
5
  MCP_PROTOCOL_VERSION,
5
6
  SHELL_RENDER,
6
7
  } from "./contract.js";
7
- import { answerText, bridgeCall } from "./mcp.js";
8
+ import { answerText, bridgeCall, editRefusal } from "./mcp.js";
8
9
  import { KitRefusal } from "./refusal.js";
9
10
 
11
+ const HOW_WE_WORK_TEXT = readFileSync(new URL("how-we-work.md", import.meta.url), "utf8");
12
+
10
13
  function readableDiscovery(discovery) {
11
14
  const identity = discovery?._meta?.["io.modelcontextprotocol/serverInfo"];
12
15
  return Array.isArray(discovery?.supportedVersions) &&
@@ -84,3 +87,22 @@ export async function commandHouseHowWeWork(bridge) {
84
87
  } while (cursor !== null);
85
88
  return pages.join("\n");
86
89
  }
90
+
91
+ export async function commandHouseCreateHowWeWork(bridge) {
92
+ const frame = await bridgeCall(bridge, {
93
+ method: "tools/call",
94
+ params: {
95
+ name: "edit",
96
+ arguments: { changes: [{ op: "create", path: HOW_WE_WORK_PATH, content: HOW_WE_WORK_TEXT }] },
97
+ },
98
+ });
99
+ if (frame.error !== undefined) {
100
+ throw new KitRefusal("kit_how_we_work_unwritable", frame.error.message ?? null);
101
+ }
102
+ if (frame.result?.isError !== true) return true;
103
+ const text = answerText(frame.result);
104
+ const { code, conflicts } = editRefusal(text);
105
+ if (code === "room_not_found" || code === "operation_denied") return false;
106
+ if (code === "edit_conflict" && conflicts?.some((conflict) => conflict.code === "path_exists")) return true;
107
+ throw new KitRefusal("kit_how_we_work_unwritable", text);
108
+ }
package/src/bridge.js CHANGED
@@ -228,6 +228,10 @@ export async function openBridge(options) {
228
228
  else void Promise.resolve(reported).finally(() => state.interrupts.delete(frame.interrupt));
229
229
  return;
230
230
  }
231
+ if (frame.type === "how_we_work") {
232
+ void options.howWeWork?.();
233
+ return;
234
+ }
231
235
  if (frame.type === "turn_ended") {
232
236
  state.stop = {
233
237
  providerContinuation: frame.provider_continuation === true,
@@ -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) {
@@ -388,6 +188,7 @@ export function parseConfiguration(document) {
388
188
  "migrators",
389
189
  "clients",
390
190
  "host",
191
+ "skills",
391
192
  ],
392
193
  "configuration",
393
194
  );
@@ -397,10 +198,12 @@ export function parseConfiguration(document) {
397
198
  if (!EXECUTION_AUTHORITIES.includes(declared.execution_authority)) {
398
199
  refuse("execution_authority");
399
200
  }
201
+ const skills = declared.skills === undefined ? true : declared.skills;
202
+ if (typeof skills !== "boolean") refuse("skills");
400
203
  if (!Array.isArray(declared.instances)) refuse("instances");
401
204
  if (!Array.isArray(declared.migrators)) refuse("migrators");
402
- const instances = declared.instances.map((instance, position) =>
403
- instanceOf(instance, `instances[${position}]`),
205
+ const instances = declared.instances.flatMap((instance, position) =>
206
+ instance?.preset === "custom" ? [instanceOf(instance, `instances[${position}]`)] : [],
404
207
  );
405
208
  instances.forEach((instance, position) => {
406
209
  const first = instances.findIndex((candidate) => candidate.id === instance.id);
@@ -428,6 +231,7 @@ export function parseConfiguration(document) {
428
231
  migrators,
429
232
  clients,
430
233
  host: hostOf(declared.host, "host"),
234
+ skills,
431
235
  };
432
236
  }
433
237
 
@@ -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
  }
package/src/contract.js CHANGED
@@ -259,6 +259,10 @@ export const KIT_LOGIN_REPORT_PATH = "/kit/logins/report";
259
259
  export const GUIDED_LOGIN_TIMEOUT_MS = 10 * 60 * 1000;
260
260
  export const GUIDED_LOGIN_OUTPUT_MAX_BYTES = 64 * 1024;
261
261
 
262
+ export const KIT_SOURCES_DESIRED_PATH = "/kit/sources/desired";
263
+ export const KIT_SOURCES_REPORT_PATH = "/kit/sources/report";
264
+ export const SOURCE_CAPTURE_INTERVAL_MS = 15 * 60 * 1000;
265
+
262
266
  export const EXECUTION_AUTHORITIES = ["allow", "refuse"];
263
267
 
264
268
  export const PERMISSION_OPTION_KINDS = Object.freeze({
package/src/delivery.js CHANGED
@@ -54,10 +54,8 @@ const FAILURE_CATEGORIES = {
54
54
  google_provider_unavailable: "provider",
55
55
  google_provider_response_invalid: "provider",
56
56
  google_checkpoint_invalid: "provider",
57
- google_service_unsupported: "config",
58
- google_oauth_client_unconfigured: "config",
59
- google_drive_selection_exceeded: "config",
60
57
  google_drive_item_missing: "provider",
58
+ api_disabled: "config",
61
59
  };
62
60
 
63
61
  export function fileDescriptor(file) {
@@ -184,8 +182,7 @@ export function stallOf(code, houseRetry = null) {
184
182
 
185
183
  export function healthOfRun(stall) {
186
184
  if (stall === null) return { state: "ok", category: null };
187
- return {
188
- state: stall.disposition === "retry" ? "retrying" : "failed",
189
- category: stall.category ?? FAILURE_CATEGORIES[stall.code] ?? "internal",
190
- };
185
+ const category = stall.category ?? FAILURE_CATEGORIES[stall.code] ?? "internal";
186
+ if (stall.disposition === "retry") return { state: "retrying", category };
187
+ return { state: "failed", category, cause: stall.code };
191
188
  }