@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,107 @@
1
+ import { isAbsolute } from "node:path";
2
+ import {
3
+ ITEM_ATTACHMENTS_MAX,
4
+ ITEM_BODY_MAX_BYTES,
5
+ KIT_CONTRACT_VERSION,
6
+ ORIGINAL_NAME_MAX,
7
+ } from "./contract.js";
8
+ import { declarationReader } from "./declaration.js";
9
+ import { originalDescriptor } from "./original.js";
10
+ import { KitRefusal } from "./refusal.js";
11
+
12
+ const NAMESPACE = /^[a-z0-9][a-z0-9._-]*(\/[a-z0-9][a-z0-9._-]*)*$/;
13
+ const OBSERVED_AT = /^\d{4}-\d{2}-\d{2}T([01]\d|2[0-3]):[0-5]\d:[0-5]\d(\.\d{1,6})?Z$/;
14
+ const ITEM_FIELDS = [
15
+ "type",
16
+ "namespace",
17
+ "native_object_id",
18
+ "native_version",
19
+ "observed_at",
20
+ "body",
21
+ "author",
22
+ "url",
23
+ "attachments",
24
+ ];
25
+ const ATTACHMENT_FIELDS = ["url", "file", "name"];
26
+ const FILE_FIELDS = ["path", "bytes", "sha256", "media_type"];
27
+
28
+ const { refuse, closed, text, optionalText } = declarationReader("kit_connector_line_invalid");
29
+
30
+ function attachmentsOf(value) {
31
+ if (value === undefined) return [];
32
+ if (!Array.isArray(value) || value.length > ITEM_ATTACHMENTS_MAX) refuse("attachments");
33
+ return value.map((entry) => {
34
+ const attachment = closed(entry, ATTACHMENT_FIELDS, "attachments");
35
+ const name = optionalText(attachment.name, "attachments.name", 512);
36
+ const named = name === null ? {} : { name };
37
+ if (attachment.file === undefined) {
38
+ return { url: text(attachment.url, "attachments.url", 2048), ...named };
39
+ }
40
+ if (attachment.url !== undefined) refuse("attachments");
41
+ if (name !== null && name.length > ORIGINAL_NAME_MAX) refuse("attachments.name");
42
+ const file = closed(attachment.file, FILE_FIELDS, "attachments.file");
43
+ const path = text(file.path, "attachments.file.path", 4096);
44
+ if (!isAbsolute(path)) refuse("attachments.file.path");
45
+ let descriptor;
46
+ try {
47
+ descriptor = originalDescriptor(file);
48
+ } catch {
49
+ refuse("attachments.file");
50
+ }
51
+ return { file: { path, ...descriptor }, ...named };
52
+ });
53
+ }
54
+
55
+ function itemOf(line) {
56
+ const namespace = text(line.namespace, "namespace", 128);
57
+ if (!NAMESPACE.test(namespace)) refuse("namespace");
58
+ const observedAt = text(line.observed_at, "observed_at", 64);
59
+ if (!OBSERVED_AT.test(observedAt) || Number.isNaN(Date.parse(observedAt))) refuse("observed_at");
60
+ if (typeof line.body !== "string" || Buffer.byteLength(line.body, "utf8") > ITEM_BODY_MAX_BYTES) {
61
+ refuse("body");
62
+ }
63
+ return {
64
+ namespace,
65
+ nativeObjectId: text(line.native_object_id, "native_object_id", 512),
66
+ nativeVersion: optionalText(line.native_version, "native_version", 256),
67
+ observedAt,
68
+ body: line.body,
69
+ author: optionalText(line.author, "author", 512),
70
+ url: optionalText(line.url, "url", 2048),
71
+ attachments: attachmentsOf(line.attachments),
72
+ };
73
+ }
74
+
75
+ export function connectorRequest(request) {
76
+ return `${JSON.stringify({
77
+ contract: KIT_CONTRACT_VERSION,
78
+ instance: request.instance,
79
+ cursor: request.cursor,
80
+ limit: request.limit,
81
+ })}\n`;
82
+ }
83
+
84
+ export function parseConnectorRun(stdout, limit) {
85
+ const lines = stdout.split("\n");
86
+ if (lines.at(-1) === "") lines.pop();
87
+ const items = [];
88
+ let cursor = null;
89
+ for (const line of lines) {
90
+ if (cursor !== null) refuse("line after the terminal cursor");
91
+ let parsed;
92
+ try {
93
+ parsed = JSON.parse(line);
94
+ } catch {
95
+ refuse("line is not JSON");
96
+ }
97
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) refuse("line");
98
+ if (parsed.type === "cursor") {
99
+ cursor = text(closed(parsed, ["type", "cursor"], "cursor").cursor, "cursor", 2048);
100
+ continue;
101
+ }
102
+ if (parsed.type !== "item") refuse("type");
103
+ items.push(itemOf(closed(parsed, ITEM_FIELDS, "item")));
104
+ }
105
+ if (items.length > limit) throw new KitRefusal("kit_connector_limit_exceeded", `${items.length}`);
106
+ return { items, dropped: [], cursor, more: cursor !== null && items.length > 0 };
107
+ }
@@ -0,0 +1,61 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readdir, rm } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { KitRefusal } from "./refusal.js";
5
+ import { readNote, writeNote } from "./state.js";
6
+
7
+ function continuationDirectory(directory) {
8
+ return join(directory, "state", "continuations");
9
+ }
10
+
11
+ function continuationName(run) {
12
+ return `${createHash("sha256").update(run).digest("hex")}.json`;
13
+ }
14
+
15
+ function path(directory, run) {
16
+ return join(continuationDirectory(directory), continuationName(run));
17
+ }
18
+
19
+ export async function writeContinuation(directory, continuation) {
20
+ await writeNote(path(directory, continuation.run), { installation: directory, ...continuation });
21
+ }
22
+
23
+ export async function readContinuation(directory, run) {
24
+ const stored = await readNote(path(directory, run), null);
25
+ if (stored === null) return null;
26
+ if (stored.installation !== directory || stored.run !== run ||
27
+ typeof stored.conversation !== "boolean" ||
28
+ [stored.agentId, stored.workingDirectory, stored.routeId].some((value) => typeof value !== "string" || value.length === 0) ||
29
+ (stored.providerSessionId !== null && (typeof stored.providerSessionId !== "string" || stored.providerSessionId.length === 0 || stored.providerSessionId.length > 512))) {
30
+ throw new KitRefusal("kit_continuation_unreadable", run);
31
+ }
32
+ return stored;
33
+ }
34
+
35
+ export async function readContinuationRuns(directory) {
36
+ let present;
37
+ try {
38
+ present = await readdir(continuationDirectory(directory));
39
+ } catch (error) {
40
+ if (error?.code === "ENOENT") return [];
41
+ throw new KitRefusal("kit_state_unreadable", continuationDirectory(directory));
42
+ }
43
+ const runs = [];
44
+ for (const name of present.filter((entry) => entry.endsWith(".json")).sort()) {
45
+ let stored;
46
+ try {
47
+ stored = await readNote(join(continuationDirectory(directory), name), null);
48
+ } catch (refusal) {
49
+ if (!(refusal instanceof KitRefusal)) throw refusal;
50
+ continue;
51
+ }
52
+ if (stored?.installation !== directory || typeof stored.run !== "string") continue;
53
+ if (continuationName(stored.run) !== name) continue;
54
+ runs.push(stored.run);
55
+ }
56
+ return runs;
57
+ }
58
+
59
+ export async function removeContinuation(directory, run) {
60
+ await rm(path(directory, run), { force: true });
61
+ }
@@ -0,0 +1,363 @@
1
+ import { CLAUDE_PASTED_CODE, CODEX_DEVICE_CODE, GROK_DEVICE_CODE } from "./agent-logins.js";
2
+
3
+ export const KIT_CONTRACT_VERSION = "2026-09-26-house-agent-kinds";
4
+ export const KIT_CAPTURE_BATCH_MAX = 100;
5
+ export const COMMAND_INPUT_MAX_BYTES = 4 * 1024;
6
+ export const AUTHORIZATION_OUTPUT_MAX_BYTES = 4 * 1024;
7
+ export const CONNECTOR_OUTPUT_MAX_BYTES = 8 * 1024 * 1024;
8
+ export const CONNECTOR_TIMEOUT_MS = 120_000;
9
+ export const ITEM_BODY_MAX_BYTES = 1024 * 1024;
10
+ export const ITEM_ATTACHMENTS_MAX = 32;
11
+ export const DROPPED_ITEM_NOTES_MAX = 100;
12
+ export const LEASE_RENEWAL_MARGIN_MS = CONNECTOR_TIMEOUT_MS + 60_000;
13
+ export const KIT_CREDENTIAL = /^ahk_[A-Za-z0-9_-]+$/;
14
+ export const DECLARED_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
15
+
16
+ export const TMUX_SUPPORTED_MINIMUM = "3.2";
17
+ export const TMUX_COMMAND_TIMEOUT_MS = 10_000;
18
+ export const COMMAND_ADAPTER_INITIALIZE_TIMEOUT_MS = 10_000;
19
+ export const COMMAND_ADAPTER_SESSION_TIMEOUT_MS = 30_000;
20
+
21
+ export const WORKER_EXIT_GRACE_MS = 5_000;
22
+ export const WORKER_EXIT_POLL_MS = 100;
23
+ export const WORKER_SUPERVISION_POLL_MS = 2_000;
24
+ export const WORKER_COMMAND_POLL_MS = 2_000;
25
+ export const WORKER_HEARTBEAT_MS = 30_000;
26
+ export const WORKER_COMMAND_BATCH_MAX = 10;
27
+ export const WORKER_RUN_TOKEN_MARGIN_MS = 60_000;
28
+ export const TELEGRAM_IMAGE_UPLOAD_CEILING_BYTES = 10 * 1024 * 1024;
29
+ export const CONVERSATION_FILE_TRANSFER_TIMEOUT_MS = 30_000;
30
+ export const WORKER_INHERITED_ENVIRONMENT = ["PATH", "HOME"];
31
+ export const WORKER_ATTACH_ENVIRONMENT = ["PATH", "HOME", "TERM"];
32
+
33
+ export const EXECUTION_OUTCOMES = ["completed", "failed"];
34
+ export const EXECUTION_FAILURE_CAUSE_MAX = 200;
35
+ export const PROVIDER_CAPACITY_EXHAUSTED = "provider_capacity_exhausted";
36
+ export const PROVIDER_REFUSED = "provider_refused";
37
+ export const PROVIDER_CONTINUATION_UNAVAILABLE = "kit_provider_continuation_unavailable";
38
+
39
+ export function runOutcomePath(run) {
40
+ return `/kit/runs/${encodeURIComponent(run)}/outcome`;
41
+ }
42
+
43
+ export const KIT_SESSIONS_REPORT_PATH = "/kit/sessions/report";
44
+ export const KIT_SESSIONS_REPORT_MAX = 256;
45
+
46
+ export const ASSISTANT_TEXT_MAX_BYTES = 4096;
47
+ export const ASSISTANT_BLOCKS_MAX = 500;
48
+ export const ASSISTANT_TABLE_COLUMNS_MAX = 20;
49
+
50
+ export function assistantDraftsPath(workerSession) {
51
+ return `/kit/sessions/${encodeURIComponent(workerSession)}/drafts`;
52
+ }
53
+
54
+ export function assistantTextPath(workerSession) {
55
+ return `/kit/sessions/${encodeURIComponent(workerSession)}/assistant-messages`;
56
+ }
57
+
58
+ export const TELEGRAM_API_ORIGIN = "https://api.telegram.org";
59
+ export const TELEGRAM_INGRESS_BATCH_MAX = 100;
60
+ export const TELEGRAM_DELIVERY_BATCH_MAX = 10;
61
+ export const TELEGRAM_DOWNLOAD_CEILING_BYTES = 20 * 1024 * 1024;
62
+ export const TELEGRAM_BOT_API_TIMEOUT_MS = 30_000;
63
+ export const TELEGRAM_GATEWAY_POLL_MS = 2_000;
64
+ export const TELEGRAM_CONTROL_KEYBOARD_COMMANDS = ["/stop", "/kill"];
65
+ export const TELEGRAM_MEDIA_KINDS = [
66
+ "document",
67
+ "photo",
68
+ "audio",
69
+ "video",
70
+ "voice",
71
+ "video_note",
72
+ "sticker",
73
+ "animation",
74
+ ];
75
+ export const TELEGRAM_UNSUPPORTED_MEDIA_FIELDS = [
76
+ "contact",
77
+ "location",
78
+ "venue",
79
+ "poll",
80
+ "dice",
81
+ "game",
82
+ "invoice",
83
+ "successful_payment",
84
+ ];
85
+ export const TELEGRAM_UPDATE_OUTCOMES = ["accepted", "duplicate", "refused"];
86
+ export const TELEGRAM_DELIVERY_KINDS = [
87
+ "agent_choice",
88
+ "final_text",
89
+ "refusal",
90
+ "conversation_closed",
91
+ "control_reply",
92
+ "fresh_session_boundary",
93
+ "control_keyboard",
94
+ "interaction_card",
95
+ "plan",
96
+ "conversation_topic",
97
+ "conversation_file_link",
98
+ ];
99
+ export const PERSONAL_BOT_TOKEN_RELATIVE_PATH = "secrets/personal-telegram-bot.token";
100
+ export const PERSONAL_BOT_OFFSET_RELATIVE_PATH = "state/telegram/get-updates-offset.json";
101
+ export const PERSONAL_BOT_DISCONNECT_RELATIVE_PATH = "state/telegram/disconnecting.json";
102
+
103
+ export function telegramApiUrl(token, method) {
104
+ return `${TELEGRAM_API_ORIGIN}/bot${token}/${method}`;
105
+ }
106
+
107
+ export function telegramFileUrl(token, path) {
108
+ return `${TELEGRAM_API_ORIGIN}/file/bot${token}/${path}`;
109
+ }
110
+
111
+ export const BRIDGE_LINE_MAX_BYTES = 4 * 1024 * 1024;
112
+ export const BRIDGE_SOCKET_ENVIRONMENT = "HOUSE_KIT_BRIDGE";
113
+ export const BRIDGE_NONCE_ENVIRONMENT = "HOUSE_KIT_BRIDGE_NONCE";
114
+ export const BRIDGE_RECONNECT_MS = 200;
115
+ export const BRIDGE_RECONNECT_MAX_MS = 5_000;
116
+
117
+ export const ACP_PROTOCOL = 1;
118
+ export const ACP_AUTH_REQUIRED = -32000;
119
+ export const ADAPTER_EXIT_GRACE_MS = 2_000;
120
+ export const ELICITATION_FIELD_KINDS = Object.freeze([
121
+ "string",
122
+ "number",
123
+ "integer",
124
+ "boolean",
125
+ "array",
126
+ ]);
127
+ export const TURN_OUTCOMES = Object.freeze(["completed", "interrupted", "refused", "exhausted"]);
128
+
129
+ const PROTOCOL_STOP_REASONS = Object.freeze({
130
+ end_turn: "completed",
131
+ cancelled: "interrupted",
132
+ refusal: "refused",
133
+ max_tokens: "exhausted",
134
+ max_turn_requests: "exhausted",
135
+ });
136
+
137
+ export const ACP_CLIENT_INFO = Object.freeze({
138
+ name: "@agentshouse/kit",
139
+ version: KIT_CONTRACT_VERSION,
140
+ });
141
+
142
+ export const ACP_CLIENT_CAPABILITIES = Object.freeze({
143
+ fs: Object.freeze({ readTextFile: false, writeTextFile: false }),
144
+ terminal: false,
145
+ elicitation: Object.freeze({ form: Object.freeze({}) }),
146
+ });
147
+
148
+ export const ADAPTER_CONTRACTS = Object.freeze({
149
+ "codex-acp": Object.freeze({
150
+ kind: "codex-acp",
151
+ version: "2",
152
+ protocol: ACP_PROTOCOL,
153
+ release: Object.freeze({
154
+ package: "@agentclientprotocol/codex-acp",
155
+ minimum: "1.12.0",
156
+ }),
157
+ binary: "codex-acp",
158
+ arguments: Object.freeze([]),
159
+ settings: Object.freeze([
160
+ Object.freeze({
161
+ file: ".codex/config.toml",
162
+ setting: "tools.experimental_request_user_input.enabled",
163
+ value: "true",
164
+ }),
165
+ ]),
166
+ signIn: Object.freeze(["cli", "login", "--device-auth"]),
167
+ signedIn: Object.freeze(["cli", "login", "status"]),
168
+ guidedLogin: CODEX_DEVICE_CODE,
169
+ identity: Object.freeze({ agentInfo: "@agentclientprotocol/codex-acp" }),
170
+ nativeQuestion: null,
171
+ sessionLoad: true,
172
+ sessionConfig: Object.freeze({ model: "model", effort: "reasoning_effort" }),
173
+ capabilities: Object.freeze({
174
+ field_kinds: ELICITATION_FIELD_KINDS,
175
+ decline: true,
176
+ stop_reasons: Object.freeze({
177
+ ...PROTOCOL_STOP_REASONS,
178
+ [PROVIDER_CAPACITY_EXHAUSTED]: "exhausted",
179
+ }),
180
+ }),
181
+ }),
182
+ "claude-agent-acp": Object.freeze({
183
+ kind: "claude-agent-acp",
184
+ version: "1",
185
+ protocol: ACP_PROTOCOL,
186
+ release: Object.freeze({
187
+ package: "@agentclientprotocol/claude-agent-acp",
188
+ minimum: "0.79.0",
189
+ }),
190
+ binary: "claude-agent-acp",
191
+ arguments: Object.freeze([]),
192
+ settings: Object.freeze([]),
193
+ signIn: Object.freeze(["--cli", "auth", "login", "--claudeai"]),
194
+ signedIn: Object.freeze(["--cli", "auth", "status"]),
195
+ guidedLogin: CLAUDE_PASTED_CODE,
196
+ identity: Object.freeze({ agentInfo: "@agentclientprotocol/claude-agent-acp" }),
197
+ nativeQuestion: null,
198
+ sessionLoad: false,
199
+ sessionConfig: Object.freeze({ model: "model", effort: "effort" }),
200
+ capabilities: Object.freeze({
201
+ field_kinds: ELICITATION_FIELD_KINDS,
202
+ decline: true,
203
+ stop_reasons: PROTOCOL_STOP_REASONS,
204
+ }),
205
+ }),
206
+ "grok-build": Object.freeze({
207
+ kind: "grok-build",
208
+ version: "1",
209
+ protocol: ACP_PROTOCOL,
210
+ release: Object.freeze({
211
+ package: "@xai-official/grok",
212
+ minimum: "1.0.41",
213
+ }),
214
+ binary: "grok",
215
+ arguments: Object.freeze(["agent", "--no-leader", "stdio"]),
216
+ settings: Object.freeze([]),
217
+ signIn: Object.freeze(["login", "--device-auth"]),
218
+ signedIn: Object.freeze({ initializeMeta: "defaultAuthMethodId" }),
219
+ guidedLogin: GROK_DEVICE_CODE,
220
+ identity: Object.freeze({ initializeMeta: "grokShell", versionMeta: "agentVersion" }),
221
+ nativeQuestion: "x.ai/ask_user_question",
222
+ sessionLoad: true,
223
+ sessionConfig: Object.freeze({ model: "model", effort: "reasoning_effort" }),
224
+ capabilities: Object.freeze({
225
+ field_kinds: Object.freeze(["string", "array"]),
226
+ decline: false,
227
+ stop_reasons: Object.freeze({
228
+ end_turn: "completed",
229
+ cancelled: "interrupted",
230
+ }),
231
+ }),
232
+ }),
233
+ });
234
+
235
+ export const ADAPTER_KINDS = Object.freeze(Object.keys(ADAPTER_CONTRACTS));
236
+
237
+ export const KIT_AGENTS_REPORT_PATH = "/kit/agents/report";
238
+ export const KIT_AGENTS_DESIRED_PATH = "/kit/agents/desired";
239
+ export const AGENT_HOME = "agents";
240
+ export const AGENT_STATE_FILE = "state.json";
241
+ export const AGENT_RELEASES_HOME = "releases";
242
+ export const AGENT_UPDATE_INTERVAL_MS = 6 * 60 * 60 * 1000;
243
+ export const AGENT_INSTALL_TIMEOUT_MS = 15 * 60 * 1000;
244
+ export const AGENT_SIGNED_IN_TIMEOUT_MS = 30_000;
245
+
246
+ export const LAUNCH_DIRECTORY_BASE = "/agents/house";
247
+
248
+ export const KIT_SETUP_ACQUIRE_PATH = "/kit/setup/acquire";
249
+ export const KIT_SETUP_REPORT_PATH = "/kit/setup/report";
250
+ export const KIT_LOGIN_REPORT_PATH = "/kit/logins/report";
251
+ export const GUIDED_LOGIN_TIMEOUT_MS = 10 * 60 * 1000;
252
+ export const GUIDED_LOGIN_OUTPUT_MAX_BYTES = 64 * 1024;
253
+
254
+ export const EXECUTION_AUTHORITIES = ["allow", "refuse"];
255
+
256
+ export const PERMISSION_OPTION_KINDS = Object.freeze({
257
+ allow: "allow_once",
258
+ refuse: "reject_once",
259
+ });
260
+
261
+ export const KIT_HOST_CAPACITY_PATH = "/kit/host-capacity";
262
+ export const HOST_CPU_SAMPLE_MS = 1_000;
263
+
264
+ export const KIT_WORKSPACES_ACQUIRE_PATH = "/kit/workspaces/acquire";
265
+ export const WORKSPACE_ACQUIRE_MAX = 10;
266
+ export const WORKSPACE_POLL_MS = 2_000;
267
+ export const WORKSPACE_LABEL = "house-kit.workspace";
268
+ export const WORKSPACE_MOUNT = "/workspace";
269
+ export const WORKSPACE_KIT_HOME = "/workspace/.house-kit";
270
+ export const WORKSPACE_DISK_IMAGE = "disk.img";
271
+ export const WORKSPACE_DISK_MOUNT = "mnt";
272
+ export const WORKSPACE_MOUNT_OPTIONS = "loop,nosuid,nodev";
273
+ export const HOST_MOUNT_INFO = "/proc/self/mountinfo";
274
+ export const WORKSPACE_SYSTEM_RESERVE_BYTES = 512 * 1024 * 1024;
275
+ export const WORKSPACE_COMMAND_TIMEOUT_MS = 120_000;
276
+ export const WORKSPACE_READY_ATTEMPTS = 60;
277
+ export const WORKSPACE_READY_POLL_MS = 1_000;
278
+ export const WORKSPACE_STOP_TIMEOUT_SECONDS = 10;
279
+ export const WORKSPACE_OPERATION_WITHDRAWN = "hosted_environment_operation_not_found";
280
+ export const CGROUP_CONTROLLERS = "/sys/fs/cgroup/cgroup.controllers";
281
+ export const LOOP_CONTROL = "/dev/loop-control";
282
+
283
+ export function workspaceOutcomePath(operation) {
284
+ return `/kit/workspaces/${operation}/outcome`;
285
+ }
286
+
287
+ export const KIT_DOOR_DISCOVER_PATH = "/kit/door/discover";
288
+ export const KIT_DOOR_BOOTSTRAP_PATH = "/kit/door/bootstrap";
289
+ export const KIT_DOOR_ENUMERATE_PATH = "/kit/door/enumerate";
290
+ export const KIT_DOOR_BATCH_PATH = "/kit/door/batch";
291
+ export const EXPLICIT_SYNC_VERSION = "explicit-sync/1";
292
+ export const KIT_DOOR_UNMOUNT_PATH = "/kit/door/unmount";
293
+
294
+ export const MIGRATOR_CONTRACT = "kit-migrator/1";
295
+ export const MIGRATOR_CATEGORY = "migrator";
296
+ export const MIGRATOR_MANIFEST = "migrator.json";
297
+ export const MIGRATOR_CONTENT_HOME = "content";
298
+ export const MIGRATOR_ATTACHMENTS_HOME = "attachments";
299
+ export const MIGRATOR_MODEL_HOME = "model";
300
+ export const MIGRATOR_DOWNLOAD_OUTCOMES = ["downloaded", "unavailable", "skipped"];
301
+ export const MIGRATOR_TIMEOUT_MS = 60 * 60 * 1000;
302
+ export const MIGRATOR_OUTPUT_MAX_BYTES = 64 * 1024;
303
+ export const MIGRATOR_ATTACHMENTS_MAX = 256;
304
+ export const MOUNT_REPORT_NAME = "MIGRATION.md";
305
+
306
+ export const KIT_ATTACHMENTS_UPLOAD_PATH = "/kit/attachments/upload";
307
+ export const KIT_ATTACHMENTS_GET_PATH = "/kit/attachments/get";
308
+ export const ATTACHMENT_REFERENCE = /^at_[1-9a-z][0-9a-z]*\.[0-9a-z]$/;
309
+ export const ATTACHMENT_TRANSFER_CONTRACT = "attachment-transfer/1";
310
+ export const ATTACHMENT_FILE_META = "agents.house/file";
311
+ export const KIT_CAPTURE_ORIGINALS_SAVE_PATH = "/kit/capture/originals/save";
312
+ export const KIT_CAPTURE_ORIGINALS_GET_PATH = "/kit/capture/originals/get";
313
+ export const KIT_CONVERSATION_ORIGINALS_SAVE_PATH = "/kit/conversation/originals/save";
314
+ export const KIT_CONVERSATION_ORIGINALS_GET_PATH = "/kit/conversation/originals/get";
315
+ export const BYTE_GRANT_OPERATION_HEADER = "x-house-byte-operation";
316
+ export const BYTE_GRANT_POST_BYTES_MAX = 32 * 1024 * 1024;
317
+ export const ORIGINAL_NAME_MAX = 255;
318
+ export const ORIGINAL_MEDIA_TYPE_MAX = 255;
319
+ export const SHA256_DIGEST = /^[a-f0-9]{64}$/;
320
+ export const ORIGINAL_UUID =
321
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
322
+
323
+ export const MCP_PROTOCOL_VERSION = "2026-07-28";
324
+ export const MCP_PROTOCOL_VERSION_META = "io.modelcontextprotocol/protocolVersion";
325
+ export const MCP_CLIENT_INFO_META = "io.modelcontextprotocol/clientInfo";
326
+ export const MCP_CLIENT_CAPABILITIES_META = "io.modelcontextprotocol/clientCapabilities";
327
+ export const AGENT_OPERATION_META = "agents.house/agent-operation";
328
+
329
+ export const ELICITATION_CREATE = "elicitation/create";
330
+ export const REQUEST_PERMISSION = "session/request_permission";
331
+ export const SESSION_UPDATE = "session/update";
332
+ export const SESSION_SET_CONFIG_OPTION = "session/set_config_option";
333
+ export const ELICITATION_SECRET_META = "agents.house/secret";
334
+ export const ELICITATION_FREE_TEXT_META = "agents.house/free-text";
335
+ export const NATIVE_CUSTOM_ANSWER_META = "_askUserQuestionCustomAnswer";
336
+ export const NATIVE_CODEX_META = "codex";
337
+ export const NATIVE_USER_NOTE_ROLE = "user_note";
338
+ export const NATIVE_OTHER_OPTION = "None of the above";
339
+ export const INTERACTION_OUTCOMES = ["resolved", "expired"];
340
+ export const INTERACTION_CORRELATION_MAX = 64;
341
+ export const INTERACTION_PROPERTY_MAX = 64;
342
+ export const INTERACTION_VALUE_MAX = 4096;
343
+ export const INTERACTION_OPTIONS_MAX = 32;
344
+ export const INTERACTION_VALUES_MAX = INTERACTION_OPTIONS_MAX + 1;
345
+
346
+ export function sessionInteractionsPath(workerSession) {
347
+ return `/kit/sessions/${encodeURIComponent(workerSession)}/interactions`;
348
+ }
349
+
350
+ export function interactionOutcomePath(interaction) {
351
+ return `/kit/interactions/${encodeURIComponent(interaction)}/outcome`;
352
+ }
353
+
354
+ export const ELICITATION_FORM_MODE = "form";
355
+ export const ELICITATION_URL_MODE = "url";
356
+ export const SECRET_CEREMONY_STEPS_MAX = 8;
357
+ export const SECRET_STEP_LABEL_MAX = 200;
358
+ export const SECRET_STEP_TEXT_MAX = 4096;
359
+ export const SECRET_VALUE_MAX = 4096;
360
+
361
+ export function secretInputPath(subject) {
362
+ return `/kit/secret-input/${encodeURIComponent(subject)}`;
363
+ }
@@ -0,0 +1,143 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readdir, rm } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { BYTE_GRANT_POST_BYTES_MAX } from "./contract.js";
5
+ import { describedBytes, readOriginalFile, saveConversationOriginal } from "./original.js";
6
+ import { KitRefusal } from "./refusal.js";
7
+ import { readNote, writeNote } from "./state.js";
8
+
9
+ const SETTLED_ASSOCIATION = new Set(["conversation_original_not_found"]);
10
+
11
+ function stateDirectory(directory) {
12
+ return join(directory, "state", "conversation-originals");
13
+ }
14
+
15
+ function statePath(directory, held) {
16
+ const identity = `attachment:${held.attachment}`;
17
+ return join(stateDirectory(directory), `${createHash("sha256").update(identity).digest("hex")}.json`);
18
+ }
19
+
20
+ function report(held) {
21
+ return {
22
+ attachment: held.attachment,
23
+ run: held.run,
24
+ original: held.original,
25
+ version: held.version,
26
+ save: held.save,
27
+ error: held.error,
28
+ };
29
+ }
30
+
31
+ function sameDescriptor(held, observed) {
32
+ return (
33
+ held !== null &&
34
+ observed !== null &&
35
+ held.bytes === observed.bytes &&
36
+ held.sha256 === observed.sha256
37
+ );
38
+ }
39
+
40
+ export async function holdConversationFile(context, file, bytes) {
41
+ const oversized = bytes !== null && bytes.length > BYTE_GRANT_POST_BYTES_MAX;
42
+ const identity = { attachment: file.attachment };
43
+ const path = statePath(context.directory, identity);
44
+ const noted = await readNote(path, null);
45
+ const observed = bytes === null || oversized ? null : describedBytes(bytes);
46
+ // House fixes a version's descriptor the first time one is declared and refuses
47
+ // every later one, so a file held again keeps the descriptor its note carries.
48
+ // Bytes that no longer answer that descriptor are not this original's bytes.
49
+ const descriptor = noted?.descriptor ?? observed;
50
+ const beyondBound = descriptor === null && (oversized || noted?.exceeded === true);
51
+ const held = {
52
+ ...identity,
53
+ run: file.run,
54
+ path: file.path ?? noted?.path ?? null,
55
+ descriptor,
56
+ exceeded: beyondBound,
57
+ original: null,
58
+ version: null,
59
+ save: { status: "pending", failure: null },
60
+ error: beyondBound ? "original_bytes_exceeded" : null,
61
+ };
62
+ await writeNote(path, held, context.signal);
63
+ return { held, body: sameDescriptor(descriptor, observed) ? bytes : null };
64
+ }
65
+
66
+ export async function settleConversationFile(context, held, body) {
67
+ let settled;
68
+ let answered = false;
69
+ try {
70
+ const saved = await saveConversationOriginal(context.session, {
71
+ attachment: held.attachment,
72
+ descriptor: held.descriptor,
73
+ body: held.exceeded ? null : body,
74
+ signal: context.signal,
75
+ });
76
+ answered = true;
77
+ settled = {
78
+ ...held,
79
+ original: saved.original,
80
+ version: saved.version,
81
+ save: saved.save,
82
+ error: held.exceeded ? "original_bytes_exceeded" : null,
83
+ };
84
+ } catch (refusal) {
85
+ if (!(refusal instanceof KitRefusal) || refusal.code === "kit_command_cancelled") throw refusal;
86
+ settled = { ...held, error: refusal.code };
87
+ }
88
+ const path = statePath(context.directory, settled);
89
+ if (settled.save.status === "saved" || (held.exceeded && answered) || SETTLED_ASSOCIATION.has(settled.error)) {
90
+ await rm(path, { force: true });
91
+ } else {
92
+ await writeNote(path, settled, context.signal);
93
+ }
94
+ return report(settled);
95
+ }
96
+
97
+ export async function retainConversationFile(context, file, bytes) {
98
+ const { held, body } = await holdConversationFile(context, file, bytes);
99
+ return settleConversationFile(context, held, body);
100
+ }
101
+
102
+ async function heldFiles(directory) {
103
+ let names;
104
+ try {
105
+ names = await readdir(stateDirectory(directory));
106
+ } catch (error) {
107
+ if (error?.code === "ENOENT") return [];
108
+ throw new KitRefusal("kit_state_unreadable", stateDirectory(directory));
109
+ }
110
+ const held = [];
111
+ for (const name of names.filter((entry) => /^[a-f0-9]{64}\.json$/.test(entry)).sort()) {
112
+ held.push(await readNote(join(stateDirectory(directory), name), null));
113
+ }
114
+ return held.filter((note) => note !== null);
115
+ }
116
+
117
+ async function localBytes(held) {
118
+ if (held.path === null) return { bytes: null, exceeded: false };
119
+ try {
120
+ return { bytes: await readOriginalFile(held.path, held.descriptor?.bytes ?? null), exceeded: false };
121
+ } catch (refusal) {
122
+ if (!(refusal instanceof KitRefusal) || refusal.code !== "original_bytes_exceeded") throw refusal;
123
+ return { bytes: null, exceeded: true };
124
+ }
125
+ }
126
+
127
+ export async function retryConversationFiles(context, settled = new Set()) {
128
+ const reported = [];
129
+ for (const held of await heldFiles(context.directory)) {
130
+ if (settled.has(held.attachment)) continue;
131
+ const local = held.exceeded ? { bytes: null, exceeded: true } : await localBytes(held);
132
+ const descriptor = held.descriptor ?? (local.bytes === null ? null : describedBytes(local.bytes));
133
+ const exceeded = held.exceeded || (held.descriptor === null && local.exceeded);
134
+ reported.push(
135
+ await settleConversationFile(
136
+ context,
137
+ { ...held, descriptor, exceeded, error: exceeded ? "original_bytes_exceeded" : held.error },
138
+ local.bytes,
139
+ ),
140
+ );
141
+ }
142
+ return reported;
143
+ }