@agentshouse/kit 0.0.1 → 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1696 -2
- package/bin/connect-linux.sh +127 -0
- package/bin/kit.js +1023 -0
- package/package.json +33 -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 +449 -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 +1085 -0
- package/src/inbound.js +175 -0
- package/src/instance/google.js +1 -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 +185 -0
- package/src/source-originals.js +116 -0
- package/src/source.js +286 -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 +116 -0
- package/src/sync.js +477 -0
- package/src/telegram.js +1045 -0
- package/src/terminal.js +57 -0
- package/src/tmux.js +119 -0
- package/src/worker.js +756 -0
- package/src/workspace.js +500 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { KitRefusal } from "../refusal.js";
|
|
2
|
+
import { gmailPage } from "./gmail.js";
|
|
3
|
+
import { calendarPage } from "./calendar.js";
|
|
4
|
+
import { drivePage } from "./drive.js";
|
|
5
|
+
import { declaredPage } from "./declared.js";
|
|
6
|
+
|
|
7
|
+
export async function googlePage(options, dependencies = {}) {
|
|
8
|
+
const common = {
|
|
9
|
+
directory: options.directory,
|
|
10
|
+
instance: options.instance,
|
|
11
|
+
client: options.client,
|
|
12
|
+
selection: options.service.selection,
|
|
13
|
+
cursor: options.cursor,
|
|
14
|
+
limit: options.limit,
|
|
15
|
+
backfillFloor: options.backfillFloor,
|
|
16
|
+
signal: options.signal,
|
|
17
|
+
};
|
|
18
|
+
if (options.service.service === "gmail") return gmailPage(common, dependencies);
|
|
19
|
+
if (options.service.service === "calendar") return calendarPage(common, dependencies);
|
|
20
|
+
if (options.service.service === "drive") return drivePage(common, dependencies);
|
|
21
|
+
if (options.service.declaration !== null) {
|
|
22
|
+
return declaredPage({ ...common, service: options.service }, dependencies);
|
|
23
|
+
}
|
|
24
|
+
throw new KitRefusal("google_service_unsupported", options.service.service);
|
|
25
|
+
}
|
package/src/hook.js
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { readFile, readdir, readlink, rm, stat } from "node:fs/promises";
|
|
2
|
+
import { dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
3
|
+
import { DECLARED_ID } from "./contract.js";
|
|
4
|
+
import { declarationReader } from "./declaration.js";
|
|
5
|
+
import { KitRefusal } from "./refusal.js";
|
|
6
|
+
import { readNote, writeNote, writeText } from "./state.js";
|
|
7
|
+
|
|
8
|
+
const SESSION_START_EVENT = "SessionStart";
|
|
9
|
+
const HOOK_INSTRUCTION_MAX = 16 * 1024;
|
|
10
|
+
const SETTINGS_MODE = 0o600;
|
|
11
|
+
const LINK_BUDGET = 32;
|
|
12
|
+
|
|
13
|
+
const { refuse, closed, text } = declarationReader("kit_hook_rejected");
|
|
14
|
+
|
|
15
|
+
function servedSessionStartHook(value) {
|
|
16
|
+
const served = closed(value, ["id", "version", "event", "instruction"], "session_start_hook");
|
|
17
|
+
const id = text(served.id, "session_start_hook.id", 128);
|
|
18
|
+
if (!DECLARED_ID.test(id)) refuse("session_start_hook.id");
|
|
19
|
+
if (served.event !== SESSION_START_EVENT) refuse("session_start_hook.event");
|
|
20
|
+
return {
|
|
21
|
+
id,
|
|
22
|
+
version: text(served.version, "session_start_hook.version", 64),
|
|
23
|
+
event: SESSION_START_EVENT,
|
|
24
|
+
instruction: text(served.instruction, "session_start_hook.instruction", HOOK_INSTRUCTION_MAX),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function hooksNamespace(directory) {
|
|
29
|
+
return join(directory, "state", "hooks");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function recordPath(directory) {
|
|
33
|
+
return join(directory, "state", "hooks.json");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function shellQuoted(value) {
|
|
37
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function instructionCommand(path) {
|
|
41
|
+
return `cat ${shellQuoted(path)}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function active(signal) {
|
|
45
|
+
if (signal?.aborted === true) throw new KitRefusal("kit_command_cancelled");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function plainDocument(value) {
|
|
49
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function pathSegments(path) {
|
|
53
|
+
return path.split(sep).filter((segment) => segment.length > 0);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function linkTarget(path, declared) {
|
|
57
|
+
try {
|
|
58
|
+
return await readlink(path);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error.code === "EINVAL" || error.code === "ENOENT" || error.code === "ENOTDIR") return null;
|
|
61
|
+
throw new KitRefusal("kit_client_settings_unreadable", declared);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function canonicalPath(declared) {
|
|
66
|
+
let remaining = pathSegments(resolve(declared));
|
|
67
|
+
let resolved = sep;
|
|
68
|
+
let links = LINK_BUDGET;
|
|
69
|
+
while (remaining.length > 0) {
|
|
70
|
+
const segment = remaining.shift();
|
|
71
|
+
if (segment === ".") continue;
|
|
72
|
+
if (segment === "..") {
|
|
73
|
+
resolved = dirname(resolved);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const candidate = join(resolved, segment);
|
|
77
|
+
const target = await linkTarget(candidate, declared);
|
|
78
|
+
if (target === null) {
|
|
79
|
+
resolved = candidate;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
links -= 1;
|
|
83
|
+
if (links < 0) throw new KitRefusal("kit_client_settings_refused", declared);
|
|
84
|
+
remaining = [...pathSegments(target), ...remaining];
|
|
85
|
+
if (isAbsolute(target)) resolved = sep;
|
|
86
|
+
}
|
|
87
|
+
return resolved;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function within(path, root) {
|
|
91
|
+
return path === root || path.startsWith(`${root}${sep}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function declaredDestination(installation, client, taken) {
|
|
95
|
+
const path = await canonicalPath(client.settings);
|
|
96
|
+
if (within(path, installation.home) || path === installation.configuration || taken.has(path)) {
|
|
97
|
+
throw new KitRefusal("kit_client_settings_refused", client.settings);
|
|
98
|
+
}
|
|
99
|
+
taken.add(path);
|
|
100
|
+
return { kind: client.kind, declared: client.settings, path, desired: true };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function recordedDestination(installation, recorded) {
|
|
104
|
+
const path = await canonicalPath(recorded.path);
|
|
105
|
+
if (path !== recorded.path || within(path, installation.home)) {
|
|
106
|
+
throw new KitRefusal("kit_client_settings_refused", recorded.path);
|
|
107
|
+
}
|
|
108
|
+
return { kind: null, declared: recorded.path, path, desired: false };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function hookSections(document, declared) {
|
|
112
|
+
const hooks = document.hooks === undefined ? {} : document.hooks;
|
|
113
|
+
if (!plainDocument(hooks)) throw new KitRefusal("kit_client_settings_unreadable", declared);
|
|
114
|
+
const groups = hooks[SESSION_START_EVENT] === undefined ? [] : hooks[SESSION_START_EVENT];
|
|
115
|
+
if (!Array.isArray(groups)) throw new KitRefusal("kit_client_settings_unreadable", declared);
|
|
116
|
+
return { hooks, groups };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function identityOf(status) {
|
|
120
|
+
return { device: status.dev, node: status.ino };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function readSettings(destination) {
|
|
124
|
+
let status;
|
|
125
|
+
try {
|
|
126
|
+
status = await stat(destination.path);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (error.code === "ENOENT") {
|
|
129
|
+
return { document: {}, contents: null, mode: SETTINGS_MODE, identity: null, present: false };
|
|
130
|
+
}
|
|
131
|
+
throw new KitRefusal("kit_client_settings_unreadable", destination.declared);
|
|
132
|
+
}
|
|
133
|
+
if (!status.isFile()) throw new KitRefusal("kit_client_settings_refused", destination.declared);
|
|
134
|
+
let stored;
|
|
135
|
+
try {
|
|
136
|
+
stored = await readFile(destination.path, "utf8");
|
|
137
|
+
} catch {
|
|
138
|
+
throw new KitRefusal("kit_client_settings_unreadable", destination.declared);
|
|
139
|
+
}
|
|
140
|
+
let document;
|
|
141
|
+
try {
|
|
142
|
+
document = JSON.parse(stored);
|
|
143
|
+
} catch {
|
|
144
|
+
throw new KitRefusal("kit_client_settings_unreadable", destination.declared);
|
|
145
|
+
}
|
|
146
|
+
if (!plainDocument(document)) {
|
|
147
|
+
throw new KitRefusal("kit_client_settings_unreadable", destination.declared);
|
|
148
|
+
}
|
|
149
|
+
hookSections(document, destination.declared);
|
|
150
|
+
return {
|
|
151
|
+
document,
|
|
152
|
+
contents: stored,
|
|
153
|
+
mode: status.mode & 0o777,
|
|
154
|
+
identity: identityOf(status),
|
|
155
|
+
present: true,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function prepared(destination, guard) {
|
|
160
|
+
const path = await canonicalPath(destination.declared);
|
|
161
|
+
if (path !== destination.path || within(path, guard.home) || path === guard.configuration) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
let status = null;
|
|
165
|
+
try {
|
|
166
|
+
status = await stat(path);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (error.code !== "ENOENT") return false;
|
|
169
|
+
}
|
|
170
|
+
if (status === null) return !destination.present;
|
|
171
|
+
return (
|
|
172
|
+
destination.present &&
|
|
173
|
+
status.isFile() &&
|
|
174
|
+
status.dev === destination.identity.device &&
|
|
175
|
+
status.ino === destination.identity.node
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function readRecord(directory) {
|
|
180
|
+
const path = recordPath(directory);
|
|
181
|
+
const stored = await readNote(path, null);
|
|
182
|
+
if (stored === null) return [];
|
|
183
|
+
const unreadable = () => {
|
|
184
|
+
throw new KitRefusal("kit_state_unreadable", path);
|
|
185
|
+
};
|
|
186
|
+
if (!plainDocument(stored) || !Array.isArray(stored.destinations)) unreadable();
|
|
187
|
+
return stored.destinations.map((entry) => {
|
|
188
|
+
if (!plainDocument(entry) || typeof entry.settings !== "string") unreadable();
|
|
189
|
+
if (!Array.isArray(entry.commands)) unreadable();
|
|
190
|
+
if (entry.commands.some((command) => typeof command !== "string")) unreadable();
|
|
191
|
+
return { path: entry.settings, commands: entry.commands };
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function recordOf(destinations) {
|
|
196
|
+
return {
|
|
197
|
+
destinations: destinations.map((destination) => ({
|
|
198
|
+
settings: destination.path,
|
|
199
|
+
commands: [...destination.commands],
|
|
200
|
+
})),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function sameRecord(one, other) {
|
|
205
|
+
return JSON.stringify(one) === JSON.stringify(other);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function ownedEntry(entry, commands) {
|
|
209
|
+
return (
|
|
210
|
+
plainDocument(entry) &&
|
|
211
|
+
entry.type === "command" &&
|
|
212
|
+
typeof entry.command === "string" &&
|
|
213
|
+
commands.has(entry.command)
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function keptGroup(group, commands) {
|
|
218
|
+
if (!plainDocument(group) || !Array.isArray(group.hooks)) return group;
|
|
219
|
+
const kept = group.hooks.filter((entry) => !ownedEntry(entry, commands));
|
|
220
|
+
if (kept.length === group.hooks.length) return group;
|
|
221
|
+
if (kept.length === 0) return null;
|
|
222
|
+
return { ...group, hooks: kept };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function reconciledSettings(destination, command) {
|
|
226
|
+
const settings = destination.document;
|
|
227
|
+
const { hooks, groups: declared } = hookSections(settings, destination.declared);
|
|
228
|
+
const kept = declared
|
|
229
|
+
.map((group) => keptGroup(group, destination.commands))
|
|
230
|
+
.filter((group) => group !== null);
|
|
231
|
+
const groups =
|
|
232
|
+
command === null ? kept : [...kept, { hooks: [{ type: "command", command }] }];
|
|
233
|
+
if (groups.length > 0) {
|
|
234
|
+
return { ...settings, hooks: { ...hooks, [SESSION_START_EVENT]: groups } };
|
|
235
|
+
}
|
|
236
|
+
const { [SESSION_START_EVENT]: _sessionStart, ...remaining } = hooks;
|
|
237
|
+
if (Object.keys(remaining).length > 0) return { ...settings, hooks: remaining };
|
|
238
|
+
const { hooks: _hooks, ...document } = settings;
|
|
239
|
+
return document;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function instructionPath(directory, id) {
|
|
243
|
+
return join(hooksNamespace(directory), `${id}.txt`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function pruneInstructions(directory, hook, signal) {
|
|
247
|
+
active(signal);
|
|
248
|
+
const namespace = hooksNamespace(directory);
|
|
249
|
+
if (hook === null) {
|
|
250
|
+
await rm(namespace, { recursive: true, force: true });
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const current = `${hook.id}.txt`;
|
|
254
|
+
for (const present of await readdir(namespace)) {
|
|
255
|
+
if (present !== current) await rm(join(namespace, present), { recursive: true });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function restored(written) {
|
|
260
|
+
const unrestored = [];
|
|
261
|
+
for (const target of [...written].reverse()) {
|
|
262
|
+
try {
|
|
263
|
+
if (target.present) await writeText(target.path, target.contents, undefined, target.mode);
|
|
264
|
+
else await rm(target.path, { force: true });
|
|
265
|
+
} catch {
|
|
266
|
+
unrestored.push(target.path);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (unrestored.length > 0) {
|
|
270
|
+
throw new KitRefusal("kit_client_settings_unrestored", unrestored.join(" "));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function writeSettings(targets, guard, signal) {
|
|
275
|
+
const written = [];
|
|
276
|
+
try {
|
|
277
|
+
for (const target of targets) {
|
|
278
|
+
if (!target.desired && !target.present) continue;
|
|
279
|
+
active(signal);
|
|
280
|
+
if (!(await prepared(target, guard))) {
|
|
281
|
+
throw new KitRefusal("kit_client_settings_refused", target.declared);
|
|
282
|
+
}
|
|
283
|
+
await writeNote(target.path, target.next, signal, target.mode);
|
|
284
|
+
written.push(target);
|
|
285
|
+
}
|
|
286
|
+
} catch (error) {
|
|
287
|
+
await restored(written);
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function writeReconciliation(plan, targets, hook, command, signal) {
|
|
293
|
+
const { directory, recorded, guard } = plan;
|
|
294
|
+
const intent = recordOf(targets);
|
|
295
|
+
if (!sameRecord(intent, recordOf(recorded))) {
|
|
296
|
+
active(signal);
|
|
297
|
+
await writeNote(recordPath(directory), intent, signal);
|
|
298
|
+
}
|
|
299
|
+
if (hook !== null) {
|
|
300
|
+
active(signal);
|
|
301
|
+
await writeText(instructionPath(directory, hook.id), `${hook.instruction}\n`, signal);
|
|
302
|
+
}
|
|
303
|
+
await writeSettings(targets, guard, signal);
|
|
304
|
+
const desired = targets.filter((target) => target.desired);
|
|
305
|
+
active(signal);
|
|
306
|
+
if (desired.length === 0) {
|
|
307
|
+
await rm(recordPath(directory), { force: true });
|
|
308
|
+
} else {
|
|
309
|
+
await writeNote(
|
|
310
|
+
recordPath(directory),
|
|
311
|
+
recordOf(desired.map((target) => ({ path: target.path, commands: new Set([command]) }))),
|
|
312
|
+
signal,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
await pruneInstructions(directory, hook, signal);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export async function planSessionStartHook(installation, clients, signal) {
|
|
319
|
+
const recorded = await readRecord(installation.directory);
|
|
320
|
+
const plan = {
|
|
321
|
+
directory: installation.directory,
|
|
322
|
+
declared: clients.length,
|
|
323
|
+
recorded,
|
|
324
|
+
targets: [],
|
|
325
|
+
guard: null,
|
|
326
|
+
};
|
|
327
|
+
if (clients.length === 0 && recorded.length === 0) return plan;
|
|
328
|
+
const home = await canonicalPath(installation.directory);
|
|
329
|
+
const configuration = await canonicalPath(installation.configuration);
|
|
330
|
+
plan.guard = { home, configuration };
|
|
331
|
+
const taken = new Set();
|
|
332
|
+
const destinations = new Map();
|
|
333
|
+
for (const client of clients) {
|
|
334
|
+
active(signal);
|
|
335
|
+
const destination = await declaredDestination({ home, configuration }, client, taken);
|
|
336
|
+
destinations.set(destination.path, { ...destination, commands: new Set() });
|
|
337
|
+
}
|
|
338
|
+
for (const entry of recorded) {
|
|
339
|
+
active(signal);
|
|
340
|
+
if (!destinations.has(entry.path)) {
|
|
341
|
+
const destination = await recordedDestination({ home }, entry);
|
|
342
|
+
destinations.set(destination.path, { ...destination, commands: new Set() });
|
|
343
|
+
}
|
|
344
|
+
const known = destinations.get(entry.path);
|
|
345
|
+
for (const previous of entry.commands) known.commands.add(previous);
|
|
346
|
+
}
|
|
347
|
+
plan.targets = [...destinations.values()];
|
|
348
|
+
for (const target of plan.targets) {
|
|
349
|
+
active(signal);
|
|
350
|
+
Object.assign(target, await readSettings(target));
|
|
351
|
+
}
|
|
352
|
+
return plan;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export async function commitSessionStartHook(plan, served, signal) {
|
|
356
|
+
if (plan.declared === 0 && plan.recorded.length === 0) return { installed: [], removed: [] };
|
|
357
|
+
const hook = plan.declared === 0 ? null : servedSessionStartHook(served);
|
|
358
|
+
const command =
|
|
359
|
+
hook === null ? null : instructionCommand(instructionPath(plan.directory, hook.id));
|
|
360
|
+
for (const target of plan.targets) {
|
|
361
|
+
if (target.desired && command !== null) target.commands.add(command);
|
|
362
|
+
target.next = reconciledSettings(target, target.desired ? command : null);
|
|
363
|
+
}
|
|
364
|
+
await writeReconciliation(plan, plan.targets, hook, command, signal);
|
|
365
|
+
return {
|
|
366
|
+
installed: plan.targets
|
|
367
|
+
.filter((target) => target.desired)
|
|
368
|
+
.map((target) => ({
|
|
369
|
+
kind: target.kind,
|
|
370
|
+
settings: target.declared,
|
|
371
|
+
id: hook.id,
|
|
372
|
+
version: hook.version,
|
|
373
|
+
})),
|
|
374
|
+
removed: plan.targets.filter((target) => !target.desired).map((target) => target.path),
|
|
375
|
+
};
|
|
376
|
+
}
|
package/src/host.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { readFile, statfs } from "node:fs/promises";
|
|
2
|
+
import { readConfiguration } from "./configuration.js";
|
|
3
|
+
import { HOST_CPU_SAMPLE_MS } from "./contract.js";
|
|
4
|
+
import { openSession, reportHostCapacity } from "./house.js";
|
|
5
|
+
import { KitRefusal } from "./refusal.js";
|
|
6
|
+
|
|
7
|
+
const PROC_STAT = "/proc/stat";
|
|
8
|
+
const PROC_MEMINFO = "/proc/meminfo";
|
|
9
|
+
const PROCESSOR_LINE = /^cpu[0-9]+\s/;
|
|
10
|
+
const AGGREGATE_LINE = /^cpu\s+(.*)$/;
|
|
11
|
+
const MEMINFO_LINE = /^(\w+):\s+(\d+)\s+kB$/;
|
|
12
|
+
|
|
13
|
+
function unmeasurable(source) {
|
|
14
|
+
throw new KitRefusal("kit_host_unmeasurable", source);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function readSource(read, path) {
|
|
18
|
+
try {
|
|
19
|
+
return await read(path);
|
|
20
|
+
} catch {
|
|
21
|
+
return unmeasurable(path);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function processorTime(document) {
|
|
26
|
+
const lines = document.split("\n");
|
|
27
|
+
const aggregate = lines.map((line) => AGGREGATE_LINE.exec(line)).find((match) => match !== null);
|
|
28
|
+
if (aggregate === null || aggregate === undefined) unmeasurable(PROC_STAT);
|
|
29
|
+
const fields = aggregate[1].trim().split(/\s+/).slice(0, 8).map(Number);
|
|
30
|
+
if (fields.length < 5 || fields.some((field) => !Number.isFinite(field))) {
|
|
31
|
+
unmeasurable(PROC_STAT);
|
|
32
|
+
}
|
|
33
|
+
const processors = lines.filter((line) => PROCESSOR_LINE.test(line)).length;
|
|
34
|
+
if (processors === 0) unmeasurable(PROC_STAT);
|
|
35
|
+
return {
|
|
36
|
+
processors,
|
|
37
|
+
busy: fields.reduce((total, field) => total + field, 0) - fields[3] - fields[4],
|
|
38
|
+
idle: fields[3] + fields[4],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function processors(read, wait) {
|
|
43
|
+
const first = processorTime(await readSource(read, PROC_STAT));
|
|
44
|
+
await wait(HOST_CPU_SAMPLE_MS);
|
|
45
|
+
const second = processorTime(await readSource(read, PROC_STAT));
|
|
46
|
+
const elapsed = second.busy + second.idle - first.busy - first.idle;
|
|
47
|
+
if (elapsed <= 0 || second.processors !== first.processors) unmeasurable(PROC_STAT);
|
|
48
|
+
const total = second.processors * 1000;
|
|
49
|
+
const used = Math.min(total, Math.round(((second.busy - first.busy) / elapsed) * total));
|
|
50
|
+
return { total, used, available: total - used };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function memory(document) {
|
|
54
|
+
const named = new Map(
|
|
55
|
+
document
|
|
56
|
+
.split("\n")
|
|
57
|
+
.map((line) => MEMINFO_LINE.exec(line))
|
|
58
|
+
.filter((match) => match !== null)
|
|
59
|
+
.map((match) => [match[1], Number(match[2]) * 1024]),
|
|
60
|
+
);
|
|
61
|
+
const total = named.get("MemTotal");
|
|
62
|
+
const available = named.get("MemAvailable");
|
|
63
|
+
if (total === undefined || available === undefined || total < 1 || available > total) {
|
|
64
|
+
unmeasurable(PROC_MEMINFO);
|
|
65
|
+
}
|
|
66
|
+
return { total, used: total - available, available };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function workspaceDisk(path, statistics) {
|
|
70
|
+
const total = statistics.blocks * statistics.bsize;
|
|
71
|
+
const used = (statistics.blocks - statistics.bfree) * statistics.bsize;
|
|
72
|
+
const available = statistics.bavail * statistics.bsize;
|
|
73
|
+
if (!Number.isSafeInteger(total) || total < 1 || used < 0 || available > total) {
|
|
74
|
+
unmeasurable(path);
|
|
75
|
+
}
|
|
76
|
+
return { path, total, used, available };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function measureHost(workspace, dependencies = {}) {
|
|
80
|
+
const read = dependencies.readFile ?? ((path) => readFile(path, "utf8"));
|
|
81
|
+
const statistics = dependencies.statfs ?? statfs;
|
|
82
|
+
const wait = dependencies.wait ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
83
|
+
const now = dependencies.now ?? (() => new Date());
|
|
84
|
+
const cpu = await processors(read, wait);
|
|
85
|
+
const ram = memory(await readSource(read, PROC_MEMINFO));
|
|
86
|
+
let filesystem;
|
|
87
|
+
try {
|
|
88
|
+
filesystem = await statistics(workspace);
|
|
89
|
+
} catch {
|
|
90
|
+
return unmeasurable(workspace);
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
observed_at: now().toISOString(),
|
|
94
|
+
cpu,
|
|
95
|
+
memory: ram,
|
|
96
|
+
disk: workspaceDisk(workspace, filesystem),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function declaredMaxima(maxima) {
|
|
101
|
+
return {
|
|
102
|
+
...(maxima.cpuMillicores === null ? {} : { cpu_millicores: maxima.cpuMillicores }),
|
|
103
|
+
...(maxima.memoryBytes === null ? {} : { memory_bytes: maxima.memoryBytes }),
|
|
104
|
+
...(maxima.diskBytes === null ? {} : { disk_bytes: maxima.diskBytes }),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function reportHost(options, dependencies = {}) {
|
|
109
|
+
const configuration = await readConfiguration(options.configuration);
|
|
110
|
+
if (configuration.host === null) {
|
|
111
|
+
throw new KitRefusal("kit_host_unconfigured", options.configuration);
|
|
112
|
+
}
|
|
113
|
+
const observed = await measureHost(configuration.host.workspace, dependencies);
|
|
114
|
+
const declaration =
|
|
115
|
+
configuration.host.maxima === null
|
|
116
|
+
? observed
|
|
117
|
+
: { ...observed, maxima: declaredMaxima(configuration.host.maxima) };
|
|
118
|
+
const session = await openSession(options, dependencies);
|
|
119
|
+
await reportHostCapacity(session, declaration);
|
|
120
|
+
return declaration;
|
|
121
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { BRIDGE_NONCE_ENVIRONMENT, BRIDGE_SOCKET_ENVIRONMENT } from "./contract.js";
|
|
3
|
+
import { bridgeCall } from "./mcp.js";
|
|
4
|
+
import { KitRefusal } from "./refusal.js";
|
|
5
|
+
|
|
6
|
+
const HELP = ["--help", "-h"];
|
|
7
|
+
const usage = (verb) => `usage: house ${verb} ['<arguments as one JSON object>' | - for standard input]`;
|
|
8
|
+
|
|
9
|
+
async function answered(method, params) {
|
|
10
|
+
const socket = process.env[BRIDGE_SOCKET_ENVIRONMENT];
|
|
11
|
+
const nonce = process.env[BRIDGE_NONCE_ENVIRONMENT];
|
|
12
|
+
if (socket === undefined || nonce === undefined) throw new KitRefusal("kit_bridge_unavailable");
|
|
13
|
+
const frame = await bridgeCall({ socket, nonce }, { method, params });
|
|
14
|
+
if (frame.error !== undefined) throw new KitRefusal(frame.error.message ?? "kit_house_refused");
|
|
15
|
+
return frame.result;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function listed() {
|
|
19
|
+
const tools = (await answered("tools/list", {}))?.tools;
|
|
20
|
+
if (!Array.isArray(tools)) throw new KitRefusal("kit_house_listing_unreadable");
|
|
21
|
+
return tools;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function standardInput() {
|
|
25
|
+
let text = "";
|
|
26
|
+
process.stdin.setEncoding("utf8");
|
|
27
|
+
for await (const chunk of process.stdin) text += chunk;
|
|
28
|
+
return text;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function argumentsOf(verb, text) {
|
|
32
|
+
if (text === undefined) return {};
|
|
33
|
+
let parsed = null;
|
|
34
|
+
try {
|
|
35
|
+
parsed = JSON.parse(text === "-" ? await standardInput() : text);
|
|
36
|
+
} catch {}
|
|
37
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
38
|
+
throw new KitRefusal("kit_house_arguments_invalid", usage(verb));
|
|
39
|
+
}
|
|
40
|
+
return parsed;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function located(verb, args) {
|
|
44
|
+
const local = (path) => (typeof path === "string" ? resolve(path) : path);
|
|
45
|
+
if (verb === "upload_attachment") return { ...args, path: local(args.path) };
|
|
46
|
+
if (verb === "append_record" && Array.isArray(args.attachments)) {
|
|
47
|
+
return { ...args, attachments: args.attachments.map(local) };
|
|
48
|
+
}
|
|
49
|
+
return args;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function house([verb, argument, ...rest]) {
|
|
53
|
+
if (verb === undefined || HELP.includes(verb)) {
|
|
54
|
+
const tools = await listed();
|
|
55
|
+
return { out: [usage("<verb>"), ...tools.map((tool) => `${tool.name}\n${tool.description}`)].join("\n\n") };
|
|
56
|
+
}
|
|
57
|
+
if (HELP.includes(argument)) {
|
|
58
|
+
const tool = (await listed()).find((one) => one.name === verb);
|
|
59
|
+
if (tool === undefined) throw new KitRefusal("kit_house_verb_unknown", verb);
|
|
60
|
+
return { out: [usage(verb), tool.description, JSON.stringify(tool.inputSchema, null, 2)].join("\n\n") };
|
|
61
|
+
}
|
|
62
|
+
if (rest.length > 0) throw new KitRefusal("kit_house_arguments_invalid", usage(verb));
|
|
63
|
+
const args = located(verb, await argumentsOf(verb, argument));
|
|
64
|
+
const result = await answered("tools/call", { name: verb, arguments: args });
|
|
65
|
+
const text = (result?.content ?? []).filter((item) => item.type === "text").map((item) => item.text).join("\n");
|
|
66
|
+
return result?.isError === true ? { err: text, status: 1 } : { out: text };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
house(process.argv.slice(2)).then(
|
|
70
|
+
({ out, err, status = 0 }) => {
|
|
71
|
+
if (out !== undefined) process.stdout.write(`${out}\n`);
|
|
72
|
+
if (err !== undefined) process.stderr.write(`${err}\n`);
|
|
73
|
+
process.exitCode = status;
|
|
74
|
+
},
|
|
75
|
+
(refusal) => {
|
|
76
|
+
process.stderr.write(`${refusal instanceof Error ? refusal.message : "kit_house_failed"}\n`);
|
|
77
|
+
process.exitCode = 1;
|
|
78
|
+
},
|
|
79
|
+
);
|