@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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1696 -2
  3. package/bin/connect-linux.sh +127 -0
  4. package/bin/kit.js +1023 -0
  5. package/package.json +33 -3
  6. package/src/acp.js +99 -0
  7. package/src/adapter-mcp.js +44 -0
  8. package/src/adapter.js +252 -0
  9. package/src/agent-logins.js +65 -0
  10. package/src/agents.js +449 -0
  11. package/src/apply.js +29 -0
  12. package/src/attach.js +52 -0
  13. package/src/binding.js +90 -0
  14. package/src/blocks.js +156 -0
  15. package/src/bridge.js +449 -0
  16. package/src/bytes.js +89 -0
  17. package/src/channel.js +37 -0
  18. package/src/command.js +109 -0
  19. package/src/configuration.js +465 -0
  20. package/src/connector-login.js +91 -0
  21. package/src/connector.js +107 -0
  22. package/src/continuation.js +61 -0
  23. package/src/contract.js +363 -0
  24. package/src/conversation-originals.js +143 -0
  25. package/src/custom/authorization.js +63 -0
  26. package/src/declaration.js +35 -0
  27. package/src/delivery.js +191 -0
  28. package/src/directory.js +102 -0
  29. package/src/draft.js +89 -0
  30. package/src/google/calendar.js +177 -0
  31. package/src/google/declared.js +146 -0
  32. package/src/google/drive.js +435 -0
  33. package/src/google/gateway.js +166 -0
  34. package/src/google/gmail.js +249 -0
  35. package/src/google/id.js +1 -0
  36. package/src/google/oauth.js +273 -0
  37. package/src/google/source.js +25 -0
  38. package/src/hook.js +376 -0
  39. package/src/host.js +121 -0
  40. package/src/house-main.js +79 -0
  41. package/src/house.js +1085 -0
  42. package/src/inbound.js +175 -0
  43. package/src/instance/google.js +1 -0
  44. package/src/launcher-main.js +19 -0
  45. package/src/launcher.js +276 -0
  46. package/src/local-files.js +113 -0
  47. package/src/login.js +274 -0
  48. package/src/mcp.js +71 -0
  49. package/src/migrator.js +248 -0
  50. package/src/mount.js +582 -0
  51. package/src/operation.js +20 -0
  52. package/src/original.js +631 -0
  53. package/src/peer.js +124 -0
  54. package/src/plan.js +57 -0
  55. package/src/question.js +316 -0
  56. package/src/refusal.js +79 -0
  57. package/src/repository.js +521 -0
  58. package/src/session-bin/house +2 -0
  59. package/src/settlement.js +57 -0
  60. package/src/setup.js +185 -0
  61. package/src/source-originals.js +116 -0
  62. package/src/source.js +286 -0
  63. package/src/starter-set/grilling/LICENSE +21 -0
  64. package/src/starter-set/grilling/SKILL.md +30 -0
  65. package/src/starter-set/grilling/agents/openai.yaml +3 -0
  66. package/src/starter-set.js +17 -0
  67. package/src/state.js +157 -0
  68. package/src/sync-library.js +293 -0
  69. package/src/sync-state.js +116 -0
  70. package/src/sync.js +477 -0
  71. package/src/telegram.js +1045 -0
  72. package/src/terminal.js +57 -0
  73. package/src/tmux.js +119 -0
  74. package/src/worker.js +756 -0
  75. package/src/workspace.js +500 -0
@@ -0,0 +1,500 @@
1
+ import { access, mkdir, readFile, rm } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { readConfiguration } from "./configuration.js";
4
+ import {
5
+ CGROUP_CONTROLLERS,
6
+ HOST_MOUNT_INFO,
7
+ LOOP_CONTROL,
8
+ WORKSPACE_ACQUIRE_MAX,
9
+ WORKSPACE_COMMAND_TIMEOUT_MS,
10
+ WORKSPACE_DISK_IMAGE,
11
+ WORKSPACE_DISK_MOUNT,
12
+ WORKSPACE_KIT_HOME,
13
+ WORKSPACE_MOUNT_OPTIONS,
14
+ WORKSPACE_LABEL,
15
+ WORKSPACE_MOUNT,
16
+ WORKSPACE_OPERATION_WITHDRAWN,
17
+ WORKSPACE_POLL_MS,
18
+ WORKSPACE_READY_ATTEMPTS,
19
+ WORKSPACE_READY_POLL_MS,
20
+ WORKSPACE_STOP_TIMEOUT_SECONDS,
21
+ WORKSPACE_SYSTEM_RESERVE_BYTES,
22
+ } from "./contract.js";
23
+ import { runCommand } from "./command.js";
24
+ import {
25
+ acquireWorkspaces,
26
+ maintainControl,
27
+ openSession,
28
+ releaseControl,
29
+ reportWorkspaceOutcome,
30
+ } from "./house.js";
31
+ import { KitRefusal } from "./refusal.js";
32
+
33
+ const REQUIRED_CONTROLLERS = ["cpu", "memory"];
34
+ const MEMINFO = "/proc/meminfo";
35
+ const MEM_AVAILABLE = /^MemAvailable:\s+(\d+)\s+kB$/m;
36
+
37
+ export function workspaceIdentity(operation) {
38
+ return `house-kit-${operation.name}-${operation.environment.slice(0, 8)}`;
39
+ }
40
+
41
+ export function workspaceEnforcement(maxima) {
42
+ return [
43
+ `cgroup-v2 cpu.max at ${maxima.cpu_millicores} millicores`,
44
+ `memory.max and swap at ${maxima.memory_bytes} bytes`,
45
+ `dedicated ext4 image of ${maxima.disk_bytes} bytes mounted as the only writable workspace`,
46
+ ].join(", ");
47
+ }
48
+
49
+ export function workspaceDirectory(root, identity) {
50
+ return join(root, identity);
51
+ }
52
+
53
+ export function workspaceImagePath(root, identity) {
54
+ return join(root, identity, WORKSPACE_DISK_IMAGE);
55
+ }
56
+
57
+ export function workspaceMountPath(root, identity) {
58
+ return join(root, identity, WORKSPACE_DISK_MOUNT);
59
+ }
60
+
61
+ export function workspaceMountArguments(root, identity, image) {
62
+ return [
63
+ "run",
64
+ "--rm",
65
+ "--privileged",
66
+ "--pid=host",
67
+ "--entrypoint",
68
+ "nsenter",
69
+ image,
70
+ "-t",
71
+ "1",
72
+ "-m",
73
+ "--",
74
+ "mount",
75
+ "-o",
76
+ WORKSPACE_MOUNT_OPTIONS,
77
+ workspaceImagePath(root, identity),
78
+ workspaceMountPath(root, identity),
79
+ ];
80
+ }
81
+
82
+ export function workspaceUnmountArguments(root, identity, image) {
83
+ return [
84
+ "run",
85
+ "--rm",
86
+ "--privileged",
87
+ "--pid=host",
88
+ "--entrypoint",
89
+ "nsenter",
90
+ image,
91
+ "-t",
92
+ "1",
93
+ "-m",
94
+ "--",
95
+ "umount",
96
+ workspaceMountPath(root, identity),
97
+ ];
98
+ }
99
+
100
+ export function mountedPaths(document) {
101
+ return new Set(
102
+ document
103
+ .split("\n")
104
+ .map((line) => line.split(" ")[4])
105
+ .filter((target) => target !== undefined && target !== ""),
106
+ );
107
+ }
108
+
109
+ export function workspaceCreateArguments(identity, operation, image, house, root) {
110
+ return [
111
+ "create",
112
+ "--name",
113
+ identity,
114
+ "--label",
115
+ `${WORKSPACE_LABEL}=${operation.environment}`,
116
+ "--cpus",
117
+ (operation.maxima.cpu_millicores / 1000).toFixed(3),
118
+ "--memory",
119
+ `${operation.maxima.memory_bytes}b`,
120
+ "--memory-swap",
121
+ `${operation.maxima.memory_bytes}b`,
122
+ "--restart",
123
+ "no",
124
+ "--mount",
125
+ `type=bind,source=${workspaceMountPath(root, identity)},target=${WORKSPACE_MOUNT}`,
126
+ "--env",
127
+ `HOUSE_KIT_HOME=${WORKSPACE_KIT_HOME}`,
128
+ "--env",
129
+ `HOUSE_KIT_HOUSE=${house}`,
130
+ "--workdir",
131
+ WORKSPACE_MOUNT,
132
+ image,
133
+ ];
134
+ }
135
+
136
+ export function memoryAvailable(document) {
137
+ const found = MEM_AVAILABLE.exec(document);
138
+ if (found === null) throw new KitRefusal("kit_host_unmeasurable", MEMINFO);
139
+ return Number(found[1]) * 1024;
140
+ }
141
+
142
+ function adapter(dependencies) {
143
+ return {
144
+ run: dependencies.run ?? runCommand,
145
+ read: dependencies.readFile ?? ((path) => readFile(path, "utf8")),
146
+ reachable: dependencies.access ?? access,
147
+ make: dependencies.mkdir ?? mkdir,
148
+ remove: dependencies.rm ?? rm,
149
+ wait: dependencies.wait ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
150
+ };
151
+ }
152
+
153
+ async function docker(host, args, stdin = "") {
154
+ return host.run({
155
+ command: ["docker", ...args],
156
+ environment: {},
157
+ stdin,
158
+ timeoutMs: WORKSPACE_COMMAND_TIMEOUT_MS,
159
+ });
160
+ }
161
+
162
+ export async function workspacePrerequisites(host, image) {
163
+ let controllers;
164
+ try {
165
+ controllers = await host.read(CGROUP_CONTROLLERS);
166
+ } catch {
167
+ return { supported: false, refusal: `${CGROUP_CONTROLLERS} is absent, so no cgroup v2 ceiling can be applied` };
168
+ }
169
+ const present = controllers.trim().split(/\s+/);
170
+ const missing = REQUIRED_CONTROLLERS.filter((controller) => !present.includes(controller));
171
+ if (missing.length > 0) {
172
+ return {
173
+ supported: false,
174
+ refusal: `the cgroup v2 controllers ${missing.join(" and ")} are not delegated to this host`,
175
+ };
176
+ }
177
+ try {
178
+ await host.reachable(LOOP_CONTROL);
179
+ } catch {
180
+ return {
181
+ supported: false,
182
+ refusal: `${LOOP_CONTROL} is absent, so no workspace volume can carry an enforced size`,
183
+ };
184
+ }
185
+ const version = await docker(host, ["version", "--format", "{{.Server.Version}}"]);
186
+ if (version.status !== 0 || version.stdout.trim() === "") {
187
+ return { supported: false, refusal: "the container runtime daemon did not answer a version" };
188
+ }
189
+ const formatter = await docker(host, [
190
+ "run",
191
+ "--rm",
192
+ "--entrypoint",
193
+ "sh",
194
+ image,
195
+ "-c",
196
+ "command -v mkfs.ext4",
197
+ ]);
198
+ if (formatter.status !== 0) {
199
+ return {
200
+ supported: false,
201
+ refusal: `the declared workspace image ${image} carries no mkfs.ext4, so no volume ceiling can be created`,
202
+ };
203
+ }
204
+ return { supported: true, runtime: `docker ${version.stdout.trim()}` };
205
+ }
206
+
207
+ function bootstrapConfiguration() {
208
+ return JSON.stringify({
209
+ availability: "continuous",
210
+ execution_authority: "allow",
211
+ migrators: [],
212
+ instances: [],
213
+ clients: [],
214
+ host: null,
215
+ });
216
+ }
217
+
218
+ function provisionArguments(root, identity, image, target, mode) {
219
+ return [
220
+ "run",
221
+ "--rm",
222
+ "--interactive",
223
+ "--mount",
224
+ `type=bind,source=${workspaceMountPath(root, identity)},target=${WORKSPACE_MOUNT}`,
225
+ "--entrypoint",
226
+ "sh",
227
+ image,
228
+ "-c",
229
+ `set -e; mkdir -p ${WORKSPACE_KIT_HOME}; chmod 700 ${WORKSPACE_KIT_HOME}; cat > ${target}; chmod ${mode} ${target}`,
230
+ ];
231
+ }
232
+
233
+ async function provisionWorkspaceState(host, root, identity, image, authority) {
234
+ for (const [target, mode, content] of [
235
+ [`${WORKSPACE_KIT_HOME}/credential`, "600", `${authority.credential}\n`],
236
+ [`${WORKSPACE_KIT_HOME}/kit.json`, "600", `${bootstrapConfiguration()}\n`],
237
+ ]) {
238
+ const written = await docker(
239
+ host,
240
+ provisionArguments(root, identity, image, target, mode),
241
+ content,
242
+ );
243
+ if (written.status !== 0) {
244
+ throw new KitRefusal("kit_workspace_unprovisioned", written.stderr.trim() || identity);
245
+ }
246
+ }
247
+ }
248
+
249
+ export async function workspaceMounted(host, root, identity) {
250
+ const target = workspaceMountPath(root, identity);
251
+ return mountedPaths(await host.read(HOST_MOUNT_INFO)).has(target);
252
+ }
253
+
254
+ async function mountWorkspace(host, settings, identity) {
255
+ if (await workspaceMounted(host, settings.root, identity)) return null;
256
+ const mounted = await docker(
257
+ host,
258
+ workspaceMountArguments(settings.root, identity, settings.image),
259
+ );
260
+ if (mounted.status !== 0) {
261
+ return `the workspace disk could not be mounted at its ceiling: ${mounted.stderr.trim()}`;
262
+ }
263
+ return null;
264
+ }
265
+
266
+ export async function createWorkspace(host, settings, operation) {
267
+ const prerequisites = await workspacePrerequisites(host, settings.image);
268
+ if (!prerequisites.supported) {
269
+ return { outcome: "unsupported", refusal: prerequisites.refusal };
270
+ }
271
+ const identity = workspaceIdentity(operation);
272
+ await host.make(workspaceMountPath(settings.root, identity), { recursive: true });
273
+ const formatted = await docker(host, [
274
+ "run",
275
+ "--rm",
276
+ "--mount",
277
+ `type=bind,source=${workspaceDirectory(settings.root, identity)},target=/image`,
278
+ "--entrypoint",
279
+ "sh",
280
+ settings.image,
281
+ "-c",
282
+ `set -e; rm -f /image/${WORKSPACE_DISK_IMAGE}; truncate -s ${operation.maxima.disk_bytes} /image/${WORKSPACE_DISK_IMAGE}; mkfs.ext4 -q -F /image/${WORKSPACE_DISK_IMAGE}`,
283
+ ]);
284
+ if (formatted.status !== 0) {
285
+ return {
286
+ outcome: "unsupported",
287
+ refusal: `the workspace disk image could not be created at its declared ceiling: ${formatted.stderr.trim()}`,
288
+ };
289
+ }
290
+ const mountRefusal = await mountWorkspace(host, settings, identity);
291
+ if (mountRefusal !== null) return { outcome: "unsupported", refusal: mountRefusal };
292
+ if (operation.authority !== undefined) {
293
+ await provisionWorkspaceState(host, settings.root, identity, settings.image, operation.authority);
294
+ }
295
+ const created = await docker(
296
+ host,
297
+ workspaceCreateArguments(identity, operation, settings.image, settings.house, settings.root),
298
+ );
299
+ if (created.status !== 0) {
300
+ return { outcome: "failed", refusal: `the workspace was refused: ${created.stderr.trim()}` };
301
+ }
302
+ return {
303
+ outcome: "stopped",
304
+ workspace: identity,
305
+ runtime: prerequisites.runtime,
306
+ enforcement: workspaceEnforcement(operation.maxima),
307
+ };
308
+ }
309
+
310
+ async function workspacePresent(host, identity) {
311
+ const inspected = await docker(host, ["container", "inspect", "--format", "{{.State.Status}}", identity]);
312
+ return inspected.status === 0 ? inspected.stdout.trim() : null;
313
+ }
314
+
315
+ export async function startWorkspace(host, settings, operation) {
316
+ const identity = operation.workspace ?? workspaceIdentity(operation);
317
+ if ((await workspacePresent(host, identity)) === null) return { outcome: "absent" };
318
+ const available = memoryAvailable(await host.read(MEMINFO));
319
+ if (available - operation.maxima.memory_bytes < WORKSPACE_SYSTEM_RESERVE_BYTES) {
320
+ return {
321
+ outcome: "failed",
322
+ refusal: `starting this workspace would leave less than the ${WORKSPACE_SYSTEM_RESERVE_BYTES} byte host reserve`,
323
+ };
324
+ }
325
+ const mountRefusal = await mountWorkspace(host, settings, identity);
326
+ if (mountRefusal !== null) return { outcome: "unsupported", refusal: mountRefusal };
327
+ const started = await docker(host, ["start", identity]);
328
+ if (started.status !== 0) {
329
+ return { outcome: "failed", refusal: `the workspace did not start: ${started.stderr.trim()}` };
330
+ }
331
+ const prerequisites = await workspacePrerequisites(host, settings.image);
332
+ if (!prerequisites.supported) {
333
+ return { outcome: "unsupported", refusal: prerequisites.refusal };
334
+ }
335
+ for (let attempt = 0; attempt < WORKSPACE_READY_ATTEMPTS; attempt += 1) {
336
+ const ready = await docker(host, [
337
+ "exec",
338
+ identity,
339
+ "sh",
340
+ "-c",
341
+ `test -d ${WORKSPACE_KIT_HOME}`,
342
+ ]);
343
+ if (ready.status === 0) {
344
+ return {
345
+ outcome: "ready",
346
+ workspace: identity,
347
+ runtime: prerequisites.runtime,
348
+ enforcement: workspaceEnforcement(operation.maxima),
349
+ };
350
+ }
351
+ if ((await workspacePresent(host, identity)) === null) return { outcome: "absent" };
352
+ await host.wait(WORKSPACE_READY_POLL_MS);
353
+ }
354
+ return { outcome: "failed", refusal: "the workspace never proved it accepts execution" };
355
+ }
356
+
357
+ export async function stopWorkspace(host, operation) {
358
+ const identity = operation.workspace ?? workspaceIdentity(operation);
359
+ if ((await workspacePresent(host, identity)) === null) return { outcome: "absent" };
360
+ const stopped = await docker(host, [
361
+ "stop",
362
+ "--timeout",
363
+ String(WORKSPACE_STOP_TIMEOUT_SECONDS),
364
+ identity,
365
+ ]);
366
+ if (stopped.status !== 0) {
367
+ return { outcome: "failed", refusal: `the workspace did not stop: ${stopped.stderr.trim()}` };
368
+ }
369
+ return {
370
+ outcome: "stopped",
371
+ workspace: identity,
372
+ runtime: operation.runtime ?? "docker",
373
+ enforcement: workspaceEnforcement(operation.maxima),
374
+ };
375
+ }
376
+
377
+ export async function deleteWorkspace(host, settings, operation) {
378
+ const identity = operation.workspace ?? workspaceIdentity(operation);
379
+ const present = await workspacePresent(host, identity);
380
+ const removed = present === null ? { status: 0 } : await docker(host, ["rm", "--force", identity]);
381
+ if (removed.status !== 0) {
382
+ return { outcome: "failed", refusal: `the workspace was not removed: ${removed.stderr.trim()}` };
383
+ }
384
+ if (await workspaceMounted(host, settings.root, identity)) {
385
+ const unmounted = await docker(
386
+ host,
387
+ workspaceUnmountArguments(settings.root, identity, settings.image),
388
+ );
389
+ if (unmounted.status !== 0) {
390
+ return {
391
+ outcome: "failed",
392
+ refusal: `the workspace disk was not unmounted: ${unmounted.stderr.trim()}`,
393
+ };
394
+ }
395
+ }
396
+ await host.remove(workspaceDirectory(settings.root, identity), { recursive: true, force: true });
397
+ return { outcome: "removed" };
398
+ }
399
+
400
+ export async function managedWorkspaces(host) {
401
+ const listed = await docker(host, [
402
+ "ps",
403
+ "--all",
404
+ "--filter",
405
+ `label=${WORKSPACE_LABEL}`,
406
+ "--format",
407
+ `{{.Names}} {{.Label "${WORKSPACE_LABEL}"}} {{.State}}`,
408
+ ]);
409
+ if (listed.status !== 0) {
410
+ throw new KitRefusal("kit_workspace_inventory_unavailable", listed.stderr.trim() || null);
411
+ }
412
+ return listed.stdout
413
+ .split("\n")
414
+ .map((line) => line.trim())
415
+ .filter((line) => line !== "")
416
+ .map((line) => {
417
+ const [name, environment, state] = line.split(/\s+/);
418
+ return { name, environment, state };
419
+ });
420
+ }
421
+
422
+ export async function reconcileWorkspaces(host, authorized) {
423
+ const expected = new Map(
424
+ authorized.map((workspace) => [workspace.environment, workspace.expected]),
425
+ );
426
+ const stopped = [];
427
+ for (const managed of await managedWorkspaces(host)) {
428
+ if (managed.state !== "running") continue;
429
+ if (expected.get(managed.environment) === "running") continue;
430
+ const settled = await docker(host, [
431
+ "stop",
432
+ "--timeout",
433
+ String(WORKSPACE_STOP_TIMEOUT_SECONDS),
434
+ managed.name,
435
+ ]);
436
+ if (settled.status === 0) stopped.push(managed.name);
437
+ }
438
+ return stopped;
439
+ }
440
+
441
+ export async function performOperation(host, settings, operation) {
442
+ if (operation.kind === "create") return createWorkspace(host, settings, operation);
443
+ if (operation.kind === "start") return startWorkspace(host, settings, operation);
444
+ if (operation.kind === "stop") return stopWorkspace(host, operation);
445
+ return deleteWorkspace(host, settings, operation);
446
+ }
447
+
448
+ export async function serveWorkspaces(options, dependencies = {}) {
449
+ const configuration = await readConfiguration(options.configuration);
450
+ if (configuration.host === null || configuration.host.image === null) {
451
+ throw new KitRefusal("kit_workspace_host_unconfigured", options.configuration);
452
+ }
453
+ const host = adapter(dependencies);
454
+ const settings = {
455
+ root: configuration.host.workspace,
456
+ image: configuration.host.image,
457
+ house: options.house,
458
+ };
459
+ const session = await openSession(options, dependencies);
460
+ const settled = [];
461
+ const withdrawn = [];
462
+ let reconciled = [];
463
+ const passes = options.passes ?? Number.POSITIVE_INFINITY;
464
+ for (let pass = 0; pass < passes; pass += 1) {
465
+ const work = await maintainControl(
466
+ session,
467
+ async (signal) => {
468
+ const acquired = await acquireWorkspaces(session, WORKSPACE_ACQUIRE_MAX, signal);
469
+ reconciled = await reconcileWorkspaces(host, acquired.workspaces);
470
+ for (const operation of acquired.operations) {
471
+ let report;
472
+ try {
473
+ report = await performOperation(host, settings, operation);
474
+ } catch (error) {
475
+ if (!(error instanceof KitRefusal)) throw error;
476
+ report = { outcome: "failed", refusal: error.message };
477
+ }
478
+ try {
479
+ await reportWorkspaceOutcome(session, operation.operation, report, signal);
480
+ } catch (error) {
481
+ if (!(error instanceof KitRefusal) || error.code !== WORKSPACE_OPERATION_WITHDRAWN) {
482
+ throw error;
483
+ }
484
+ withdrawn.push({ operation: operation.operation, kind: operation.kind });
485
+ continue;
486
+ }
487
+ settled.push({ operation: operation.operation, kind: operation.kind, ...report });
488
+ }
489
+ return acquired;
490
+ },
491
+ options.signal,
492
+ { release: false },
493
+ );
494
+ if (pass + 1 < passes && work.operations.length === 0) {
495
+ await host.wait(WORKSPACE_POLL_MS);
496
+ }
497
+ }
498
+ await releaseControl(session);
499
+ return { settled, reconciled, withdrawn };
500
+ }