@agentshouse/kit 0.1.0-alpha.2 → 0.1.0-alpha.20

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.2/connect-linux.sh)
13
+ bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.20/connect-linux.sh)
14
14
  ```
15
15
 
16
16
  The release asset pins one immutable multi-architecture image digest. It creates
@@ -20,12 +20,99 @@ and starts the container with Docker's `unless-stopped` lifecycle. Pass
20
20
  `--workspace /absolute/path` for one other workspace root or `--manual` for
21
21
  the existing URL and one-time-code login route. `--house <origin>` selects
22
22
  another House and is retained in Kit home. A repeated invocation reuses the
23
- same container, credential and Environment; a different installed image is
24
- left for the explicit update journey.
23
+ same container, credential and Environment.
24
+
25
+ Running a newer release's bootstrap the same way is the only update. When the
26
+ installed container runs another image, the bootstrap pulls this release's
27
+ image and asks the running resident Kit to drain with `kit resident drain`. The
28
+ resident stops acquiring Runs, resumptions, setup and agent CLI work, while the
29
+ Sessions it supervises keep their renewal, owner stops, answers and reports.
30
+ While any Agent Session is still running or unreported, including one whose Run
31
+ was being acquired when the drain began, the bootstrap names it and leaves the
32
+ container as it is; rerun it once they end. Once nothing remains,
33
+ the resident stops renewing, releases its exact lease and stays quiescent, and
34
+ the bootstrap replaces the container with one on this release's image over the
35
+ same Kit home and workspace. The credential, Environment, Kit-home state and
36
+ everything under the workspace are kept; anything installed elsewhere in the old
37
+ container is not. A stopped container is replaced without a drain.
38
+
39
+ If House refuses the installed Kit with `kit_upgrade_required`, including in
40
+ its answer to a command the resident serves, the resident calls House no more,
41
+ neither to release nor to report. It keeps supervising
42
+ every running Session locally and never stops one to make progress. A Session
43
+ that ends keeps its exact binding, stop observation and outcome in Kit home,
44
+ and the drain answers ready once no supervised process runs. The updated Kit
45
+ waits for the old lease to expire, acquires the next one and reports each held
46
+ Session before it acquires any work, restoring no process and inventing no
47
+ outcome.
48
+
49
+ The versioned `bootstrap.json` is the machine-readable command source for this
50
+ release. It is exported from the npm package as `@agentshouse/kit/bootstrap`
51
+ (JSON import) and published byte-for-byte beside `connect-linux.sh` in the
52
+ GitHub release. Its schema version is `1`, its `version` is the Kit package
53
+ version, and `hosts` contains only bootstrap scripts published in that release.
54
+ The package exports only the `./bootstrap` subpath; `./package.json` and source
55
+ subpaths are not exported. For this release, only `hosts.linux` exists. In this
56
+ schema example, `<version>` stands for the package's version:
57
+
58
+ ```json
59
+ {
60
+ "schemaVersion": 1,
61
+ "version": "<version>",
62
+ "hosts": {
63
+ "linux": {
64
+ "command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v<version>/connect-linux.sh)",
65
+ "asset": {
66
+ "name": "connect-linux.sh",
67
+ "url": "https://github.com/agentshouse/kit/releases/download/v<version>/connect-linux.sh"
68
+ },
69
+ "workspace": { "argument": "--workspace", "quoting": "posix" }
70
+ }
71
+ }
72
+ }
73
+ ```
74
+
75
+ `command` is complete for the default workspace. To show a selected absolute
76
+ workspace, append the declared `workspace.argument` and the path as one POSIX
77
+ shell-quoted argument. For example, `/home/user/My Agents' House` becomes
78
+ `--workspace '/home/user/My Agents'\'' House'`. Quote the entire path before
79
+ adding it to a copyable shell command; never interpolate it as shell syntax.
80
+ The bootstrap validates an absolute workspace root and encodes it as one Docker
81
+ `--mount` CSV source field, including commas and double quotes in the name.
82
+ The package manifest contains no image digest: the release workflow
83
+ fills the immutable image digest into the published script after the package
84
+ has been built.
25
85
 
26
86
  The bootstrap does not install native Kit on an ordinary computer. The native
27
87
  package remains for managed Nodes and an explicitly selected eligible Kit host.
28
88
 
89
+ The same bootstrap is the host's entry point for the existing `kit` commands:
90
+ append `kit` and the command to the connect command above, as in
91
+ `bash <(curl -fsSL …/connect-linux.sh) kit apply` or
92
+ `bash <(curl -fsSL …/connect-linux.sh) kit telegram connect`. It forwards the
93
+ command and its arguments unchanged into the installed container through the
94
+ host's Docker client and ends with the command's own output and exit status.
95
+ Standard input stays attached and a terminal stays a terminal, so a hidden
96
+ prompt such as the Telegram bot token is read inside the container and never
97
+ becomes an argument or printed output. With the output redirected, the
98
+ command's output and errors stay separate and the terminal stops echoing typed
99
+ input until the command ends. A running container runs the command beside its
100
+ resident Kit; a stopped one runs it in a one-off container over the same Kit
101
+ home and workspace and stays stopped. `kit login` takes the bootstrap's own
102
+ login route, a one-off container on the host network so the host browser can
103
+ return to its loopback listener, or `--manual` for the URL and one-time code.
104
+ Forwarding installs no host-native Kit and gives the container no Docker socket
105
+ or further mount; it refuses when no Kit is installed or the installed one
106
+ differs from this bootstrap's.
107
+
108
+ The owner's working copy push is forwarded the same way: run
109
+ `bash <(curl -fsSL …/connect-linux.sh) house git push [commit] --owner` from a
110
+ directory beneath the workspace root, such as a Room copy under
111
+ `working-copies/rooms/`. The bootstrap runs the container's `house` in the
112
+ matching directory beneath `/agents/house`, through the resident's lease when
113
+ the container runs, and refuses a directory outside the workspace before
114
+ anything reaches House.
115
+
29
116
  ## Building the package
30
117
 
31
118
  The repository checkout is the development source. Build the package
@@ -48,7 +135,8 @@ Runtime dependencies stay minimal and exactly pinned in `package.json` and
48
135
  `package-lock.json`: House's own `@agentshouse/mdmodel`, `yaml`, which reads
49
136
  House's text answers, and the Agent Client Protocol SDK
50
137
  `@agentclientprotocol/sdk` with its peer `zod`, which is this Kit's one protocol
51
- client for the compiled adapter contracts. The SDK's approval
138
+ client for the compiled adapter contracts, and `ws` for the authenticated Kit
139
+ control stream. The SDK's approval
52
140
  receipt is [`docs/agents/acp-sdk-dependency-approval.json`](docs/agents/acp-sdk-dependency-approval.json),
53
141
  which the User wrote once this adoption's proof had passed. The adapter
54
142
  packages themselves are Environment prerequisites, not Kit dependencies, and
@@ -575,6 +663,17 @@ minimum's major - under
575
663
  `$HOUSE_KIT_HOME/agents/<kind>/releases/<release>/`, never touching what the
576
664
  owner installed globally, and an acquired route's command is that installed
577
665
  executable rather than a path anyone writes into `kit.json`.
666
+ The resident Kit also listens for an `agents` availability pointer on its
667
+ control stream. It reads the whole wanted list from House over HTTPS, removes
668
+ Kit-owned CLI directories that are no longer wanted, installs newly wanted
669
+ kinds, and reports its held state immediately, including while another Agent
670
+ run remains active. A newly wanted kind that needs provider authorization uses
671
+ the existing guided House login ceremony; Kit reports readiness again when it
672
+ ends. A reconnect reads the list again, so a pointer missed during a stream
673
+ outage changes no authority.
674
+ Removal waits for an active worker pass or any unresolved live WorkerSession
675
+ binding to stop using a CLI directory; the Kit keeps reporting that kind until
676
+ it removes the directory.
578
677
 
579
678
  Every candidate release is proved before it serves: the Kit installs it beside
580
679
  the current release, runs the contract's own conformance check - the protocol's
@@ -669,11 +768,13 @@ prompt's stop reason through the contract's own table to one of four turn
669
768
  outcomes: completed, interrupted, refused by the provider, or exhausted. A stop
670
769
  reason the contract does not name, including a vendor-specific one a different
671
770
  contract names, fails the turn as `kit_adapter_contract_mismatch` rather than
672
- being guessed. It sends the request
673
- as the first prompt block, followed by current House guidance fetched through
674
- the Run connection. Missing or unreadable guidance refuses the turn; the
675
- launcher does not copy the private Routine instruction into the acquisition
676
- request. It forwards only adapter-classified User-visible assistant
771
+ being guessed. After committing the Run, and again on every resumption, the
772
+ Kit reads what the Session was started for from House's
773
+ `/kit/runs/<run>/request` with the Session credential: the Routine Request with
774
+ its input, references, requester and the pinned revision's instruction, or the
775
+ sealed conversation turn. It sends that as the first prompt block, followed by
776
+ current House guidance fetched through the Run connection, so the Agent calls
777
+ nothing to learn it. An unreadable request or guidance refuses the turn. It forwards only adapter-classified User-visible assistant
677
778
  text back over the local channel, and leaves reasoning, tool traces, and
678
779
  terminal output where they are. The supervised process's own House calls travel
679
780
  that same channel through the `house` CLI. The Kit holds the WorkerRunToken, replaces it in place when
@@ -736,7 +837,9 @@ nonzero exit status. The CLI holds
736
837
  no credential: it calls over its Session's bridge, which attaches the Session
737
838
  credential and refuses a caller without that Session's nonce as
738
839
  `kit_bridge_unauthorized`. House network calls use only the selected
739
- `2026-07-28` profile.
840
+ `2026-07-28` profile. `house git push` is the one local verb House does not list;
841
+ [working copies](#working-copies) describe it, and `house --help` and
842
+ `house git push --help` explain it.
740
843
 
741
844
  Two verbs name files on the Session's own computer: `upload_attachment` takes
742
845
  one `path`, and `append_record` takes `attachments` as local paths. The CLI
@@ -819,8 +922,8 @@ receiving a complete byte stream, validating any reported size, and writing the
819
922
  file. A lost acknowledgement preserves the completed local file for House's
820
923
  idempotent admission and later cleanup. Failed files remain pending; House's
821
924
  all-files-ready gate keeps their turn unprocessed.
822
- The provider receives absolute local paths beside the owning messages, both in
823
- its initial prompt and in `session_get_request`. The cache persists across
925
+ The provider receives absolute local paths beside the owning messages in its
926
+ first prompt. The cache persists across
824
927
  turns; only House-authorized terminal Run cleanup removes its Run directory.
825
928
 
826
929
  The accepted attachment's retained original is separate from that cache. The Kit
@@ -845,8 +948,8 @@ outcome it handled, and exits non-zero while any of them is unsaved.
845
948
  One `kit worker run` then takes one House-authorized resumption, or otherwise
846
949
  acquires one waiting Run, and supervises exactly that WorkerSession. A resumed
847
950
  conversation loads its retained native provider session through ACP
848
- `session/load`, receives the current input from House through
849
- `session_get_request`, and executes one turn. Historical output replayed during
951
+ `session/load`, receives the current input House answers for the Run in its
952
+ first prompt, and executes one turn. Historical output replayed during
850
953
  loading is not emitted as new assistant output. Missing or rejected provider
851
954
  continuation never starts a replacement provider conversation in the same Run.
852
955
 
@@ -913,7 +1016,12 @@ content, no prompt, and no resolved secret, and Session authority is always
913
1016
  reissued by House rather than recovered from disk. It is evidence for
914
1017
  reconciliation, never permission to resume or recreate work, and nothing keeps
915
1018
  it once House has answered for the Run: a settled terminal state, a confirmed
916
- owner stop, and a retained Provider continuation each drop it.
1019
+ owner stop, and a retained Provider continuation each drop it. When the
1020
+ supervised process ends, the binding first records what the Kit observed: the
1021
+ sealed final text, whether the turn completed, the capacity or provider refusal,
1022
+ the execution failure and whether a Provider continuation is retained. The Kit
1023
+ then reports that stop, and a restarted Kit reports the same recorded stop
1024
+ instead of treating the session as lost.
917
1025
 
918
1026
  A restarted `kit worker run` reconciles the records it kept before it acquires
919
1027
  anything new. It reports every retained settlement note first, because those
@@ -1062,10 +1170,27 @@ retained references still waiting to be saved.
1062
1170
  Every command that changes or reads installation state takes this
1063
1171
  installation's exclusive Environment control lease for the length of its run
1064
1172
  and renews it before it expires, so two Kit commands never work against the
1065
- same installation at once. A command that ends, or is interrupted once its work
1066
- has stopped, releases that exact lease so the next command acquires at once; a
1067
- refused command leaves it to expire. A `kit login` credential is presented only to take
1068
- that lease.
1173
+ same installation at once. A command that ends, is refused, or is interrupted
1174
+ once its work has stopped releases that exact lease so the next command acquires
1175
+ at once. A `kit login` credential is presented only to take that lease.
1176
+
1177
+ While `kit resident` holds that lease, a command runs through it instead: the
1178
+ command presents no credential, the resident confirms its current lease with
1179
+ House and sends the command's House requests under it, an interrupted command
1180
+ cancels its request there, and the lease stays held when the command ends. The
1181
+ resident serves one such command at a time and refuses another with
1182
+ `environment_control_lease_active`, so two commands still never work at once.
1183
+ A resident whose lease House refuses answers the command with that refusal; the
1184
+ command never acquires a lease of its own while a resident serves it. While an
1185
+ update drains, `state/update-drain` refuses every such command, `kit login` and
1186
+ `kit worker run` with `kit_update_draining` before it enrolls, acquires or
1187
+ delegates anything. A command already served keeps the resident's lease until
1188
+ it ends, while the resident refuses its acquisition of new work and its
1189
+ Telegram updates with `kit_update_draining`, which ends a served
1190
+ `kit telegram serve` once each of its activities reports the deliveries and
1191
+ files it already acquired; until it ends, the drain answers that it waits for
1192
+ that command and the bootstrap leaves the container as it is. The bootstrap
1193
+ removes the file when it replaces the container.
1069
1194
 
1070
1195
  Interrupting the command cancels the running connector without advancing the
1071
1196
  checkpoint.
@@ -1104,15 +1229,132 @@ interrupted run is never resumed: the next run opens a new manifest and House
1104
1229
  answers with what is still needed. The command reports the Publication
1105
1230
  reference, the paths delivered, the items refused and the ending.
1106
1231
 
1232
+ ## Working copies
1233
+
1234
+ The Environment owner chooses in House which Rooms this Environment keeps
1235
+ locally, and whether it keeps `/private`. An Environment whose owner never chose
1236
+ copies nothing, and a Room joined later stays uncopied until the owner selects
1237
+ it. The resident Kit reads the saved choice through
1238
+ `POST /kit/working-copy/selection` when it starts, whenever its control stream
1239
+ connects or reconnects, and whenever House points it there after a save;
1240
+ `kit worker run` reads it once before its pass.
1241
+
1242
+ Each selected Room becomes one ordinary Git repository at
1243
+ `/agents/house/working-copies/rooms/<room_ref>`, and `/private` one at
1244
+ `/agents/house/working-copies/private`. The Room reference names the location,
1245
+ so a Room handle change moves nothing. Every Agent launched afterwards receives
1246
+ the House path and local repository of each active copy in its context, one
1247
+ line each, such as `/rooms/work: /agents/house/working-copies/rooms/r_1x.q`.
1248
+
1249
+ A Room repository holds the Room's complete readable content: `ROOM.md`,
1250
+ `library/`, `capture/` with its provenance sidecars, and `collaboration/`. The
1251
+ `/private` repository holds its Library, collaboration, each Room's private
1252
+ `AGENTS.md`, the `agents/` cards and `house/`. Attached settings and attachment
1253
+ bytes are not copied. The branch `main` starts at the commit House served, which
1254
+ tracks exactly the files House declares writable under the Environment's
1255
+ current grant. Every other file is read-only House content: the generated
1256
+ `.gitignore` names and explains it, so Git never tracks it, and the matching
1257
+ `.ignore` keeps it visible to ripgrep and other native search. Neither file
1258
+ grants anything, and House refuses a change to a read-only path however it is
1259
+ committed.
1260
+
1261
+ Local edits, staging and commits change nothing in House until a commit is
1262
+ pushed. `refs/house/received` records the House content last received; each
1263
+ commit Kit wrote there, and each commit it acknowledged for a push, is a House
1264
+ state. The House revision of every file content Kit received, keyed by path and
1265
+ Git blob, stays in Kit home under `state/working-copies/`, outside the
1266
+ repository. A local commit is a local tree, never a House revision.
1267
+
1268
+ `house git push [commit]`, run anywhere inside a copy or one of its linked
1269
+ worktrees, submits one commit, `HEAD` by default, resolved once. Kit compares
1270
+ that commit's tree with the newest House state in its history, so every linked
1271
+ worktree keeps its own base, and sends the difference as one ordinary House edit:
1272
+ a changed field, section or preamble against its own prior value, any other
1273
+ change as the whole file against its exact revision, a new file as a create and
1274
+ a committed deletion as a removal. Staged and unstaged work, and files absent
1275
+ only from the working tree, are never sent. Native `git push` reaches no House
1276
+ remote. Inside an Agent Session the push goes through that Session's bridge,
1277
+ under its own authority and provenance; outside one it needs
1278
+ `house git push [commit] --owner`, which the resident Kit carries out under its
1279
+ control lease when a resident runs, and which otherwise acquires and releases
1280
+ its own. A Session refuses `--owner`, and a missing bridge is never replaced by
1281
+ owner authority.
1282
+
1283
+ Inside an Agent Session `git` is Kit's shim in front of the installed Git, and
1284
+ every command runs exactly as that Git runs it. In a copy or one of its linked
1285
+ worktrees the shim compares that worktree's index before and after each
1286
+ command. When `git mv` succeeds, Git's own dry run of the same command names
1287
+ which path went where, and the shim hands each move to the resident Kit, which
1288
+ keeps every moved document's successive paths in Kit home. A staged move
1289
+ belongs to its worktree's index: a command there that takes its path out of the
1290
+ index or brings it in anew, such as `git reset`, `git restore --staged`,
1291
+ `git rm`, `git stash` or `git switch`, takes it back, and so does removing the
1292
+ worktree. A `git commit` whose commit carries the move ties it to that commit.
1293
+ A tied move counts for every pushed commit whose history holds one of its
1294
+ commits, whatever the index does meanwhile. `git commit --amend` and
1295
+ `git rebase`, Kit's own rebase onto House's result included, also tie it to
1296
+ their result when the rewritten branch still carries it, so aborting the rebase
1297
+ or resetting back to the original commit keeps it as well. A `git reset` that
1298
+ leaves every commit of the move behind without touching its latest path in the
1299
+ index returns it to that worktree's index, where the next commit ties it again.
1300
+ Any other rewrite, such as a cherry-pick, leaves the move tied to its earlier
1301
+ commits. A push sends House's rename of the same document from the path its
1302
+ House base holds to the latest of its counted paths the pushed commit holds,
1303
+ when the commit no longer has the first and the base did not have the second;
1304
+ a moved file whose content also changed follows as a
1305
+ whole-file replacement at its destination. House keeps the document's identity
1306
+ and rewrites the links and references that name it; Kit reads those rewrites
1307
+ through `POST /kit/door/enumerate` and records them in the acknowledgment.
1308
+ Anything else records no move and the push sends an ordinary removal and
1309
+ creation, which House refuses while another document still refers to the
1310
+ removed one: a failed `git mv`, one undone or never committed through the
1311
+ shim, and a move made with Git outside the shim such as in the owner's own
1312
+ shell. An accepted push drops the moves its commit's history holds.
1313
+
1314
+ House decides before any local Git step. A refusal changes nothing in House;
1315
+ after `edit_conflict` Kit records the current content House names for the
1316
+ refused paths in `refs/house/received`, reading by revision any content the
1317
+ refusal does not carry, so the agent merges or rebases onto it with native Git,
1318
+ commits, and pushes again. After acceptance Kit records House's result for the
1319
+ pushed paths, the contents an owner push receives or each accepted revision
1320
+ read by name, in a commit that covers the pushed one and in
1321
+ `refs/house/received`; a read-only path House regenerates, such as the Wiki
1322
+ index, stays out of Git. Kit then
1323
+ fast-forwards a clean branch still at the pushed commit, or rebases only its
1324
+ later commits. A result not yet readable, uncommitted tracked changes,
1325
+ a moved branch or a rebase conflict leave the branch as it was, with no stash or
1326
+ reset, and the push answers that House accepted it while local integration is
1327
+ pending; the next `house git push` finishes that integration without another
1328
+ House edit. A push whose answer was lost is recorded in Kit home before it is
1329
+ sent, and the next push by the same Session, or by the owner for an owner push,
1330
+ resends it under its original operation. Another caller cannot
1331
+ recover it: while the pushing Session lives it is refused as
1332
+ `kit_working_copy_push_pending`, and once that Session has ended Kit reads House
1333
+ again and drops it. When a retained push's operation has expired, Kit reads the
1334
+ Room again: if House holds the pushed content the push finishes as accepted,
1335
+ otherwise it is dropped and the next push submits it as a new operation.
1336
+
1337
+ Deselecting a Room, or losing read access to it, marks its copy inactive:
1338
+ launches omit it, a new push is refused as `kit_working_copy_inactive`, and
1339
+ nothing local is deleted; a push already sent still settles and integrates. Selecting it again reads the Room
1340
+ again, advances `refs/house/received`, and replaces only the read-only content
1341
+ Kit wrote, never the branch, the index or a tracked file. A Room the owner may
1342
+ read without sync is refused as `operation_denied` and stays uncopied, while its
1343
+ ordinary remote reads are unaffected. Kit names each refused copy on its
1344
+ standard error.
1345
+
1107
1346
  ## Explicit synchronization
1108
1347
 
1109
- Beside the reconciling Working Copy, Kit has one explicit synchronization mode.
1348
+ Beside [working copies](#working-copies), Kit has one explicit synchronization mode.
1110
1349
  It selects prefixes over the User root, materializes them into a named local
1111
1350
  directory, enumerates whole content and removals after the recorded positions,
1112
1351
  and submits a resolved exact-base change set the caller prepared. Kit does no
1113
1352
  local reconciliation. The mode shares no Working Copy manifest, never infers a
1114
- removal from a locally absent file, and writes authored mutations only for the
1115
- two Libraries and `ROOM.md`.
1353
+ removal from a locally absent file, and writes authored mutations only within
1354
+ House's declared write scopes: a selected Room's Library and, where granted,
1355
+ `ROOM.md`. `/private` selects the Private Room by its Room reference, records an independent Room
1356
+ position, and keeps its `agents/` cards and Room-specific private `AGENTS.md`
1357
+ beside `library/**`.
1116
1358
 
1117
1359
  ```sh
1118
1360
  kit sync bootstrap ./replica \
@@ -1137,8 +1379,7 @@ Library paths are House metadata, not authored Library documents. `pull` present
1137
1379
  each recorded position with its log epoch, applies each enumerated path's whole
1138
1380
  content or removal, and advances those positions. A position older than House's
1139
1381
  declared retention, or one of a log epoch before House's database was restored,
1140
- is reported as `position_expired` with rematerialization as the remedy; only
1141
- `rebootstrap` rebuilds the directory.
1382
+ is reported as `position_expired`; only `rebootstrap` rebuilds the directory.
1142
1383
 
1143
1384
  `submit` sends field, section, and preamble operations with that value's prior
1144
1385
  content as base, and whole-file, rename, and removal operations with the file
@@ -1179,7 +1420,7 @@ for a batch that reaches it.
1179
1420
 
1180
1421
  ## Attachments
1181
1422
 
1182
- Upload one local file as an attachment to a Room or to your private tree. Name
1423
+ Upload one local file as an attachment to a Room or to your Private Room. Name
1183
1424
  the place explicitly. Kit sends the descriptor and the selected bytes; it does
1184
1425
  not scan a directory, convert a URL, or attach sibling files, credentials, or
1185
1426
  absolute paths.
@@ -1386,9 +1627,12 @@ $HOUSE_KIT_HOME/
1386
1627
  state/repositories/<repository identity>.json
1387
1628
  state/sync/<replica identity>.json
1388
1629
  state/sync/<replica identity>.pending.json
1630
+ state/working-copies/rooms/<room ref>.json
1631
+ state/working-copies/private.json
1389
1632
  state/attachments/<upload identity>.json
1390
1633
  state/conversation-originals/<attachment identity>.json
1391
1634
  state/sessions/<tmux session>.json
1635
+ state/update-drain
1392
1636
  state/continuations/<run identity>.json
1393
1637
  state/<instance>/credentials/google.json
1394
1638
  state/<instance>/checkpoints/<service>.json
@@ -1583,7 +1827,7 @@ prepared model definitions survive a later content-mount refusal or unmount.
1583
1827
 
1584
1828
  The atomic text mount includes a report with pending transfer rows. Kit then
1585
1829
  uploads each downloaded file once as an attachment to the mount's Room or
1586
- private tree, independently retrieves and compares the saved bytes, and only
1830
+ Private Room, independently retrieves and compares the saved bytes, and only
1587
1831
  after that proof succeeds rewrites every link to it, in every referring
1588
1832
  document, to its attachment reference, such as `at_1x.q`. The reference is an
1589
1833
  identity House resolves, not an HTTP download URL. A link's query and fragment
@@ -8,12 +8,47 @@ WORKSPACE="$HOME/AgentsHouse"
8
8
  MANUAL=0
9
9
  WORKSPACE_SELECTED=0
10
10
  HOUSE_SELECTED=0
11
+ FORWARDING=0
12
+ FORWARDED=kit
13
+ FORWARD=()
11
14
 
12
15
  refuse() {
13
16
  printf 'kit_bootstrap_refused: %s\n' "$1" >&2
14
17
  exit 1
15
18
  }
16
19
 
20
+ bind_mount() {
21
+ local source=${1//\"/\"\"}
22
+ printf 'type=bind,"src=%s",dst=%s' "$source" "$2"
23
+ }
24
+
25
+ login_kit() {
26
+ if [[ " $* " == *' --manual '* ]]; then
27
+ "${DOCKER_RUN[@]:0:2}" -i "${DOCKER_RUN[@]:2}" login "$@"
28
+ return
29
+ fi
30
+ command -v xdg-open >/dev/null || refuse 'no host browser opener; rerun with --manual'
31
+ "${DOCKER_RUN[@]}" login "$@" --host-browser | while IFS= read -r line; do
32
+ if [[ "$line" == 'Open '* ]]; then
33
+ printf 'Opening House Login in the host browser.\n'
34
+ xdg-open "${line#Open }" >/dev/null 2>&1 || refuse 'host browser did not open; rerun with --manual'
35
+ else
36
+ printf '%s\n' "$line"
37
+ fi
38
+ done
39
+ }
40
+
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
43
+ }
44
+
45
+ if [[ "${1:-}" == kit || "${1:-}" == house ]]; then
46
+ FORWARDING=1
47
+ FORWARDED="$1"
48
+ FORWARD=("${@:2}")
49
+ set --
50
+ fi
51
+
17
52
  while (($#)); do
18
53
  case "$1" in
19
54
  --workspace|--house)
@@ -53,6 +88,10 @@ case "$DOCKER_PLATFORM" in
53
88
  *) refuse "unsupported Docker Engine platform $DOCKER_PLATFORM" ;;
54
89
  esac
55
90
  [[ "$DOCKER_ARCH" == "$ARCH" ]] || refuse "host linux/$ARCH does not match Docker Engine linux/$DOCKER_ARCH"
91
+ if ((FORWARDING)); then
92
+ docker container inspect "$NAME" >/dev/null 2>&1 || refuse 'House Kit is not installed; run this bootstrap without arguments first'
93
+ WORKSPACE=$(docker inspect --format '{{range .Mounts}}{{if eq .Destination "/agents/house"}}{{.Source}}{{end}}{{end}}' "$NAME")
94
+ fi
56
95
 
57
96
  umask 077
58
97
  KIT_HOME="$HOME/.house-kit"
@@ -65,6 +104,8 @@ for path in "$KIT_HOME" "$WORKSPACE"; do
65
104
  done
66
105
  KIT_HOME=$(cd "$KIT_HOME" && pwd -P)
67
106
  WORKSPACE=$(cd "$WORKSPACE" && pwd -P)
107
+ KIT_HOME_MOUNT=$(bind_mount "$KIT_HOME" /kit-home)
108
+ WORKSPACE_MOUNT=$(bind_mount "$WORKSPACE" /agents/house)
68
109
  if [[ -f "$KIT_HOME/credential" || -f "$KIT_HOME/environment" ]] && [[ ! -f "$KIT_HOME/house" ]]; then
69
110
  refuse 'stored House origin is missing for an enrolled Environment'
70
111
  fi
@@ -76,10 +117,11 @@ if [[ -f "$KIT_HOME/house" ]]; then
76
117
  HOUSE="$BOUND_HOUSE"
77
118
  fi
78
119
  [[ "$HOUSE" == https://* || "$HOUSE" == http://127.0.0.1:* || "$HOUSE" == http://localhost:* ]] || refuse 'House origin must use HTTPS or local loopback HTTP'
79
- DOCKER_RUN=(docker run --rm --network host --user "$(id -u):$(id -g)" --mount "type=bind,src=$KIT_HOME,dst=/kit-home" --mount "type=bind,src=$WORKSPACE,dst=/agents/house" --env HOME=/kit-home --env HOUSE_KIT_HOME=/kit-home --env HOUSE_KIT_CONTAINER=1 --env "HOUSE_KIT_HOSTNAME=$(hostname)" "$IMAGE")
120
+ DOCKER_RUN=(docker run --rm --network host --user "$(id -u):$(id -g)" --mount "$KIT_HOME_MOUNT" --mount "$WORKSPACE_MOUNT" --env HOME=/kit-home --env HOUSE_KIT_HOME=/kit-home --env HOUSE_KIT_CONTAINER=1 --env "HOUSE_KIT_HOSTNAME=$(hostname)" "$IMAGE")
80
121
 
81
122
  if docker container inspect "$NAME" >/dev/null 2>&1; then
82
- [[ "$(docker inspect --format '{{.Config.Image}}' "$NAME")" == "$IMAGE" ]] || refuse 'an installed Kit uses a different image; update it explicitly'
123
+ INSTALLED_IMAGE=$(docker inspect --format '{{.Config.Image}}' "$NAME")
124
+ ((FORWARDING == 0)) || [[ "$INSTALLED_IMAGE" == "$IMAGE" ]] || refuse 'an installed Kit uses a different image; rerun this bootstrap without arguments to update it'
83
125
  [[ "$(docker inspect --format '{{.HostConfig.RestartPolicy.Name}}' "$NAME")" == unless-stopped ]] || refuse 'the installed Kit has a different lifecycle'
84
126
  [[ "$(docker inspect --format '{{len .Mounts}}' "$NAME")" == 2 ]] || refuse 'the installed Kit has unexpected mounts'
85
127
  [[ "$(docker inspect --format '{{range .Mounts}}{{if eq .Destination "/kit-home"}}{{.Source}}{{end}}{{end}}' "$NAME")" == "$KIT_HOME" ]] || refuse 'the installed Kit uses another Kit home'
@@ -88,6 +130,44 @@ if docker container inspect "$NAME" >/dev/null 2>&1; then
88
130
  [[ "$(docker inspect --format '{{range .Mounts}}{{if eq .Destination "/agents/house"}}{{.Type}}/{{.RW}}{{end}}{{end}}' "$NAME")" == bind/true ]] || refuse 'the installed workspace is not a writable bind mount'
89
131
  [[ "$(docker inspect --format '{{index .Config.Labels "agentshouse.house"}}' "$NAME")" == "$HOUSE" ]] || refuse 'the installed Kit uses another House origin'
90
132
  [[ -f "$KIT_HOME/credential" && -f "$KIT_HOME/environment" && -f "$KIT_HOME/house" ]] || refuse 'the installed Kit has no enrolled authority'
133
+ if ((FORWARDING)); then
134
+ if [[ "$FORWARDED" == kit && "${FORWARD[0]:-}" == login ]]; then
135
+ login_kit "${FORWARD[@]:1}"
136
+ exit 0
137
+ fi
138
+ PLACED=()
139
+ ENTRY=()
140
+ if [[ "$FORWARDED" == house ]]; then
141
+ HERE=$(pwd -P)
142
+ [[ "$HERE" == "$WORKSPACE" || "$HERE" == "$WORKSPACE"/* ]] || refuse "the current directory is outside the workspace $WORKSPACE"
143
+ PLACED=(--workdir "/agents/house${HERE#"$WORKSPACE"}")
144
+ ENTRY=(--entrypoint house)
145
+ fi
146
+ TERMINAL=()
147
+ if [[ -t 0 && -t 1 ]]; then TERMINAL=(-t); fi
148
+ if [[ -t 0 && ! -t 1 ]]; then
149
+ ECHOED=$(stty -g)
150
+ trap 'stty "$ECHOED"' EXIT
151
+ stty -echo
152
+ fi
153
+ if [[ "$(docker inspect --format '{{.State.Running}}' "$NAME")" == true ]]; then
154
+ docker exec -i "${TERMINAL[@]}" "${PLACED[@]}" "$NAME" "$FORWARDED" "${FORWARD[@]}"
155
+ else
156
+ "${DOCKER_RUN[@]:0:2}" -i "${TERMINAL[@]}" "${PLACED[@]}" "${ENTRY[@]}" "${DOCKER_RUN[@]:2}" "${FORWARD[@]}"
157
+ fi
158
+ exit
159
+ fi
160
+ if [[ "$INSTALLED_IMAGE" != "$IMAGE" ]]; then
161
+ docker pull --quiet --platform "linux/$ARCH" "$IMAGE" >/dev/null
162
+ if [[ "$(docker inspect --format '{{.State.Running}}' "$NAME")" == true ]]; then
163
+ docker exec "$NAME" kit resident drain
164
+ fi
165
+ docker rm -f "$NAME" >/dev/null
166
+ rm -f "$KIT_HOME/state/update-drain"
167
+ start_resident
168
+ printf 'House Kit updated for Environment %s.\n' "$(cat "$KIT_HOME/environment")"
169
+ exit 0
170
+ fi
91
171
  "${DOCKER_RUN[@]}" credential check --house "$HOUSE"
92
172
  if [[ "$(docker inspect --format '{{.State.Running}}' "$NAME")" == true ]]; then
93
173
  printf 'House Kit is already running for Environment %s.\n' "$(cat "$KIT_HOME/environment")"
@@ -108,20 +188,12 @@ if [[ -f "$KIT_HOME/credential" ]]; then
108
188
  "${DOCKER_RUN[@]}" credential check --house "$HOUSE"
109
189
  else
110
190
  if ((MANUAL)); then
111
- "${DOCKER_RUN[@]:0:2}" -i "${DOCKER_RUN[@]:2}" login --house "$HOUSE" --manual
191
+ login_kit --house "$HOUSE" --manual
112
192
  else
113
- command -v xdg-open >/dev/null || refuse 'no host browser opener; rerun with --manual'
114
- "${DOCKER_RUN[@]}" login --house "$HOUSE" --host-browser | while IFS= read -r line; do
115
- if [[ "$line" == 'Open '* ]]; then
116
- printf 'Opening House Login in the host browser.\n'
117
- xdg-open "${line#Open }" >/dev/null 2>&1 || refuse 'host browser did not open; rerun with --manual'
118
- else
119
- printf '%s\n' "$line"
120
- fi
121
- done
193
+ login_kit --house "$HOUSE"
122
194
  fi
123
195
  fi
124
196
 
125
197
  "${DOCKER_RUN[@]}" apply --house "$HOUSE"
126
- docker run -d --name "$NAME" --restart unless-stopped --user "$(id -u):$(id -g)" --mount "type=bind,src=$KIT_HOME,dst=/kit-home" --mount "type=bind,src=$WORKSPACE,dst=/agents/house" --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
198
+ start_resident
127
199
  printf 'House Kit connected for Environment %s.\n' "$(cat "$KIT_HOME/environment")"
package/bin/kit.js CHANGED
@@ -9,7 +9,7 @@ import { setupEnvironment } from "../src/setup.js";
9
9
  import { signInConnector } from "../src/connector-login.js";
10
10
  import { attachSession } from "../src/attach.js";
11
11
  import { runResident, runWorkers } from "../src/worker.js";
12
- import { openSession, releaseControl } from "../src/house.js";
12
+ import { openSession, releaseControl, requestDrain } from "../src/house.js";
13
13
  import { GOOGLE_OAUTH_CLIENT } from "../src/instance/google.js";
14
14
  import { reportHost } from "../src/host.js";
15
15
  import { signInAgent, updateAgents } from "../src/agents.js";
@@ -140,7 +140,7 @@ async function runCredentialCheck(args, directory) {
140
140
  const house = await selectedHouse(values, directory);
141
141
  let session;
142
142
  try {
143
- session = await openSession({ house, directory });
143
+ session = await openSession({ house, directory, acquire: true });
144
144
  } catch (error) {
145
145
  if (error instanceof KitRefusal && error.code === "environment_control_lease_active") {
146
146
  write("Stored Kit authority is accepted; its Environment already has an active control lease.");
@@ -167,6 +167,23 @@ async function runResidentCommand(args, directory) {
167
167
  }
168
168
  }
169
169
 
170
+ async function runResidentDrain(args, directory) {
171
+ declared(args, [], 0, { exact: true });
172
+ const answer = await requestDrain(directory);
173
+ if (answer.ready === true) {
174
+ write("House Kit is ready for its update.");
175
+ return;
176
+ }
177
+ write(
178
+ answer.sessions.length > 0
179
+ ? `House Kit is draining for its update and waits for Agent Sessions ${answer.sessions.join(", ")}.`
180
+ : answer.serving === true
181
+ ? "House Kit is draining for its update and waits for the Kit command it serves."
182
+ : `House Kit is draining for its update; House refused its lease release (${answer.refused}).`,
183
+ );
184
+ process.exitCode = 1;
185
+ }
186
+
170
187
  async function runSetup(args, directory) {
171
188
  const { values } = declared(args, ["--house", "--config"], 0, { exact: true });
172
189
  const house = await selectedHouse(values, directory);
@@ -965,7 +982,10 @@ async function main() {
965
982
  const directory = process.env.HOUSE_KIT_HOME ?? join(homedir(), ".house-kit");
966
983
  if (command === "login") return runLogin(args, directory);
967
984
  if (command === "credential" && args[0] === "check") return runCredentialCheck(args.slice(1), directory);
968
- if (command === "resident") return runResidentCommand(args, directory);
985
+ if (command === "resident") {
986
+ if (args[0] === "drain") return runResidentDrain(args.slice(1), directory);
987
+ return runResidentCommand(args, directory);
988
+ }
969
989
  if (command === "setup") return runSetup(args, directory);
970
990
  if (command === "apply") return runApply(args, directory);
971
991
  if (command === "worker") {
package/bootstrap.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "version": "0.1.0-alpha.20",
4
+ "hosts": {
5
+ "linux": {
6
+ "command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.20/connect-linux.sh)",
7
+ "asset": {
8
+ "name": "connect-linux.sh",
9
+ "url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.20/connect-linux.sh"
10
+ },
11
+ "workspace": {
12
+ "argument": "--workspace",
13
+ "quoting": "posix"
14
+ }
15
+ }
16
+ }
17
+ }