@agentshouse/kit 0.1.0-alpha.4 → 0.1.0-alpha.41
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.
- package/README.md +41 -1671
- package/bin/connect-linux.sh +501 -53
- package/bin/connect-windows.ps1 +440 -0
- package/bin/kit.js +200 -159
- package/bootstrap.json +28 -0
- package/docs/configuration.md +104 -0
- package/docs/google.md +33 -0
- package/docs/linux.md +77 -0
- package/docs/macos.md +26 -0
- package/docs/telegram.md +29 -0
- package/docs/windows.md +37 -0
- package/package.json +7 -2
- package/src/adapter-mcp.js +92 -12
- package/src/agents.js +63 -8
- package/src/binding.js +52 -0
- package/src/blocks.js +2 -26
- package/src/bridge.js +28 -14
- package/src/bytes.js +19 -8
- package/src/configuration.js +14 -211
- package/src/connector-login.js +3 -84
- package/src/contract.js +44 -12
- package/src/conversation-originals.js +1 -3
- package/src/delivery.js +4 -7
- package/src/directory.js +6 -26
- package/src/draft.js +4 -82
- package/src/git-main.js +138 -0
- package/src/git.js +62 -0
- package/src/google/calendar.js +39 -45
- package/src/google/drive.js +61 -186
- package/src/google/entries.js +349 -0
- package/src/google/gateway.js +3 -6
- package/src/google/gmail.js +33 -23
- package/src/google/oauth.js +106 -66
- package/src/google/source.js +21 -23
- package/src/host.js +6 -2
- package/src/house-main.js +65 -8
- package/src/house.js +446 -127
- package/src/how-we-work.md +25 -0
- package/src/inbound.js +2 -18
- package/src/kit-work-stream.js +120 -0
- package/src/launcher.js +46 -10
- package/src/local-files.js +3 -11
- package/src/login.js +27 -14
- package/src/mcp.js +18 -0
- package/src/mount.js +9 -18
- package/src/original.js +4 -4
- package/src/pacing.js +41 -0
- package/src/plan.js +37 -45
- package/src/repository.js +3 -61
- package/src/session-bin/git +2 -0
- package/src/setup.js +14 -15
- package/src/skill-set/ask-matt/PHASE-BOUNDARIES.md +55 -0
- package/src/skill-set/ask-matt/SKILL.md +91 -0
- package/src/skill-set/ask-matt/agents/openai.yaml +4 -0
- package/src/skill-set/code-review/LICENSE +21 -0
- package/src/skill-set/code-review/SKILL.md +89 -0
- package/src/skill-set/code-review/agents/openai.yaml +3 -0
- package/src/skill-set/codebase-design/DEEPENING.md +37 -0
- package/src/skill-set/codebase-design/DESIGN-IT-TWICE.md +44 -0
- package/src/skill-set/codebase-design/LICENSE +21 -0
- package/src/skill-set/codebase-design/SKILL.md +116 -0
- package/src/skill-set/codebase-design/agents/openai.yaml +3 -0
- package/src/skill-set/diagnosing-bugs/LICENSE +21 -0
- package/src/skill-set/diagnosing-bugs/SKILL.md +140 -0
- package/src/skill-set/diagnosing-bugs/agents/openai.yaml +3 -0
- package/src/skill-set/diagnosing-bugs/scripts/hitl-loop.template.sh +44 -0
- package/src/skill-set/domain-modeling/ADR-FORMAT.md +47 -0
- package/src/skill-set/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/src/skill-set/domain-modeling/LICENSE +21 -0
- package/src/skill-set/domain-modeling/SKILL.md +76 -0
- package/src/skill-set/domain-modeling/agents/openai.yaml +3 -0
- package/src/skill-set/grill-me/LICENSE +21 -0
- package/src/skill-set/grill-me/SKILL.md +8 -0
- package/src/skill-set/grill-me/agents/openai.yaml +4 -0
- package/src/skill-set/grill-with-docs/LICENSE +21 -0
- package/src/skill-set/grill-with-docs/SKILL.md +8 -0
- package/src/skill-set/grill-with-docs/agents/openai.yaml +4 -0
- package/src/skill-set/grilling/LICENSE +21 -0
- package/src/{starter-set → skill-set}/grilling/SKILL.md +1 -1
- package/src/skill-set/handoff/LICENSE +21 -0
- package/src/skill-set/handoff/SKILL.md +17 -0
- package/src/skill-set/handoff/agents/openai.yaml +4 -0
- package/src/skill-set/implement/LICENSE +21 -0
- package/src/skill-set/implement/SKILL.md +16 -0
- package/src/skill-set/implement/agents/openai.yaml +4 -0
- package/src/skill-set/improve-codebase-architecture/HTML-REPORT.md +123 -0
- package/src/skill-set/improve-codebase-architecture/LICENSE +21 -0
- package/src/skill-set/improve-codebase-architecture/SKILL.md +72 -0
- package/src/skill-set/improve-codebase-architecture/agents/openai.yaml +4 -0
- package/src/skill-set/prototype/LICENSE +21 -0
- package/src/skill-set/prototype/LOGIC.md +67 -0
- package/src/skill-set/prototype/SKILL.md +28 -0
- package/src/skill-set/prototype/UI.md +112 -0
- package/src/skill-set/prototype/agents/openai.yaml +3 -0
- package/src/skill-set/research/LICENSE +21 -0
- package/src/skill-set/research/SKILL.md +14 -0
- package/src/skill-set/research/agents/openai.yaml +3 -0
- package/src/skill-set/resolving-merge-conflicts/LICENSE +21 -0
- package/src/skill-set/resolving-merge-conflicts/SKILL.md +16 -0
- package/src/skill-set/resolving-merge-conflicts/agents/openai.yaml +3 -0
- package/src/skill-set/tdd/LICENSE +21 -0
- package/src/skill-set/tdd/SKILL.md +40 -0
- package/src/skill-set/tdd/agents/openai.yaml +3 -0
- package/src/skill-set/tdd/mocking.md +59 -0
- package/src/skill-set/tdd/tests.md +77 -0
- package/src/skill-set/teach/GLOSSARY-FORMAT.md +35 -0
- package/src/skill-set/teach/LEARNING-RECORD-FORMAT.md +46 -0
- package/src/skill-set/teach/LICENSE +21 -0
- package/src/skill-set/teach/MISSION-FORMAT.md +31 -0
- package/src/skill-set/teach/RESOURCES-FORMAT.md +32 -0
- package/src/skill-set/teach/SKILL.md +141 -0
- package/src/skill-set/teach/agents/openai.yaml +4 -0
- package/src/skill-set/to-questionnaire/LICENSE +21 -0
- package/src/skill-set/to-questionnaire/SKILL.md +55 -0
- package/src/skill-set/to-questionnaire/agents/openai.yaml +4 -0
- package/src/skill-set/to-spec/LICENSE +21 -0
- package/src/skill-set/to-spec/SKILL.md +76 -0
- package/src/skill-set/to-spec/agents/openai.yaml +4 -0
- package/src/skill-set/to-tickets/LICENSE +21 -0
- package/src/skill-set/to-tickets/SKILL.md +106 -0
- package/src/skill-set/to-tickets/agents/openai.yaml +4 -0
- package/src/skill-set/wait-what/LICENSE +21 -0
- package/src/skill-set/wait-what/SKILL.md +8 -0
- package/src/skill-set/wait-what/agents/openai.yaml +4 -0
- package/src/skill-set/wayfinder/LICENSE +21 -0
- package/src/skill-set/wayfinder/SKILL.md +129 -0
- package/src/skill-set/wayfinder/agents/openai.yaml +4 -0
- package/src/skill-set/writing-for-agents/LICENSE +21 -0
- package/src/skill-set/writing-for-agents/SKILL-MECHANICS.md +22 -0
- package/src/skill-set/writing-for-agents/SKILL.md +83 -0
- package/src/skill-set/writing-for-agents/agents/openai.yaml +3 -0
- package/src/skill-set.js +85 -0
- package/src/source.js +93 -54
- package/src/state.js +19 -1
- package/src/sync-library.js +7 -11
- package/src/sync-state.js +12 -3
- package/src/sync.js +142 -43
- package/src/tasks.js +92 -0
- package/src/telegram-view.js +217 -0
- package/src/telegram.js +204 -69
- package/src/turn-frames.js +69 -0
- package/src/worker.js +738 -189
- package/src/working-copy.js +1274 -0
- package/src/workspace.js +299 -88
- package/src/google/declared.js +0 -146
- package/src/google/id.js +0 -1
- package/src/instance/google.js +0 -1
- package/src/starter-set.js +0 -17
- /package/src/{starter-set/grilling → skill-set/ask-matt}/LICENSE +0 -0
- /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
|
-
|
|
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
|
|
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 !== ""
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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?.(
|
|
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(
|
|
46
|
-
if (
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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" ?
|
|
71
|
+
body: request.method === "POST" ? body : undefined,
|
|
61
72
|
redirect: "manual",
|
|
62
73
|
signal: request.signal,
|
|
63
74
|
});
|
package/src/configuration.js
CHANGED
|
@@ -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
|
|
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"
|
|
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:
|
|
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
|
-
|
|
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"
|
|
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}]
|
|
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
|
-
|
|
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"
|
|
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.
|
|
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
|
|
package/src/connector-login.js
CHANGED
|
@@ -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
|
|
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
|
|
90
|
-
return (await
|
|
8
|
+
if (await customAuthorizationStatus(instance, context)) return "skipped";
|
|
9
|
+
return (await authorizeCustom(instance, context)) ? "ready" : "needs_attention";
|
|
91
10
|
}
|