@agentshouse/kit 0.1.0-alpha.21 → 0.1.0-alpha.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,7 +10,7 @@ On Linux `amd64` or `arm64` with a working Docker Engine, run the published,
10
10
  versioned bootstrap once:
11
11
 
12
12
  ```sh
13
- bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.21/connect-linux.sh)
13
+ bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.23/connect-linux.sh)
14
14
  ```
15
15
 
16
16
  The release asset pins one immutable multi-architecture image digest. It creates
@@ -142,11 +142,18 @@ which the User wrote once this adoption's proof had passed. The adapter
142
142
  packages themselves are Environment prerequisites, not Kit dependencies, and
143
143
  carry no record.
144
144
 
145
- The bootstrap invokes bare `kit login`. The first login names its new
145
+ The bootstrap invokes bare `kit login`, which connects to `https://agents.house`;
146
+ `--house <origin>` names another House. Login records the House in Kit home
147
+ beside the credential. Every House-bound command talks to that recorded House,
148
+ and every command but login refuses `--house` as an unknown argument. The first login names its new
146
149
  Environment after the host computer and stores its Environment id in Kit home;
147
- an explicit later login reconnects that retained id. `--environment <uuid>`
148
- selects a different retained Environment for explicit recovery. A malformed
149
- id or an answer naming a different Environment is refused before installation.
150
+ the panel renames it. An explicit later login reconnects that retained id, and
151
+ `--environment <uuid>` reconnects the Environment it names instead, whether or
152
+ not Kit home records one, and Kit home then records that id. Reconnecting issues the next credential for the same Environment
153
+ and ends the previous one, including one `envs/revoke` ended; House refuses an
154
+ Environment it no longer holds with its cause and never creates a replacement.
155
+ A malformed id or an answer naming a different Environment is refused before
156
+ installation.
150
157
 
151
158
  Use `--manual` when the browser cannot reach the Kit's loopback listener. The
152
159
  command stores the issued Kit User credential in `$HOUSE_KIT_HOME/credential`
@@ -253,7 +260,7 @@ and the batch it produces, and the lease has to outlive both.
253
260
  Apply the declared source state to House:
254
261
 
255
262
  ```sh
256
- kit apply --house https://agents.house
263
+ kit apply
257
264
  ```
258
265
 
259
266
  Only the state House declares - instances, services, `source_ref`, and Room
@@ -442,7 +449,7 @@ its owner asks it to renew what the Environment holds. The Kit acts on the
442
449
  request it acquires, never on its own:
443
450
 
444
451
  ```sh
445
- kit setup --house https://agents.house
452
+ kit setup
446
453
  ```
447
454
 
448
455
  `kit worker run` performs the same step at the start of a pass, before it
@@ -558,7 +565,7 @@ command returns exactly one JSON object such as `{ "authorized": true }`.
558
565
  ## The Worker runtime
559
566
 
560
567
  ```sh
561
- kit worker run --house https://agents.house
568
+ kit worker run
562
569
  ```
563
570
 
564
571
  Worker-runtime readiness validates the tmux this Kit declares support for,
@@ -1112,8 +1119,8 @@ into the evidence each spec writes under `tmp/delivery-evidence/`.
1112
1119
  ## Capturing
1113
1120
 
1114
1121
  ```sh
1115
- kit source capture acme --house https://agents.house # every declared service
1116
- kit source capture acme notes --house https://agents.house # one service
1122
+ kit source capture acme # every declared service
1123
+ kit source capture acme notes # one service
1117
1124
  ```
1118
1125
 
1119
1126
  Each run reads the checkpoint, acquires one bounded page, submits one batch,
@@ -1146,8 +1153,8 @@ Room and Record and exits non-zero while any stays unsaved; acquisition health
1146
1153
  stays about acquisition, so a failed save never changes it.
1147
1154
 
1148
1155
  ```sh
1149
- kit source retry-files acme --house https://agents.house # every declared service
1150
- kit source retry-files acme notes --house https://agents.house # one service
1156
+ kit source retry-files acme # every declared service
1157
+ kit source retry-files acme notes # one service
1151
1158
  ```
1152
1159
 
1153
1160
  `retry-files` saves only the references still outstanding. It runs no connector,
@@ -1203,8 +1210,7 @@ Git credential:
1203
1210
  ```sh
1204
1211
  kit repository capture /srv/acme \
1205
1212
  --room work-room \
1206
- --source-handle acme-repository \
1207
- --house https://agents.house
1213
+ --source-handle acme-repository
1208
1214
  ```
1209
1215
 
1210
1216
  The command runs ordinary `git pull` in the checkout with the local User's Git
@@ -1306,6 +1312,19 @@ control lease when a resident runs, and which otherwise acquires and releases
1306
1312
  its own. A Session refuses `--owner`, and a missing bridge is never replaced by
1307
1313
  owner authority.
1308
1314
 
1315
+ A Session's edit too large for one MCP request of 4 MiB goes through House's
1316
+ `prepare_edit_upload` Command on that Session's bridge, naming every changed
1317
+ path, the byte count and the SHA-256 of the bytes to upload under the push's
1318
+ operation. Those bytes are the serialized changes, padded with trailing spaces
1319
+ to one byte over 4 MiB when only the request around them passed the bound,
1320
+ because House takes no smaller upload. Kit sends them to the upload address
1321
+ House returns and then calls `edit` with the paths and the returned upload
1322
+ instead of the changes, so House applies them as the same one edit. The upload
1323
+ is recorded in Kit home before the edit is sent, so a push whose answer was lost
1324
+ resends that same edit; when House no longer holds the uploaded bytes and has
1325
+ not applied the edit, the push uploads them again once under the same operation.
1326
+ A change set beyond House's batch bounds is refused whole and never split.
1327
+
1309
1328
  Inside an Agent Session `git` is Kit's shim in front of the installed Git, and
1310
1329
  every command runs exactly as that Git runs it. In a copy or one of its linked
1311
1330
  worktrees the shim compares that worktree's index before and after each
@@ -1387,13 +1406,12 @@ beside `library/**`.
1387
1406
 
1388
1407
  ```sh
1389
1408
  kit sync bootstrap ./replica \
1390
- --house https://agents.house \
1391
1409
  --prefix /rooms/work/library \
1392
1410
  --prefix /rooms/work/capture \
1393
1411
  --prefix /private/library
1394
- kit sync pull ./replica --house https://agents.house
1395
- kit sync submit ./replica --house https://agents.house --changes ./changes.json
1396
- kit sync rebootstrap ./replica --house https://agents.house
1412
+ kit sync pull ./replica
1413
+ kit sync submit ./replica --changes ./changes.json
1414
+ kit sync rebootstrap ./replica
1397
1415
  ```
1398
1416
 
1399
1417
  `bootstrap` writes `ROOM.md`, Library text, House-derived projections, and
@@ -1456,16 +1474,13 @@ absolute paths.
1456
1474
 
1457
1475
  ```sh
1458
1476
  kit attachment upload ./notes.bin \
1459
- --house https://agents.house \
1460
1477
  --room room:work \
1461
1478
  --media-type application/pdf
1462
1479
 
1463
1480
  kit attachment upload ./notes.bin \
1464
- --house https://agents.house \
1465
1481
  --private
1466
1482
 
1467
1483
  kit attachment get \
1468
- --house https://agents.house \
1469
1484
  --attachment at_1x.q \
1470
1485
  --room room:work \
1471
1486
  --output ./notes.bin
@@ -1511,7 +1526,7 @@ Any missing prerequisite refuses the create before House ever presents the
1511
1526
  workspace as constrained, naming the exact missing piece.
1512
1527
 
1513
1528
  ```sh
1514
- kit workspace host --house https://agents.house
1529
+ kit workspace host
1515
1530
  ```
1516
1531
 
1517
1532
  The command acquires this host's workspace work under its Environment control
@@ -1566,7 +1581,7 @@ image from the original the Session stored.
1566
1581
  Bind the bot to this Environment:
1567
1582
 
1568
1583
  ```sh
1569
- kit telegram connect --house https://agents.house
1584
+ kit telegram connect
1570
1585
  ```
1571
1586
 
1572
1587
  The command asks for the token on a hidden terminal prompt, or reads one line
@@ -1585,7 +1600,7 @@ proven owner's Telegram user id.
1585
1600
  Run the gateway:
1586
1601
 
1587
1602
  ```sh
1588
- kit telegram serve --house https://agents.house
1603
+ kit telegram serve
1589
1604
  ```
1590
1605
 
1591
1606
  The command holds one Environment control lease for the whole run and performs
@@ -1631,7 +1646,7 @@ command is resident and stops on SIGINT or SIGTERM, printing what it settled.
1631
1646
  Release the bot:
1632
1647
 
1633
1648
  ```sh
1634
- kit telegram disconnect --house https://agents.house
1649
+ kit telegram disconnect
1635
1650
  ```
1636
1651
 
1637
1652
  The command writes a local disconnect marker first, deletes the webhook, removes
@@ -1677,8 +1692,8 @@ Disabling a service or an instance leaves its state in place, so re-enabling
1677
1692
  resumes from the stored cursor instead of importing again.
1678
1693
 
1679
1694
  ```sh
1680
- kit source reset acme --house https://agents.house # every service
1681
- kit source reset acme notes --house https://agents.house # one service
1695
+ kit source reset acme # every service
1696
+ kit source reset acme notes # one service
1682
1697
  ```
1683
1698
 
1684
1699
  Reset is the only deletion of captured state. It removes the named checkpoints
@@ -1715,7 +1730,7 @@ refused rather than read as no limit. A ceiling is a shared ceiling, never a
1715
1730
  reservation, and House infers no supported Agent count from any of these values.
1716
1731
 
1717
1732
  ```sh
1718
- kit host report --house https://agents.house
1733
+ kit host report
1719
1734
  ```
1720
1735
 
1721
1736
  The command reads `/proc/stat` twice a second apart, reads `/proc/meminfo`, and
@@ -1737,7 +1752,7 @@ Description=Report measured host capacity to House
1737
1752
  [Service]
1738
1753
  Type=oneshot
1739
1754
  User=house-kit
1740
- ExecStart=/usr/bin/kit host report --house https://agents.house
1755
+ ExecStart=/usr/bin/kit host report
1741
1756
  ```
1742
1757
 
1743
1758
  ```
@@ -1786,11 +1801,10 @@ manifest, URLs, or attachments.
1786
1801
 
1787
1802
  ```sh
1788
1803
  kit migrate emit my-import --package /home/ada/exports/project
1789
- kit migrate mount /home/ada/exports/project --house https://agents.house \
1790
- --subtree /rooms/work/library/project
1804
+ kit migrate mount /home/ada/exports/project --subtree /rooms/work/library/project
1791
1805
  # Or emit and mount in one maintained mount session:
1792
1806
  kit migrate run my-import --package /home/ada/exports/another-project \
1793
- --house https://agents.house --subtree /private/library/another-project
1807
+ --subtree /private/library/another-project
1794
1808
  ```
1795
1809
 
1796
1810
  Emission refuses an existing output directory. The module reads one JSON line
@@ -1877,8 +1891,8 @@ leaves the pending mounted report plus the local outcomes. Re-migration is
1877
1891
  explicit unmount followed by mount.
1878
1892
 
1879
1893
  ```sh
1880
- kit migrate unmount /rooms/work/library/project --house https://agents.house
1881
- kit migrate unmount /rooms/work/library/project --house https://agents.house \
1894
+ kit migrate unmount /rooms/work/library/project
1895
+ kit migrate unmount /rooms/work/library/project \
1882
1896
  --confirm /rooms/work/library/project
1883
1897
  ```
1884
1898
 
@@ -39,7 +39,7 @@ login_kit() {
39
39
  }
40
40
 
41
41
  start_resident() {
42
- docker run -d --name "$NAME" --restart unless-stopped --user "$(id -u):$(id -g)" --mount "$KIT_HOME_MOUNT" --mount "$WORKSPACE_MOUNT" --label "agentshouse.house=$HOUSE" --env HOME=/kit-home --env HOUSE_KIT_HOME=/kit-home --env HOUSE_KIT_CONTAINER=1 "$IMAGE" resident --house "$HOUSE" >/dev/null
42
+ docker run -d --name "$NAME" --restart unless-stopped --user "$(id -u):$(id -g)" --mount "$KIT_HOME_MOUNT" --mount "$WORKSPACE_MOUNT" --label "agentshouse.house=$HOUSE" --env HOME=/kit-home --env HOUSE_KIT_HOME=/kit-home --env HOUSE_KIT_CONTAINER=1 "$IMAGE" resident >/dev/null
43
43
  }
44
44
 
45
45
  if [[ "${1:-}" == kit || "${1:-}" == house ]]; then
@@ -168,7 +168,7 @@ if docker container inspect "$NAME" >/dev/null 2>&1; then
168
168
  printf 'House Kit updated for Environment %s.\n' "$(cat "$KIT_HOME/environment")"
169
169
  exit 0
170
170
  fi
171
- "${DOCKER_RUN[@]}" credential check --house "$HOUSE"
171
+ "${DOCKER_RUN[@]}" credential check
172
172
  if [[ "$(docker inspect --format '{{.State.Running}}' "$NAME")" == true ]]; then
173
173
  printf 'House Kit is already running for Environment %s.\n' "$(cat "$KIT_HOME/environment")"
174
174
  else
@@ -185,15 +185,14 @@ if [[ ! -f "$KIT_HOME/kit.json" ]]; then
185
185
  fi
186
186
 
187
187
  if [[ -f "$KIT_HOME/credential" ]]; then
188
- "${DOCKER_RUN[@]}" credential check --house "$HOUSE"
188
+ "${DOCKER_RUN[@]}" credential check
189
189
  else
190
- if ((MANUAL)); then
191
- login_kit --house "$HOUSE" --manual
192
- else
193
- login_kit --house "$HOUSE"
194
- fi
190
+ LOGIN=()
191
+ if ((HOUSE_SELECTED)); then LOGIN+=(--house "$HOUSE"); fi
192
+ if ((MANUAL)); then LOGIN+=(--manual); fi
193
+ login_kit "${LOGIN[@]}"
195
194
  fi
196
195
 
197
- "${DOCKER_RUN[@]}" apply --house "$HOUSE"
196
+ "${DOCKER_RUN[@]}" apply
198
197
  start_resident
199
198
  printf 'House Kit connected for Environment %s.\n' "$(cat "$KIT_HOME/environment")"
package/bin/kit.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import { join, resolve } from "node:path";
5
- import { connectHouse, storedEnvironment, storedHouse } from "../src/login.js";
5
+ import { connectHouse, loginHouse, storedEnvironment, storedHouse } from "../src/login.js";
6
6
  import { apply } from "../src/apply.js";
7
7
  import { captureSource, resetSource, retrySourceOriginals, sourceStatus } from "../src/source.js";
8
8
  import { setupEnvironment } from "../src/setup.js";
@@ -32,44 +32,44 @@ const USAGE =
32
32
  "usage: kit <login|resident|credential|setup|apply|agents|source|repository|worker|sync|migrate|attachment|host|workspace|telegram> ...";
33
33
  const LOGIN_USAGE =
34
34
  "usage: kit login [--house <url>] [--environment <uuid>] [--manual|--host-browser]";
35
- const HOST_USAGE = "usage: kit host report [--house <url>] [--config <path>]";
35
+ const HOST_USAGE = "usage: kit host report [--config <path>]";
36
36
  const AGENTS_USAGE =
37
- "usage: kit agents update [--house <url>] [--config <path>] | kit agents sign-in <kind> [--house <url>] [--config <path>]";
38
- const WORKSPACE_USAGE = "usage: kit workspace host [--house <url>] [--config <path>] [--passes <count>]";
37
+ "usage: kit agents update [--config <path>] | kit agents sign-in <kind> [--config <path>]";
38
+ const WORKSPACE_USAGE = "usage: kit workspace host [--config <path>] [--passes <count>]";
39
39
  const SOURCE_USAGE =
40
- "usage: kit source <capture|reset|retry-files> <instance> [service] [--house <url>] [--config <path>]\n" +
40
+ "usage: kit source <capture|reset|retry-files> <instance> [service] [--config <path>]\n" +
41
41
  " kit source status <instance> [service] [--config <path>]\n" +
42
42
  " kit source sign-in <instance> [--config <path>]";
43
- const SETUP_USAGE = "usage: kit setup [--house <url>] [--config <path>]";
43
+ const SETUP_USAGE = "usage: kit setup [--config <path>]";
44
44
  const REPOSITORY_USAGE =
45
- "usage: kit repository capture <checkout> --room <room> --source-handle <handle> [--house <url>]";
45
+ "usage: kit repository capture <checkout> --room <room> --source-handle <handle>";
46
46
  const WORKER_USAGE =
47
- "usage: kit worker run [--house <url>] [--config <path>] | kit worker attach [<session>]";
47
+ "usage: kit worker run [--config <path>] | kit worker attach [<session>]";
48
48
  const SYNC_USAGE =
49
49
  "usage: kit sync <bootstrap|pull|rebootstrap|submit> ...";
50
50
  const SYNC_BOOTSTRAP_USAGE =
51
- "usage: kit sync bootstrap <directory> [--house <url>] --prefix <path> [--prefix <path> ...]";
52
- const SYNC_PULL_USAGE = "usage: kit sync pull <directory> [--house <url>]";
53
- const SYNC_REBOOTSTRAP_USAGE = "usage: kit sync rebootstrap <directory> [--house <url>]";
51
+ "usage: kit sync bootstrap <directory> --prefix <path> [--prefix <path> ...]";
52
+ const SYNC_PULL_USAGE = "usage: kit sync pull <directory>";
53
+ const SYNC_REBOOTSTRAP_USAGE = "usage: kit sync rebootstrap <directory>";
54
54
  const SYNC_SUBMIT_USAGE =
55
- "usage: kit sync submit <directory> [--house <url>] [--changes <path>]";
55
+ "usage: kit sync submit <directory> [--changes <path>]";
56
56
  const MIGRATE_USAGE = "usage: kit migrate <emit|mount|unmount|run> ...";
57
57
  const MIGRATE_EMIT_USAGE =
58
58
  "usage: kit migrate emit <migrator> --package <directory> [--config <path>]";
59
59
  const MIGRATE_MOUNT_USAGE =
60
- "usage: kit migrate mount <package> [--house <url>] --subtree <path>";
60
+ "usage: kit migrate mount <package> --subtree <path>";
61
61
  const MIGRATE_UNMOUNT_USAGE =
62
- "usage: kit migrate unmount <subtree> [--house <url>] [--confirm <subtree>]";
62
+ "usage: kit migrate unmount <subtree> [--confirm <subtree>]";
63
63
  const MIGRATE_RUN_USAGE =
64
- "usage: kit migrate run <migrator> [--house <url>] --subtree <path> --package <directory> [--config <path>]";
64
+ "usage: kit migrate run <migrator> --subtree <path> --package <directory> [--config <path>]";
65
65
  const ATTACHMENT_USAGE = "usage: kit attachment <upload|get> ...";
66
66
  const ATTACHMENT_UPLOAD_USAGE =
67
- "usage: kit attachment upload <file> [--house <url>] (--room <ref> | --private) [--name <name>] [--media-type <type>]";
67
+ "usage: kit attachment upload <file> (--room <ref> | --private) [--name <name>] [--media-type <type>]";
68
68
  const ATTACHMENT_GET_USAGE =
69
- "usage: kit attachment get [--house <url>] --attachment <at_ref> (--room <ref> | --private) [--output <path>]";
69
+ "usage: kit attachment get --attachment <at_ref> (--room <ref> | --private) [--output <path>]";
70
70
  const TELEGRAM_USAGE =
71
- "usage: kit telegram <connect|disconnect> [--house <url>]\n" +
72
- " kit telegram serve [--house <url>] [--passes <count>]";
71
+ "usage: kit telegram <connect|disconnect>\n" +
72
+ " kit telegram serve [--passes <count>]";
73
73
 
74
74
  function declared(argv, accepted, positionals, options = {}) {
75
75
  const repeated = options.repeated ?? [];
@@ -115,14 +115,10 @@ function builtGoogleOAuthClient() {
115
115
  return GOOGLE_OAUTH_CLIENT;
116
116
  }
117
117
 
118
- async function selectedHouse(values, directory) {
119
- return storedHouse(directory, false, values["--house"]);
120
- }
121
-
122
118
  async function runLogin(args, directory) {
123
119
  const { values } = declared(args, ["--house", "--manual", "--host-browser", "--environment"], 0, { flags: ["--host-browser"] });
124
120
  if (values["--manual"] && values["--host-browser"]) throw new Error(LOGIN_USAGE);
125
- const house = await storedHouse(directory, true, values["--house"]);
121
+ const house = await loginHouse(directory, values["--house"]);
126
122
  const presentation = values["--manual"] === true ? "manual" : "loopback";
127
123
  await connectHouse({
128
124
  house,
@@ -135,9 +131,9 @@ async function runLogin(args, directory) {
135
131
  }
136
132
 
137
133
  async function runCredentialCheck(args, directory) {
138
- const { values } = declared(args, ["--house"], 0, { exact: true });
134
+ declared(args, [], 0, { exact: true });
139
135
  if (await storedEnvironment(directory) === null) throw new KitRefusal("kit_environment_invalid", "no enrolled Environment id");
140
- const house = await selectedHouse(values, directory);
136
+ const house = await storedHouse(directory);
141
137
  let session;
142
138
  try {
143
139
  session = await openSession({ house, directory, acquire: true });
@@ -153,8 +149,8 @@ async function runCredentialCheck(args, directory) {
153
149
  }
154
150
 
155
151
  async function runResidentCommand(args, directory) {
156
- const { values } = declared(args, ["--house", "--config"], 0, { exact: true });
157
- const house = await selectedHouse(values, directory);
152
+ const { values } = declared(args, ["--config"], 0, { exact: true });
153
+ const house = await storedHouse(directory);
158
154
  const cancellation = new AbortController();
159
155
  const stop = () => cancellation.abort();
160
156
  process.once("SIGINT", stop);
@@ -185,9 +181,8 @@ async function runResidentDrain(args, directory) {
185
181
  }
186
182
 
187
183
  async function runSetup(args, directory) {
188
- const { values } = declared(args, ["--house", "--config"], 0, { exact: true });
189
- const house = await selectedHouse(values, directory);
190
- if (!house) throw new Error(SETUP_USAGE);
184
+ const { values } = declared(args, ["--config"], 0, { exact: true });
185
+ const house = await storedHouse(directory);
191
186
  const cancellation = new AbortController();
192
187
  const cancel = () => cancellation.abort();
193
188
  process.once("SIGINT", cancel);
@@ -240,9 +235,8 @@ async function runSourceSignIn(args, directory) {
240
235
  }
241
236
 
242
237
  async function runApply(args, directory) {
243
- const { values } = declared(args, ["--house", "--config"], 0);
244
- const house = await selectedHouse(values, directory);
245
- if (!house) throw new Error("usage: kit apply --house <url> [--config <path>]");
238
+ const { values } = declared(args, ["--config"], 0);
239
+ const house = await storedHouse(directory);
246
240
  const cancellation = new AbortController();
247
241
  const cancel = () => cancellation.abort();
248
242
  process.once("SIGINT", cancel);
@@ -269,9 +263,9 @@ async function runApply(args, directory) {
269
263
  }
270
264
 
271
265
  async function runSourceCapture(args, directory) {
272
- const { values, rest } = declared(args, ["--house", "--config"], 2);
273
- const house = await selectedHouse(values, directory);
274
- if (!house || rest.length === 0) throw new Error(SOURCE_USAGE);
266
+ const { values, rest } = declared(args, ["--config"], 2);
267
+ if (rest.length === 0) throw new Error(SOURCE_USAGE);
268
+ const house = await storedHouse(directory);
275
269
  const cancellation = new AbortController();
276
270
  process.once("SIGINT", () => cancellation.abort());
277
271
  const result = await captureSource(
@@ -314,9 +308,9 @@ function writeSourceOriginals(instance, service) {
314
308
  }
315
309
 
316
310
  async function runSourceRetryFiles(args, directory) {
317
- const { values, rest } = declared(args, ["--house", "--config"], 2);
318
- const house = await selectedHouse(values, directory);
319
- if (!house || rest.length === 0) throw new Error(SOURCE_USAGE);
311
+ const { values, rest } = declared(args, ["--config"], 2);
312
+ if (rest.length === 0) throw new Error(SOURCE_USAGE);
313
+ const house = await storedHouse(directory);
320
314
  const cancellation = new AbortController();
321
315
  process.once("SIGINT", () => cancellation.abort());
322
316
  const result = await retrySourceOriginals({
@@ -359,9 +353,9 @@ async function runSourceStatus(args, directory) {
359
353
  }
360
354
 
361
355
  async function runSourceReset(args, directory) {
362
- const { values, rest } = declared(args, ["--house", "--config"], 2);
363
- const house = await selectedHouse(values, directory);
364
- if (!house || rest.length === 0) throw new Error(SOURCE_USAGE);
356
+ const { values, rest } = declared(args, ["--config"], 2);
357
+ if (rest.length === 0) throw new Error(SOURCE_USAGE);
358
+ const house = await storedHouse(directory);
365
359
  const result = await resetSource({
366
360
  house,
367
361
  directory,
@@ -377,11 +371,11 @@ async function runSourceReset(args, directory) {
377
371
  }
378
372
 
379
373
  async function runRepositoryCapture(args, directory) {
380
- const { values, rest } = declared(args, ["--house", "--room", "--source-handle"], 1);
381
- const house = await selectedHouse(values, directory);
374
+ const { values, rest } = declared(args, ["--room", "--source-handle"], 1);
382
375
  const room = values["--room"];
383
376
  const sourceHandle = values["--source-handle"];
384
- if (!house || !room || !sourceHandle || rest.length !== 1) throw new Error(REPOSITORY_USAGE);
377
+ if (!room || !sourceHandle || rest.length !== 1) throw new Error(REPOSITORY_USAGE);
378
+ const house = await storedHouse(directory);
385
379
  const cancellation = new AbortController();
386
380
  const cancel = () => cancellation.abort();
387
381
  process.once("SIGINT", cancel);
@@ -429,9 +423,8 @@ function writeConversationFiles(supervised) {
429
423
  }
430
424
 
431
425
  async function runWorkerRun(args, directory) {
432
- const { values } = declared(args, ["--house", "--config"], 0);
433
- const house = await selectedHouse(values, directory);
434
- if (!house) throw new Error(WORKER_USAGE);
426
+ const { values } = declared(args, ["--config"], 0);
427
+ const house = await storedHouse(directory);
435
428
  const supervised = await runWorkers(
436
429
  {
437
430
  house,
@@ -500,12 +493,12 @@ function syncCancellation() {
500
493
  }
501
494
 
502
495
  async function runSyncBootstrap(args, directory) {
503
- const { values, lists, rest } = declared(args, ["--house", "--prefix"], 1, {
496
+ const { lists, rest } = declared(args, ["--prefix"], 1, {
504
497
  repeated: ["--prefix"],
505
498
  exact: true,
506
499
  });
507
- const house = await selectedHouse(values, directory);
508
- if (!house || lists["--prefix"].length === 0) throw new Error(SYNC_BOOTSTRAP_USAGE);
500
+ if (lists["--prefix"].length === 0) throw new Error(SYNC_BOOTSTRAP_USAGE);
501
+ const house = await storedHouse(directory);
509
502
  const run = syncCancellation();
510
503
  try {
511
504
  const result = await bootstrap({
@@ -524,9 +517,8 @@ async function runSyncBootstrap(args, directory) {
524
517
  }
525
518
 
526
519
  async function runSyncPull(args, directory) {
527
- const { values, rest } = declared(args, ["--house"], 1, { exact: true });
528
- const house = await selectedHouse(values, directory);
529
- if (!house) throw new Error(SYNC_PULL_USAGE);
520
+ const { rest } = declared(args, [], 1, { exact: true });
521
+ const house = await storedHouse(directory);
530
522
  const run = syncCancellation();
531
523
  try {
532
524
  const result = await pull({
@@ -544,9 +536,8 @@ async function runSyncPull(args, directory) {
544
536
  }
545
537
 
546
538
  async function runSyncRebootstrap(args, directory) {
547
- const { values, rest } = declared(args, ["--house"], 1, { exact: true });
548
- const house = await selectedHouse(values, directory);
549
- if (!house) throw new Error(SYNC_REBOOTSTRAP_USAGE);
539
+ const { rest } = declared(args, [], 1, { exact: true });
540
+ const house = await storedHouse(directory);
550
541
  const run = syncCancellation();
551
542
  try {
552
543
  const result = await rebootstrap({
@@ -571,9 +562,8 @@ async function readChanges(path) {
571
562
  }
572
563
 
573
564
  async function runSyncSubmit(args, directory) {
574
- const { values, rest } = declared(args, ["--house", "--changes"], 1, { exact: true });
575
- const house = await selectedHouse(values, directory);
576
- if (!house) throw new Error(SYNC_SUBMIT_USAGE);
565
+ const { values, rest } = declared(args, ["--changes"], 1, { exact: true });
566
+ const house = await storedHouse(directory);
577
567
  const run = syncCancellation();
578
568
  try {
579
569
  const result = await submit({
@@ -646,11 +636,11 @@ async function mountEmitted(packagePath, options, run) {
646
636
  }
647
637
 
648
638
  async function runMigrateMount(args, directory) {
649
- const { values, rest } = declared(args, ["--house", "--subtree"], 1, { exact: true });
650
- const house = await selectedHouse(values, directory);
651
- if (!house || rest.length !== 1 || values["--subtree"] === undefined) {
639
+ const { values, rest } = declared(args, ["--subtree"], 1, { exact: true });
640
+ if (rest.length !== 1 || values["--subtree"] === undefined) {
652
641
  throw new Error(MIGRATE_MOUNT_USAGE);
653
642
  }
643
+ const house = await storedHouse(directory);
654
644
  const run = migrateCancellation();
655
645
  try {
656
646
  await mountEmitted(rest[0], { house, directory, subtree: values["--subtree"] }, run);
@@ -660,18 +650,17 @@ async function runMigrateMount(args, directory) {
660
650
  }
661
651
 
662
652
  async function runMigrateRun(args, directory) {
663
- const { values, rest } = declared(args, ["--house", "--subtree", "--package", "--config"], 1, {
653
+ const { values, rest } = declared(args, ["--subtree", "--package", "--config"], 1, {
664
654
  exact: true,
665
655
  });
666
- const house = await selectedHouse(values, directory);
667
656
  if (
668
- !house ||
669
657
  rest.length !== 1 ||
670
658
  values["--subtree"] === undefined ||
671
659
  values["--package"] === undefined
672
660
  ) {
673
661
  throw new Error(MIGRATE_RUN_USAGE);
674
662
  }
663
+ const house = await storedHouse(directory);
675
664
  const run = migrateCancellation();
676
665
  try {
677
666
  const emitted = await emitPackage(
@@ -688,9 +677,9 @@ async function runMigrateRun(args, directory) {
688
677
  }
689
678
 
690
679
  async function runMigrateUnmount(args, directory) {
691
- const { values, rest } = declared(args, ["--house", "--confirm"], 1, { exact: true });
692
- const house = await selectedHouse(values, directory);
693
- if (!house || rest.length !== 1) throw new Error(MIGRATE_UNMOUNT_USAGE);
680
+ const { values, rest } = declared(args, ["--confirm"], 1, { exact: true });
681
+ if (rest.length !== 1) throw new Error(MIGRATE_UNMOUNT_USAGE);
682
+ const house = await storedHouse(directory);
694
683
  const run = migrateCancellation();
695
684
  try {
696
685
  const result = await unmountSubtree({
@@ -746,12 +735,12 @@ function attachmentCancellation() {
746
735
  async function runAttachmentUpload(args, directory) {
747
736
  const { values, rest } = declared(
748
737
  args,
749
- ["--house", "--room", "--private", "--name", "--media-type"],
738
+ ["--room", "--private", "--name", "--media-type"],
750
739
  1,
751
740
  { flags: ["--private"] },
752
741
  );
753
- const house = await selectedHouse(values, directory);
754
- if (!house || rest.length !== 1) throw new Error(ATTACHMENT_UPLOAD_USAGE);
742
+ if (rest.length !== 1) throw new Error(ATTACHMENT_UPLOAD_USAGE);
743
+ const house = await storedHouse(directory);
755
744
  const run = attachmentCancellation();
756
745
  try {
757
746
  const result = await uploadAttachment({
@@ -773,13 +762,13 @@ async function runAttachmentUpload(args, directory) {
773
762
  async function runAttachmentGet(args, directory) {
774
763
  const { values } = declared(
775
764
  args,
776
- ["--house", "--attachment", "--room", "--private", "--output"],
765
+ ["--attachment", "--room", "--private", "--output"],
777
766
  0,
778
767
  { flags: ["--private"] },
779
768
  );
780
- const house = await selectedHouse(values, directory);
781
769
  const attachment = values["--attachment"];
782
- if (!house || !attachment) throw new Error(ATTACHMENT_GET_USAGE);
770
+ if (!attachment) throw new Error(ATTACHMENT_GET_USAGE);
771
+ const house = await storedHouse(directory);
783
772
  const run = attachmentCancellation();
784
773
  try {
785
774
  const result = await getAttachment({
@@ -805,9 +794,8 @@ async function runAttachment(args, directory) {
805
794
  }
806
795
 
807
796
  async function runHostReport(args, directory) {
808
- const { values } = declared(args, ["--house", "--config"], 0, { exact: true });
809
- const house = await selectedHouse(values, directory);
810
- if (!house) throw new Error(HOST_USAGE);
797
+ const { values } = declared(args, ["--config"], 0, { exact: true });
798
+ const house = await storedHouse(directory);
811
799
  const observed = await reportHost({
812
800
  house,
813
801
  directory,
@@ -819,9 +807,8 @@ async function runHostReport(args, directory) {
819
807
  }
820
808
 
821
809
  async function runAgentsUpdate(args, directory) {
822
- const { values } = declared(args, ["--house", "--config"], 0, { exact: true });
823
- const house = await selectedHouse(values, directory);
824
- if (!house) throw new Error(AGENTS_USAGE);
810
+ const { values } = declared(args, ["--config"], 0, { exact: true });
811
+ const house = await storedHouse(directory);
825
812
  const { updated } = await updateAgents({
826
813
  house,
827
814
  directory,
@@ -847,10 +834,10 @@ async function runAgentsUpdate(args, directory) {
847
834
  }
848
835
 
849
836
  async function runAgentsSignIn(args, directory) {
850
- const { values, rest } = declared(args, ["--house", "--config"], 1, { exact: true });
851
- const house = await selectedHouse(values, directory);
837
+ const { values, rest } = declared(args, ["--config"], 1, { exact: true });
852
838
  const [kind] = rest;
853
- if (!house || !kind) throw new Error(AGENTS_USAGE);
839
+ if (!kind) throw new Error(AGENTS_USAGE);
840
+ const house = await storedHouse(directory);
854
841
  const { status, reported } = await signInAgent({
855
842
  house,
856
843
  directory,
@@ -867,13 +854,12 @@ async function runAgentsSignIn(args, directory) {
867
854
  }
868
855
 
869
856
  async function runWorkspaceHost(args, directory) {
870
- const { values } = declared(args, ["--house", "--config", "--passes"], 0, { exact: true });
871
- const house = await selectedHouse(values, directory);
872
- if (!house) throw new Error(WORKSPACE_USAGE);
857
+ const { values } = declared(args, ["--config", "--passes"], 0, { exact: true });
873
858
  const passes = values["--passes"] === undefined ? undefined : Number(values["--passes"]);
874
859
  if (passes !== undefined && (!Number.isSafeInteger(passes) || passes < 1)) {
875
860
  throw new Error(WORKSPACE_USAGE);
876
861
  }
862
+ const house = await storedHouse(directory);
877
863
  const served = await serveWorkspaces({
878
864
  house,
879
865
  directory,
@@ -894,9 +880,8 @@ async function runWorkspaceHost(args, directory) {
894
880
  }
895
881
 
896
882
  async function runTelegramConnect(args, directory) {
897
- const { values } = declared(args, ["--house"], 0, { exact: true });
898
- const house = await selectedHouse(values, directory);
899
- if (!house) throw new Error(TELEGRAM_USAGE);
883
+ declared(args, [], 0, { exact: true });
884
+ const house = await storedHouse(directory);
900
885
  const cancellation = new AbortController();
901
886
  const cancel = () => cancellation.abort();
902
887
  process.once("SIGINT", cancel);
@@ -914,9 +899,8 @@ async function runTelegramConnect(args, directory) {
914
899
  }
915
900
 
916
901
  async function runTelegramDisconnect(args, directory) {
917
- const { values } = declared(args, ["--house"], 0, { exact: true });
918
- const house = await selectedHouse(values, directory);
919
- if (!house) throw new Error(TELEGRAM_USAGE);
902
+ declared(args, [], 0, { exact: true });
903
+ const house = await storedHouse(directory);
920
904
  const cancellation = new AbortController();
921
905
  const cancel = () => cancellation.abort();
922
906
  process.once("SIGINT", cancel);
@@ -936,13 +920,12 @@ async function runTelegramDisconnect(args, directory) {
936
920
  }
937
921
 
938
922
  async function runTelegramServe(args, directory) {
939
- const { values } = declared(args, ["--house", "--passes"], 0, { exact: true });
940
- const house = await selectedHouse(values, directory);
941
- if (!house) throw new Error(TELEGRAM_USAGE);
923
+ const { values } = declared(args, ["--passes"], 0, { exact: true });
942
924
  const passes = values["--passes"] === undefined ? undefined : Number(values["--passes"]);
943
925
  if (passes !== undefined && (!Number.isSafeInteger(passes) || passes < 1)) {
944
926
  throw new Error(TELEGRAM_USAGE);
945
927
  }
928
+ const house = await storedHouse(directory);
946
929
  const cancellation = new AbortController();
947
930
  const cancel = () => cancellation.abort();
948
931
  process.once("SIGINT", cancel);
package/bootstrap.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.1.0-alpha.21",
3
+ "version": "0.1.0-alpha.23",
4
4
  "hosts": {
5
5
  "linux": {
6
- "command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.21/connect-linux.sh)",
6
+ "command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.23/connect-linux.sh)",
7
7
  "asset": {
8
8
  "name": "connect-linux.sh",
9
- "url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.21/connect-linux.sh"
9
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.23/connect-linux.sh"
10
10
  },
11
11
  "workspace": {
12
12
  "argument": "--workspace",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentshouse/kit",
3
- "version": "0.1.0-alpha.21",
3
+ "version": "0.1.0-alpha.23",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
package/src/bytes.js CHANGED
@@ -31,6 +31,11 @@ async function downloadBody(response, signal) {
31
31
  return Buffer.concat(chunks, size);
32
32
  }
33
33
 
34
+ function framedBody(request) {
35
+ if (request.method !== "POST" || !Buffer.isBuffer(request.body)) throw new KitRefusal("kit_byte_body_invalid");
36
+ return Buffer.concat([Buffer.from(`${request.operation}\n`, "utf8"), request.body]);
37
+ }
38
+
34
39
  export async function transferBytes(request, dependencies = {}) {
35
40
  if (request.method !== "GET" && request.method !== "POST") {
36
41
  throw new KitRefusal("kit_byte_method_invalid", request.method);
@@ -41,23 +46,27 @@ export async function transferBytes(request, dependencies = {}) {
41
46
  if (typeof request.operation !== "string" || request.operation.length === 0) {
42
47
  throw new KitRefusal("kit_byte_operation_invalid");
43
48
  }
49
+ const framed = request.framed === true;
50
+ const body = framed ? framedBody(request) : request.body;
44
51
  if (request.method === "POST") {
45
- if (!Buffer.isBuffer(request.body)) throw new KitRefusal("kit_byte_body_invalid");
46
- if (request.body.length > BYTE_GRANT_POST_BYTES_MAX) {
52
+ if (!Buffer.isBuffer(body)) throw new KitRefusal("kit_byte_body_invalid");
53
+ if (body.length > BYTE_GRANT_POST_BYTES_MAX) {
47
54
  throw new KitRefusal("original_bytes_exceeded");
48
55
  }
49
56
  }
50
57
  const fetcher = dependencies.fetch ?? fetch;
51
- const headers = {
52
- [BYTE_GRANT_OPERATION_HEADER]: request.operation,
53
- };
54
- if (request.method === "POST") headers["content-type"] = "application/octet-stream";
58
+ const headers = framed
59
+ ? { "content-type": "text/plain; charset=utf-8" }
60
+ : {
61
+ [BYTE_GRANT_OPERATION_HEADER]: request.operation,
62
+ ...(request.method === "POST" ? { "content-type": "application/octet-stream" } : {}),
63
+ };
55
64
  let response;
56
65
  try {
57
66
  response = await fetcher(request.url, {
58
67
  method: request.method,
59
68
  headers,
60
- body: request.method === "POST" ? request.body : undefined,
69
+ body: request.method === "POST" ? body : undefined,
61
70
  redirect: "manual",
62
71
  signal: request.signal,
63
72
  });
package/src/contract.js CHANGED
@@ -308,6 +308,8 @@ export const WORKING_COPY_PUSH_METHOD = "house/git/push";
308
308
  export const WORKING_COPY_INDEX_METHOD = "house/git/index";
309
309
  export const WORKING_COPY_PUSH_HELP =
310
310
  "Submit a Room commit to House; default HEAD. Use --owner only outside an Agent Session. Native git push is not a House remote.";
311
+ export const WORKING_COPY_UPLOAD_COMMAND = "prepare_edit_upload";
312
+ export const MCP_REQUEST_BODY_MAX_BYTES = 4 * 1024 * 1024;
311
313
 
312
314
  export const MIGRATOR_CONTRACT = "kit-migrator/1";
313
315
  export const MIGRATOR_CATEGORY = "migrator";
package/src/login.js CHANGED
@@ -14,6 +14,7 @@ import { writeText } from "./state.js";
14
14
 
15
15
  const ENVIRONMENT =
16
16
  /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
17
+ const DEFAULT_HOUSE = "https://agents.house";
17
18
 
18
19
  function verifier() {
19
20
  return randomBytes(32).toString("base64url");
@@ -112,19 +113,29 @@ function houseOrigin(value) {
112
113
  return value;
113
114
  }
114
115
 
115
- export async function storedHouse(directory, firstLogin = false, requested = undefined) {
116
- let value;
116
+ async function recordedHouse(directory) {
117
117
  try {
118
- value = (await readFile(join(directory, "house"), "utf8")).trim();
118
+ return (await readFile(join(directory, "house"), "utf8")).trim();
119
119
  } catch (error) {
120
- if (error.code === "ENOENT") {
121
- if ((firstLogin || requested !== undefined) &&
122
- await storedEnvironment(directory) === null && !await storedCredentialPresent(directory)) {
123
- return houseOrigin(requested ?? "https://agents.house");
124
- }
125
- }
120
+ if (error.code === "ENOENT") return null;
126
121
  throw new KitRefusal("kit_house_invalid", error.code ?? "unreadable");
127
122
  }
123
+ }
124
+
125
+ export async function storedHouse(directory) {
126
+ const value = await recordedHouse(directory);
127
+ if (value === null) throw new KitRefusal("kit_house_invalid", "ENOENT");
128
+ return houseOrigin(value);
129
+ }
130
+
131
+ export async function loginHouse(directory, requested) {
132
+ const value = await recordedHouse(directory);
133
+ if (value === null) {
134
+ if (await storedEnvironment(directory) !== null || await storedCredentialPresent(directory)) {
135
+ throw new KitRefusal("kit_house_invalid", "ENOENT");
136
+ }
137
+ return houseOrigin(requested ?? DEFAULT_HOUSE);
138
+ }
128
139
  houseOrigin(value);
129
140
  if (requested !== undefined && requested !== value) {
130
141
  throw new KitRefusal("kit_house_mismatch", "this Environment is bound to another House origin");
@@ -133,7 +144,7 @@ export async function storedHouse(directory, firstLogin = false, requested = und
133
144
  }
134
145
 
135
146
  export function enrollmentAct(options, retained, host) {
136
- if (options.label !== undefined || (options.environment !== undefined && !ENVIRONMENT.test(options.environment))) {
147
+ if (options.environment !== undefined && !ENVIRONMENT.test(options.environment)) {
137
148
  throw new KitRefusal("kit_login_rejected", ENROLLMENT_USAGE);
138
149
  }
139
150
  const environment = options.environment ?? retained;
package/src/worker.js CHANGED
@@ -1,4 +1,4 @@
1
- import { randomBytes } from "node:crypto";
1
+ import { createHash, randomBytes } from "node:crypto";
2
2
  import { existsSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { setTimeout as delay } from "node:timers/promises";
@@ -11,6 +11,7 @@ import {
11
11
  import { readBindings, removeBinding, writeBinding } from "./binding.js";
12
12
  import { readContinuation, readContinuationRuns, writeContinuation, removeContinuation } from "./continuation.js";
13
13
  import { bridgedFrame, openBridge } from "./bridge.js";
14
+ import { transferBytes } from "./bytes.js";
14
15
  import { assistantBlocks } from "./blocks.js";
15
16
  import { retryConversationFiles } from "./conversation-originals.js";
16
17
  import { openAssistantDrafts } from "./draft.js";
@@ -25,6 +26,7 @@ import {
25
26
  ownerCaller,
26
27
  pushWorkingCopy,
27
28
  recordIndexChange,
29
+ uploadedPaths,
28
30
  workingCopyPaths,
29
31
  } from "./working-copy.js";
30
32
  import { readConfiguration } from "./configuration.js";
@@ -32,6 +34,7 @@ import { launchBase, readyLaunchDirectory } from "./directory.js";
32
34
  import {
33
35
  BRIDGE_NONCE_ENVIRONMENT,
34
36
  BRIDGE_SOCKET_ENVIRONMENT,
37
+ MCP_REQUEST_BODY_MAX_BYTES,
35
38
  PROVIDER_CONTINUATION_UNAVAILABLE,
36
39
  WORKER_COMMAND_BATCH_MAX,
37
40
  WORKER_COMMAND_POLL_MS,
@@ -42,6 +45,7 @@ import {
42
45
  WORKER_SUPERVISION_POLL_MS,
43
46
  WORKING_COPY_INDEX_METHOD,
44
47
  WORKING_COPY_PUSH_METHOD,
48
+ WORKING_COPY_UPLOAD_COMMAND,
45
49
  } from "./contract.js";
46
50
  import { EpochReportRequired, formatCommandError, houseFailed, KitRefusal } from "./refusal.js";
47
51
  import {
@@ -299,20 +303,61 @@ function acceptedWith(text) {
299
303
  return { refused: null, written: [...written.values()] };
300
304
  }
301
305
 
306
+ function toolFrame(name, args, operation) {
307
+ return bridgedFrame({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name, arguments: args } }, operation);
308
+ }
309
+
310
+ function uploadBody(changes) {
311
+ const body = Buffer.from(JSON.stringify({ changes }), "utf8");
312
+ if (body.length > MCP_REQUEST_BODY_MAX_BYTES) return body;
313
+ return Buffer.concat([body, Buffer.alloc(MCP_REQUEST_BODY_MAX_BYTES + 1 - body.length, " ")]);
314
+ }
315
+
302
316
  async function sessionPush(context, bound, authority, frame, signal) {
303
- const edit = async (body, sent) => {
304
- const answer = await callHouseMcp(
305
- context.session,
306
- await sessionAuthority(context.session, bound.session, authority, sent),
307
- bridgedFrame(
308
- { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "edit", arguments: { changes: body.changes } } },
309
- body.operation_id,
310
- ),
317
+ const call = async (sent, called) =>
318
+ callHouseMcp(context.session, await sessionAuthority(context.session, bound.session, authority, sent), called, sent);
319
+ const refused = (answer) =>
320
+ answer.error !== undefined
321
+ ? refusedWith(answer.error.message ?? "kit_house_refused")
322
+ : answer.result?.isError === true
323
+ ? refusedWith(answerText(answer.result))
324
+ : null;
325
+ const stage = async (body, sent) => {
326
+ const bytes = uploadBody(body.changes);
327
+ const sha256 = createHash("sha256").update(bytes).digest("hex");
328
+ const prepare = { paths: uploadedPaths(body.changes), bytes: bytes.length, sha256 };
329
+ const prepared = await call(
330
+ sent,
331
+ toolFrame("run_command", { command: WORKING_COPY_UPLOAD_COMMAND, arguments: prepare }, body.operation_id),
332
+ );
333
+ const refusal = refused(prepared);
334
+ if (refusal !== null) return refusal;
335
+ const transfer = answerYaml(answerText(prepared.result))?.transfer;
336
+ const staged = (
337
+ await transferBytes(
338
+ { url: transfer?.url, method: "POST", operation: transfer?.operation, body: bytes, framed: true, signal: sent },
339
+ context.session.dependencies,
340
+ )
341
+ ).transfer;
342
+ if (typeof staged?.url !== "string" || typeof staged.operation !== "string") {
343
+ throw new KitRefusal("kit_house_answer_unreadable", WORKING_COPY_UPLOAD_COMMAND);
344
+ }
345
+ return { upload: { url: staged.url, operation: staged.operation, bytes: bytes.length, sha256 } };
346
+ };
347
+ const edit = async (body, sent, retain) => {
348
+ let { upload } = body;
349
+ const inline = toolFrame("edit", { changes: body.changes }, body.operation_id);
350
+ if (upload === undefined && Buffer.byteLength(JSON.stringify(inline)) > MCP_REQUEST_BODY_MAX_BYTES) {
351
+ const staged = await stage(body, sent);
352
+ if (staged.upload === undefined) return staged;
353
+ ({ upload } = staged);
354
+ await retain(upload);
355
+ }
356
+ const answer = await call(
311
357
  sent,
358
+ upload === undefined ? inline : toolFrame("edit", { paths: uploadedPaths(body.changes), upload }, body.operation_id),
312
359
  );
313
- if (answer.error !== undefined) return refusedWith(answer.error.message ?? "kit_house_refused");
314
- const text = answerText(answer.result);
315
- return answer.result?.isError === true ? refusedWith(text) : acceptedWith(text);
360
+ return refused(answer) ?? acceptedWith(answerText(answer.result));
316
361
  };
317
362
  let outcome;
318
363
  try {
@@ -914,6 +914,10 @@ function pushedPaths(changes) {
914
914
  return new Set(changes.flatMap((change) => (change.op === "rename" ? [change.path, change.to] : [change.path])));
915
915
  }
916
916
 
917
+ export function uploadedPaths(changes) {
918
+ return [...pushedPaths(changes)].sort();
919
+ }
920
+
917
921
  async function enumerated(context, copy, since, signal) {
918
922
  const answer = await door(
919
923
  context.session,
@@ -1104,12 +1108,18 @@ async function expired(context, copy, text, signal) {
1104
1108
  return finish(context, accepted, signal);
1105
1109
  }
1106
1110
 
1107
- async function settle(context, copy, caller, signal) {
1111
+ async function settle(context, copy, caller, signal, restaged = false) {
1108
1112
  const { push } = copy;
1109
1113
  if (!sameCaller(push.caller, caller.binding)) return orphaned(context, copy, signal);
1114
+ let staged = push.upload !== undefined;
1115
+ const retain = async (upload) => {
1116
+ staged = true;
1117
+ await writeCopy(context.directory, { ...copy, push: { ...push, upload } }, signal);
1118
+ };
1110
1119
  let answer;
1111
1120
  try {
1112
- answer = await caller.submit({ operation_id: push.operation_id, changes: push.changes }, signal);
1121
+ const { operation_id, changes, upload } = push;
1122
+ answer = await caller.submit({ operation_id, changes, ...(upload === undefined ? {} : { upload }) }, signal, retain);
1113
1123
  } catch (error) {
1114
1124
  if (!definite(error)) {
1115
1125
  throw new KitRefusal("kit_working_copy_push_uncertain", "run house git push again to recover its outcome");
@@ -1118,6 +1128,12 @@ async function settle(context, copy, caller, signal) {
1118
1128
  answer = { refused: { code: error.code, text: formatCommandError(error), current: Array.isArray(current) ? current : null } };
1119
1129
  }
1120
1130
  const { refused } = answer;
1131
+ if (refused?.code === "upload_unavailable" && staged && !restaged) {
1132
+ const { upload, ...unstaged } = push;
1133
+ const cleared = { ...copy, push: unstaged };
1134
+ await writeCopy(context.directory, cleared, signal);
1135
+ return settle(context, cleared, caller, signal, true);
1136
+ }
1121
1137
  if (refused?.code === "operation_expired") return expired(context, copy, refused.text, signal);
1122
1138
  if (refused === null) {
1123
1139
  const pushed = pushedPaths(push.changes);