@agentshouse/kit 0.0.1 → 0.1.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1776 -2
- package/bin/connect-linux.sh +173 -0
- package/bin/kit.js +1023 -0
- package/bootstrap.json +17 -0
- package/package.json +37 -3
- package/src/acp.js +99 -0
- package/src/adapter-mcp.js +44 -0
- package/src/adapter.js +252 -0
- package/src/agent-logins.js +65 -0
- package/src/agents.js +504 -0
- package/src/apply.js +29 -0
- package/src/attach.js +52 -0
- package/src/binding.js +90 -0
- package/src/blocks.js +156 -0
- package/src/bridge.js +449 -0
- package/src/bytes.js +89 -0
- package/src/channel.js +37 -0
- package/src/command.js +109 -0
- package/src/configuration.js +465 -0
- package/src/connector-login.js +91 -0
- package/src/connector.js +107 -0
- package/src/continuation.js +61 -0
- package/src/contract.js +363 -0
- package/src/conversation-originals.js +143 -0
- package/src/custom/authorization.js +63 -0
- package/src/declaration.js +35 -0
- package/src/delivery.js +191 -0
- package/src/directory.js +102 -0
- package/src/draft.js +89 -0
- package/src/google/calendar.js +177 -0
- package/src/google/declared.js +146 -0
- package/src/google/drive.js +435 -0
- package/src/google/gateway.js +166 -0
- package/src/google/gmail.js +249 -0
- package/src/google/id.js +1 -0
- package/src/google/oauth.js +273 -0
- package/src/google/source.js +25 -0
- package/src/hook.js +376 -0
- package/src/host.js +121 -0
- package/src/house-main.js +79 -0
- package/src/house.js +1275 -0
- package/src/inbound.js +175 -0
- package/src/instance/google.js +1 -0
- package/src/kit-work-stream.js +84 -0
- package/src/launcher-main.js +19 -0
- package/src/launcher.js +276 -0
- package/src/local-files.js +113 -0
- package/src/login.js +274 -0
- package/src/mcp.js +71 -0
- package/src/migrator.js +248 -0
- package/src/mount.js +582 -0
- package/src/operation.js +20 -0
- package/src/original.js +631 -0
- package/src/peer.js +124 -0
- package/src/plan.js +57 -0
- package/src/question.js +316 -0
- package/src/refusal.js +79 -0
- package/src/repository.js +521 -0
- package/src/session-bin/house +2 -0
- package/src/settlement.js +57 -0
- package/src/setup.js +190 -0
- package/src/source-originals.js +116 -0
- package/src/source.js +290 -0
- package/src/starter-set/grilling/LICENSE +21 -0
- package/src/starter-set/grilling/SKILL.md +30 -0
- package/src/starter-set/grilling/agents/openai.yaml +3 -0
- package/src/starter-set.js +17 -0
- package/src/state.js +157 -0
- package/src/sync-library.js +293 -0
- package/src/sync-state.js +120 -0
- package/src/sync.js +482 -0
- package/src/telegram.js +1061 -0
- package/src/terminal.js +57 -0
- package/src/tmux.js +119 -0
- package/src/worker.js +928 -0
- package/src/workspace.js +500 -0
package/src/blocks.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ASSISTANT_BLOCKS_MAX,
|
|
3
|
+
ASSISTANT_TABLE_COLUMNS_MAX,
|
|
4
|
+
ASSISTANT_TEXT_MAX_BYTES,
|
|
5
|
+
} from "./contract.js";
|
|
6
|
+
|
|
7
|
+
const HEADING = /^(#{1,6})\s+(.+)$/;
|
|
8
|
+
const BULLET = /^ {0,3}[-*+]\s+(.+)$/;
|
|
9
|
+
const ORDERED = /^ {0,3}\d{1,9}[.)]\s+(.+)$/;
|
|
10
|
+
const TABLE_ROW = /^\s*\|.*\|\s*$/;
|
|
11
|
+
const TABLE_RULE = /^\s*\|(\s*:?-+:?\s*\|)+\s*$/;
|
|
12
|
+
const FENCE = /^```(.*)$/;
|
|
13
|
+
const FORMULA = "$$";
|
|
14
|
+
|
|
15
|
+
function cellsOf(line) {
|
|
16
|
+
return line
|
|
17
|
+
.trim()
|
|
18
|
+
.replace(/^\|/, "")
|
|
19
|
+
.replace(/\|$/, "")
|
|
20
|
+
.split("|")
|
|
21
|
+
.map((cell) => cell.trim());
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function tableAt(lines, start) {
|
|
25
|
+
if (!TABLE_ROW.test(lines[start]) || !TABLE_RULE.test(lines[start + 1] ?? "")) return null;
|
|
26
|
+
const headers = cellsOf(lines[start]);
|
|
27
|
+
if (headers.length === 0 || headers.length > ASSISTANT_TABLE_COLUMNS_MAX) return null;
|
|
28
|
+
if (headers.some((header) => header === "")) return null;
|
|
29
|
+
if (cellsOf(lines[start + 1]).length !== headers.length) return null;
|
|
30
|
+
const rows = [];
|
|
31
|
+
let index = start + 2;
|
|
32
|
+
while (index < lines.length && TABLE_ROW.test(lines[index])) {
|
|
33
|
+
const cells = cellsOf(lines[index]);
|
|
34
|
+
if (cells.length !== headers.length || cells.some((cell) => cell === "")) return null;
|
|
35
|
+
rows.push(cells);
|
|
36
|
+
index += 1;
|
|
37
|
+
}
|
|
38
|
+
if (rows.length === 0) return null;
|
|
39
|
+
return { block: { type: "table", headers, rows }, next: index };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function fencedAt(lines, start, fence) {
|
|
43
|
+
let index = start + 1;
|
|
44
|
+
const body = [];
|
|
45
|
+
while (index < lines.length && lines[index].trim() !== fence) {
|
|
46
|
+
body.push(lines[index]);
|
|
47
|
+
index += 1;
|
|
48
|
+
}
|
|
49
|
+
return { body, next: index < lines.length ? index + 1 : index };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function listAt(lines, start, ordered) {
|
|
53
|
+
const pattern = ordered ? ORDERED : BULLET;
|
|
54
|
+
const items = [];
|
|
55
|
+
let index = start;
|
|
56
|
+
while (index < lines.length) {
|
|
57
|
+
const item = pattern.exec(lines[index]);
|
|
58
|
+
if (item === null) break;
|
|
59
|
+
const named = item[1].trim();
|
|
60
|
+
if (named !== "") items.push(named);
|
|
61
|
+
index += 1;
|
|
62
|
+
}
|
|
63
|
+
if (items.length === 0) return null;
|
|
64
|
+
return { block: { type: "list", ordered, items }, next: index };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function assistantBlocks(text) {
|
|
68
|
+
if (typeof text !== "string") return null;
|
|
69
|
+
const lines = text.split("\n");
|
|
70
|
+
const blocks = [];
|
|
71
|
+
let paragraph = [];
|
|
72
|
+
const endParagraph = () => {
|
|
73
|
+
const joined = paragraph.join("\n").trim();
|
|
74
|
+
paragraph = [];
|
|
75
|
+
if (joined !== "") blocks.push({ type: "paragraph", text: joined });
|
|
76
|
+
};
|
|
77
|
+
let index = 0;
|
|
78
|
+
while (index < lines.length) {
|
|
79
|
+
const line = lines[index];
|
|
80
|
+
if (line.trim() === "") {
|
|
81
|
+
endParagraph();
|
|
82
|
+
index += 1;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const fence = FENCE.exec(line.trim());
|
|
86
|
+
if (fence !== null) {
|
|
87
|
+
endParagraph();
|
|
88
|
+
const code = fencedAt(lines, index, "```");
|
|
89
|
+
const body = code.body.join("\n");
|
|
90
|
+
const language = fence[1].trim();
|
|
91
|
+
if (body.trim() !== "") {
|
|
92
|
+
blocks.push({ type: "code", language: language === "" ? null : language, text: body });
|
|
93
|
+
}
|
|
94
|
+
index = code.next;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (line.trim() === FORMULA) {
|
|
98
|
+
endParagraph();
|
|
99
|
+
const formula = fencedAt(lines, index, FORMULA);
|
|
100
|
+
const expression = formula.body.join("\n").trim();
|
|
101
|
+
if (expression !== "") blocks.push({ type: "formula", expression });
|
|
102
|
+
index = formula.next;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const heading = HEADING.exec(line);
|
|
106
|
+
if (heading !== null) {
|
|
107
|
+
endParagraph();
|
|
108
|
+
const named = heading[2].trim();
|
|
109
|
+
if (named !== "") blocks.push({ type: "heading", level: heading[1].length, text: named });
|
|
110
|
+
index += 1;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const table = tableAt(lines, index);
|
|
114
|
+
if (table !== null) {
|
|
115
|
+
endParagraph();
|
|
116
|
+
blocks.push(table.block);
|
|
117
|
+
index = table.next;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const list = BULLET.test(line)
|
|
121
|
+
? listAt(lines, index, false)
|
|
122
|
+
: ORDERED.test(line)
|
|
123
|
+
? listAt(lines, index, true)
|
|
124
|
+
: null;
|
|
125
|
+
if (list !== null) {
|
|
126
|
+
endParagraph();
|
|
127
|
+
blocks.push(list.block);
|
|
128
|
+
index = list.next;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
paragraph.push(line);
|
|
132
|
+
index += 1;
|
|
133
|
+
}
|
|
134
|
+
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;
|
|
156
|
+
}
|
package/src/bridge.js
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { chmod, mkdir, rm } from "node:fs/promises";
|
|
3
|
+
import { createServer } from "node:net";
|
|
4
|
+
import { dirname } from "node:path";
|
|
5
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
6
|
+
import { readFrames, writeFrame } from "./channel.js";
|
|
7
|
+
import {
|
|
8
|
+
AGENT_OPERATION_META,
|
|
9
|
+
BRIDGE_RECONNECT_MAX_MS,
|
|
10
|
+
BRIDGE_RECONNECT_MS,
|
|
11
|
+
KIT_CONTRACT_VERSION,
|
|
12
|
+
MCP_CLIENT_CAPABILITIES_META,
|
|
13
|
+
MCP_CLIENT_INFO_META,
|
|
14
|
+
MCP_PROTOCOL_VERSION,
|
|
15
|
+
MCP_PROTOCOL_VERSION_META,
|
|
16
|
+
TURN_OUTCOMES,
|
|
17
|
+
} from "./contract.js";
|
|
18
|
+
import { operationId } from "./operation.js";
|
|
19
|
+
import { houseFailed } from "./refusal.js";
|
|
20
|
+
|
|
21
|
+
function bridgedFrame(request, operation) {
|
|
22
|
+
if (typeof request.method !== "string") return request;
|
|
23
|
+
const params = typeof request.params === "object" && request.params !== null ? request.params : {};
|
|
24
|
+
const declared = typeof params._meta === "object" && params._meta !== null ? params._meta : {};
|
|
25
|
+
const meta = {
|
|
26
|
+
[MCP_PROTOCOL_VERSION_META]: MCP_PROTOCOL_VERSION,
|
|
27
|
+
[MCP_CLIENT_INFO_META]: { name: "house-kit", version: KIT_CONTRACT_VERSION },
|
|
28
|
+
[MCP_CLIENT_CAPABILITIES_META]: {},
|
|
29
|
+
...declared,
|
|
30
|
+
};
|
|
31
|
+
delete meta[AGENT_OPERATION_META];
|
|
32
|
+
if (operation !== null) meta[AGENT_OPERATION_META] = operation;
|
|
33
|
+
return { ...request, params: { ...params, _meta: meta } };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function calledTool(request) {
|
|
37
|
+
if (request.method !== "tools/call") return null;
|
|
38
|
+
const name = request.params?.name;
|
|
39
|
+
return typeof name === "string" ? name : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function openBridge(options) {
|
|
43
|
+
await mkdir(dirname(options.socket), { recursive: true, mode: 0o700 });
|
|
44
|
+
await rm(options.socket, { force: true });
|
|
45
|
+
const state = {
|
|
46
|
+
assistant: [],
|
|
47
|
+
stop: null,
|
|
48
|
+
failure: null,
|
|
49
|
+
launcher: null,
|
|
50
|
+
closed: false,
|
|
51
|
+
questions: new Map(),
|
|
52
|
+
interrupts: new Set(),
|
|
53
|
+
};
|
|
54
|
+
const connections = new Set();
|
|
55
|
+
const clients = new Map();
|
|
56
|
+
const incarnation = randomUUID();
|
|
57
|
+
const mutates = options.mutates ?? (() => false);
|
|
58
|
+
const lifetime = new AbortController();
|
|
59
|
+
const signal = options.signal === undefined
|
|
60
|
+
? lifetime.signal
|
|
61
|
+
: AbortSignal.any([lifetime.signal, options.signal]);
|
|
62
|
+
let depart;
|
|
63
|
+
const departed = new Promise((resolve) => {
|
|
64
|
+
depart = resolve;
|
|
65
|
+
});
|
|
66
|
+
const endSession = () => {
|
|
67
|
+
lifetime.abort();
|
|
68
|
+
depart();
|
|
69
|
+
};
|
|
70
|
+
options.signal?.addEventListener("abort", endSession, { once: true });
|
|
71
|
+
if (options.signal?.aborted) endSession();
|
|
72
|
+
|
|
73
|
+
function deliverAnswer(pending) {
|
|
74
|
+
if (state.launcher === null || pending.answer === undefined) return;
|
|
75
|
+
writeFrame(state.launcher, { type: "answered", delivery: pending.delivery, ...pending.answer });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function unsupportedQuestion(pending) {
|
|
79
|
+
state.questions.delete(pending.delivery);
|
|
80
|
+
pending.answer = { unsupported: true };
|
|
81
|
+
deliverAnswer(pending);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function admitQuestion(pending) {
|
|
85
|
+
if (typeof options.question !== "function") {
|
|
86
|
+
unsupportedQuestion(pending);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
let backoff = BRIDGE_RECONNECT_MS;
|
|
90
|
+
while (!signal.aborted) {
|
|
91
|
+
try {
|
|
92
|
+
const admitted = await options.question(pending.form, signal);
|
|
93
|
+
if (signal.aborted) return;
|
|
94
|
+
pending.interaction = admitted.interaction;
|
|
95
|
+
if (admitted.state === "settled") {
|
|
96
|
+
unsupportedQuestion(pending);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (pending.ceremony !== null) await relaySecretInput(pending);
|
|
100
|
+
return;
|
|
101
|
+
} catch (refusal) {
|
|
102
|
+
if (signal.aborted) return;
|
|
103
|
+
if (houseFailed(refusal)) {
|
|
104
|
+
try {
|
|
105
|
+
await delay(backoff, undefined, { signal });
|
|
106
|
+
} catch {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
backoff = Math.min(backoff * 2, BRIDGE_RECONNECT_MAX_MS);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
unsupportedQuestion(pending);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function relaySecretInput(pending) {
|
|
119
|
+
if (typeof options.secret !== "function") {
|
|
120
|
+
unsupportedQuestion(pending);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
let backoff = BRIDGE_RECONNECT_MS;
|
|
124
|
+
while (!signal.aborted && state.questions.get(pending.delivery) === pending) {
|
|
125
|
+
let relayed;
|
|
126
|
+
try {
|
|
127
|
+
relayed = await options.secret(pending.interaction, pending.ceremony, signal);
|
|
128
|
+
backoff = BRIDGE_RECONNECT_MS;
|
|
129
|
+
} catch (refusal) {
|
|
130
|
+
if (signal.aborted) return;
|
|
131
|
+
if (!houseFailed(refusal)) {
|
|
132
|
+
unsupportedQuestion(pending);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
await delay(backoff, undefined, { signal });
|
|
137
|
+
} catch {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
backoff = Math.min(backoff * 2, BRIDGE_RECONNECT_MAX_MS);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (signal.aborted) return;
|
|
144
|
+
if (relayed.collected === null) continue;
|
|
145
|
+
pending.answer = {
|
|
146
|
+
interaction: pending.interaction,
|
|
147
|
+
provider_turn: pending.form.provider_turn,
|
|
148
|
+
correlation: pending.form.correlation,
|
|
149
|
+
response: { action: "accept", content: relayed.collected },
|
|
150
|
+
};
|
|
151
|
+
deliverAnswer(pending);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function asked(frame) {
|
|
157
|
+
if (
|
|
158
|
+
typeof frame.delivery !== "string" ||
|
|
159
|
+
typeof frame.form !== "object" ||
|
|
160
|
+
frame.form === null
|
|
161
|
+
) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const held = state.questions.get(frame.delivery);
|
|
165
|
+
if (held !== undefined) {
|
|
166
|
+
deliverAnswer(held);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const pending = {
|
|
170
|
+
delivery: frame.delivery,
|
|
171
|
+
form: frame.form,
|
|
172
|
+
ceremony: frame.ceremony ?? null,
|
|
173
|
+
interaction: null,
|
|
174
|
+
answer: undefined,
|
|
175
|
+
};
|
|
176
|
+
state.questions.set(frame.delivery, pending);
|
|
177
|
+
void admitQuestion(pending);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function applied(frame) {
|
|
181
|
+
const pending = state.questions.get(frame.delivery);
|
|
182
|
+
if (pending === undefined) return;
|
|
183
|
+
state.questions.delete(frame.delivery);
|
|
184
|
+
if (pending.interaction === null || typeof options.settle !== "function") return;
|
|
185
|
+
void options.settle(pending.interaction, frame.outcome === "expired" ? "expired" : "resolved").catch((refusal) => {
|
|
186
|
+
if (!signal.aborted) throw refusal;
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function fromLauncher(frame) {
|
|
191
|
+
if (frame.type === "question") {
|
|
192
|
+
asked(frame);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (frame.type === "question_applied" && typeof frame.delivery === "string") {
|
|
196
|
+
applied(frame);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (frame.type === "assistant" && typeof frame.text === "string") {
|
|
200
|
+
state.assistant.push(frame.text);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (frame.type === "draft") {
|
|
204
|
+
if (typeof frame.text === "string" && frame.text !== "") options.draft?.(frame.text);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
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
|
+
});
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (
|
|
221
|
+
frame.type === "interrupt_applied" &&
|
|
222
|
+
typeof frame.interrupt === "string" &&
|
|
223
|
+
["interrupted", "nothing_to_interrupt"].includes(frame.outcome) &&
|
|
224
|
+
state.interrupts.has(frame.interrupt)
|
|
225
|
+
) {
|
|
226
|
+
const reported = options.interrupt?.(frame.interrupt, frame.outcome);
|
|
227
|
+
if (reported === undefined) state.interrupts.delete(frame.interrupt);
|
|
228
|
+
else void Promise.resolve(reported).finally(() => state.interrupts.delete(frame.interrupt));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (frame.type === "turn_ended") {
|
|
232
|
+
state.stop = {
|
|
233
|
+
providerContinuation: frame.provider_continuation === true,
|
|
234
|
+
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
|
+
stopOutcome: TURN_OUTCOMES.includes(frame.stop_outcome) ? frame.stop_outcome : null,
|
|
236
|
+
};
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (frame.type === "failed") {
|
|
240
|
+
state.failure ??= {
|
|
241
|
+
code: typeof frame.code === "string" ? frame.code : "kit_adapter_failed",
|
|
242
|
+
detail: typeof frame.detail === "string" ? frame.detail : null,
|
|
243
|
+
};
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (frame.type === "exiting") endSession();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function deliver(client, pending) {
|
|
250
|
+
if (client.connection === null || pending.answer === undefined) return;
|
|
251
|
+
writeFrame(client.connection, {
|
|
252
|
+
type: "mcp",
|
|
253
|
+
delivery: pending.delivery,
|
|
254
|
+
frame: pending.answer,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function forward(client, pending, mutation) {
|
|
259
|
+
let backoff = BRIDGE_RECONNECT_MS;
|
|
260
|
+
while (!signal.aborted) {
|
|
261
|
+
try {
|
|
262
|
+
if (!(await options.alive())) {
|
|
263
|
+
endSession();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (signal.aborted) return;
|
|
267
|
+
const answer = await options.forward(JSON.parse(pending.request), signal);
|
|
268
|
+
if (signal.aborted) return;
|
|
269
|
+
pending.answer = answer;
|
|
270
|
+
deliver(client, pending);
|
|
271
|
+
return;
|
|
272
|
+
} catch (refusal) {
|
|
273
|
+
if (signal.aborted) return;
|
|
274
|
+
if (mutation && houseFailed(refusal)) {
|
|
275
|
+
try {
|
|
276
|
+
await delay(backoff, undefined, { signal });
|
|
277
|
+
} catch {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
backoff = Math.min(backoff * 2, BRIDGE_RECONNECT_MAX_MS);
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
pending.answer = pending.id === undefined ? null : {
|
|
284
|
+
jsonrpc: "2.0",
|
|
285
|
+
id: pending.id,
|
|
286
|
+
error: { code: -32000, message: refusal.code ?? "kit_house_unavailable" },
|
|
287
|
+
};
|
|
288
|
+
deliver(client, pending);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function fromMcp(client, connection, frame) {
|
|
295
|
+
if (client.connection !== connection) return;
|
|
296
|
+
if (frame.type === "ack") {
|
|
297
|
+
const pending = client.pending.get(frame.delivery);
|
|
298
|
+
if (pending?.answer === undefined) return;
|
|
299
|
+
client.pending.delete(frame.delivery);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (
|
|
303
|
+
frame.type !== "mcp" || typeof frame.delivery !== "string" ||
|
|
304
|
+
typeof frame.frame !== "object" || frame.frame === null
|
|
305
|
+
) {
|
|
306
|
+
connection.destroy();
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const original = JSON.stringify(frame.frame);
|
|
310
|
+
const retained = client.pending.get(frame.delivery);
|
|
311
|
+
if (retained !== undefined) {
|
|
312
|
+
if (retained.original !== original) {
|
|
313
|
+
connection.destroy();
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
deliver(client, retained);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (signal.aborted) {
|
|
320
|
+
writeFrame(connection, { type: "closed", code: "kit_bridge_unavailable" });
|
|
321
|
+
connection.end();
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const request = frame.frame;
|
|
325
|
+
const tool = calledTool(request);
|
|
326
|
+
const operation = tool !== null && mutates(tool) ? operationId() : null;
|
|
327
|
+
const pending = {
|
|
328
|
+
delivery: frame.delivery,
|
|
329
|
+
id: request.id,
|
|
330
|
+
original,
|
|
331
|
+
request: JSON.stringify(bridgedFrame(request, operation)),
|
|
332
|
+
answer: undefined,
|
|
333
|
+
};
|
|
334
|
+
client.pending.set(frame.delivery, pending);
|
|
335
|
+
void forward(client, pending, operation !== null);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function admit(peer, connection, frame) {
|
|
339
|
+
if (
|
|
340
|
+
frame.type !== "hello" ||
|
|
341
|
+
frame.nonce !== options.nonce ||
|
|
342
|
+
(frame.role !== "launcher" && frame.role !== "mcp") ||
|
|
343
|
+
(frame.role === "launcher" && state.launcher !== null) ||
|
|
344
|
+
(frame.role === "mcp" && (
|
|
345
|
+
typeof frame.client !== "string" ||
|
|
346
|
+
(frame.bridge !== null && frame.bridge !== incarnation)
|
|
347
|
+
))
|
|
348
|
+
) {
|
|
349
|
+
writeFrame(connection, { type: "refused", code: "kit_bridge_unauthorized" });
|
|
350
|
+
connection.destroy();
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
peer.role = frame.role;
|
|
354
|
+
if (frame.role !== "launcher") {
|
|
355
|
+
let client = clients.get(frame.client);
|
|
356
|
+
if (client === undefined) {
|
|
357
|
+
client = { connection: null, pending: new Map() };
|
|
358
|
+
clients.set(frame.client, client);
|
|
359
|
+
}
|
|
360
|
+
client.connection?.destroy();
|
|
361
|
+
client.connection = connection;
|
|
362
|
+
peer.client = client;
|
|
363
|
+
peer.name = frame.client;
|
|
364
|
+
writeFrame(connection, { type: "ready", bridge: incarnation });
|
|
365
|
+
for (const pending of client.pending.values()) deliver(client, pending);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
state.launcher = connection;
|
|
369
|
+
if (frame.adapter_session === true || options.start === undefined) {
|
|
370
|
+
writeFrame(connection, { type: "resumed" });
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
writeFrame(connection, { type: "start", ...options.start });
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const server = createServer((connection) => {
|
|
377
|
+
const peer = { role: null };
|
|
378
|
+
connections.add(connection);
|
|
379
|
+
connection.on("error", () => connection.destroy());
|
|
380
|
+
connection.on("close", () => {
|
|
381
|
+
connections.delete(connection);
|
|
382
|
+
if (state.launcher === connection) state.launcher = null;
|
|
383
|
+
if (peer.client?.connection !== connection) return;
|
|
384
|
+
peer.client.connection = null;
|
|
385
|
+
if (peer.client.pending.size === 0) clients.delete(peer.name);
|
|
386
|
+
});
|
|
387
|
+
readFrames(connection, {
|
|
388
|
+
onFrame: (frame) => {
|
|
389
|
+
if (peer.role === null) {
|
|
390
|
+
admit(peer, connection, frame);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (peer.role === "mcp") fromMcp(peer.client, connection, frame);
|
|
394
|
+
else fromLauncher(frame);
|
|
395
|
+
},
|
|
396
|
+
onRefusal: () => connection.destroy(),
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
await new Promise((resolve, reject) => {
|
|
401
|
+
server.once("error", reject);
|
|
402
|
+
server.listen(options.socket, resolve);
|
|
403
|
+
});
|
|
404
|
+
await chmod(options.socket, 0o600);
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
departed,
|
|
408
|
+
interrupt(command) {
|
|
409
|
+
if (state.launcher === null || state.stop !== null) return false;
|
|
410
|
+
if (state.interrupts.has(command.interrupt)) return true;
|
|
411
|
+
state.interrupts.add(command.interrupt);
|
|
412
|
+
writeFrame(state.launcher, { type: "interrupt", interrupt: command.interrupt });
|
|
413
|
+
return true;
|
|
414
|
+
},
|
|
415
|
+
answered(command) {
|
|
416
|
+
for (const pending of state.questions.values()) {
|
|
417
|
+
if (pending.interaction !== command.interaction) continue;
|
|
418
|
+
pending.answer = {
|
|
419
|
+
interaction: command.interaction,
|
|
420
|
+
provider_turn: command.providerTurn,
|
|
421
|
+
correlation: command.correlation,
|
|
422
|
+
response: command.response,
|
|
423
|
+
};
|
|
424
|
+
deliverAnswer(pending);
|
|
425
|
+
return true;
|
|
426
|
+
}
|
|
427
|
+
return false;
|
|
428
|
+
},
|
|
429
|
+
unsettledInteractions: () =>
|
|
430
|
+
[...state.questions.values()]
|
|
431
|
+
.map((pending) => pending.interaction)
|
|
432
|
+
.filter((interaction) => interaction !== null),
|
|
433
|
+
assistant: () => [...state.assistant],
|
|
434
|
+
stop: () => state.stop,
|
|
435
|
+
failure: () => state.failure,
|
|
436
|
+
stopRecovery: () => lifetime.abort(),
|
|
437
|
+
async close() {
|
|
438
|
+
if (state.closed) return;
|
|
439
|
+
state.closed = true;
|
|
440
|
+
endSession();
|
|
441
|
+
options.signal?.removeEventListener("abort", endSession);
|
|
442
|
+
for (const connection of connections) connection.destroy();
|
|
443
|
+
connections.clear();
|
|
444
|
+
clients.clear();
|
|
445
|
+
await new Promise((resolve) => server.close(resolve));
|
|
446
|
+
await rm(options.socket, { force: true });
|
|
447
|
+
},
|
|
448
|
+
};
|
|
449
|
+
}
|
package/src/bytes.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BYTE_GRANT_OPERATION_HEADER,
|
|
3
|
+
BYTE_GRANT_POST_BYTES_MAX,
|
|
4
|
+
} from "./contract.js";
|
|
5
|
+
import { houseRefusal, KitRefusal, throwHouseAnswer } from "./refusal.js";
|
|
6
|
+
|
|
7
|
+
function cancelled(signal, error) {
|
|
8
|
+
return signal?.aborted === true || error?.name === "AbortError";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function redirected(status) {
|
|
12
|
+
return status >= 300 && status < 400;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function downloadBody(response, signal) {
|
|
16
|
+
const chunks = [];
|
|
17
|
+
let size = 0;
|
|
18
|
+
try {
|
|
19
|
+
for await (const chunk of response.body ?? []) {
|
|
20
|
+
if (cancelled(signal)) throw new KitRefusal("kit_command_cancelled");
|
|
21
|
+
size += chunk.length;
|
|
22
|
+
if (size > BYTE_GRANT_POST_BYTES_MAX) throw new KitRefusal("original_bytes_exceeded");
|
|
23
|
+
chunks.push(chunk);
|
|
24
|
+
}
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (cancelled(signal, error)) throw new KitRefusal("kit_command_cancelled");
|
|
27
|
+
if (error instanceof KitRefusal) throw error;
|
|
28
|
+
throw new KitRefusal("kit_house_unavailable");
|
|
29
|
+
}
|
|
30
|
+
if (cancelled(signal)) throw new KitRefusal("kit_command_cancelled");
|
|
31
|
+
return Buffer.concat(chunks, size);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function transferBytes(request, dependencies = {}) {
|
|
35
|
+
if (request.method !== "GET" && request.method !== "POST") {
|
|
36
|
+
throw new KitRefusal("kit_byte_method_invalid", request.method);
|
|
37
|
+
}
|
|
38
|
+
if (typeof request.url !== "string" || request.url.length === 0) {
|
|
39
|
+
throw new KitRefusal("kit_byte_url_invalid");
|
|
40
|
+
}
|
|
41
|
+
if (typeof request.operation !== "string" || request.operation.length === 0) {
|
|
42
|
+
throw new KitRefusal("kit_byte_operation_invalid");
|
|
43
|
+
}
|
|
44
|
+
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) {
|
|
47
|
+
throw new KitRefusal("original_bytes_exceeded");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
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";
|
|
55
|
+
let response;
|
|
56
|
+
try {
|
|
57
|
+
response = await fetcher(request.url, {
|
|
58
|
+
method: request.method,
|
|
59
|
+
headers,
|
|
60
|
+
body: request.method === "POST" ? request.body : undefined,
|
|
61
|
+
redirect: "manual",
|
|
62
|
+
signal: request.signal,
|
|
63
|
+
});
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (cancelled(request.signal, error)) throw new KitRefusal("kit_command_cancelled");
|
|
66
|
+
throw new KitRefusal("kit_house_unavailable");
|
|
67
|
+
}
|
|
68
|
+
if (cancelled(request.signal)) throw new KitRefusal("kit_command_cancelled");
|
|
69
|
+
if (redirected(response.status)) {
|
|
70
|
+
throw new KitRefusal("kit_byte_redirect_refused");
|
|
71
|
+
}
|
|
72
|
+
if (request.method === "GET") {
|
|
73
|
+
if (response.status === 202) {
|
|
74
|
+
throwHouseAnswer(202, await response.json().catch(() => null));
|
|
75
|
+
}
|
|
76
|
+
if (!response.ok) throw houseRefusal(response.status, await response.json().catch(() => null));
|
|
77
|
+
return { bytes: await downloadBody(response, request.signal) };
|
|
78
|
+
}
|
|
79
|
+
let answer = null;
|
|
80
|
+
try {
|
|
81
|
+
answer = await response.json();
|
|
82
|
+
} catch {
|
|
83
|
+
answer = null;
|
|
84
|
+
}
|
|
85
|
+
if (cancelled(request.signal)) throw new KitRefusal("kit_command_cancelled");
|
|
86
|
+
throwHouseAnswer(response.status, answer);
|
|
87
|
+
if (answer === null) throw new KitRefusal("kit_house_unavailable");
|
|
88
|
+
return answer;
|
|
89
|
+
}
|