@agentshouse/kit 0.0.1 → 0.1.0-alpha.10

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 (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1776 -2
  3. package/bin/connect-linux.sh +173 -0
  4. package/bin/kit.js +1023 -0
  5. package/bootstrap.json +17 -0
  6. package/package.json +37 -3
  7. package/src/acp.js +99 -0
  8. package/src/adapter-mcp.js +44 -0
  9. package/src/adapter.js +252 -0
  10. package/src/agent-logins.js +65 -0
  11. package/src/agents.js +504 -0
  12. package/src/apply.js +29 -0
  13. package/src/attach.js +52 -0
  14. package/src/binding.js +90 -0
  15. package/src/blocks.js +156 -0
  16. package/src/bridge.js +449 -0
  17. package/src/bytes.js +89 -0
  18. package/src/channel.js +37 -0
  19. package/src/command.js +109 -0
  20. package/src/configuration.js +465 -0
  21. package/src/connector-login.js +91 -0
  22. package/src/connector.js +107 -0
  23. package/src/continuation.js +61 -0
  24. package/src/contract.js +363 -0
  25. package/src/conversation-originals.js +143 -0
  26. package/src/custom/authorization.js +63 -0
  27. package/src/declaration.js +35 -0
  28. package/src/delivery.js +191 -0
  29. package/src/directory.js +102 -0
  30. package/src/draft.js +89 -0
  31. package/src/google/calendar.js +177 -0
  32. package/src/google/declared.js +146 -0
  33. package/src/google/drive.js +435 -0
  34. package/src/google/gateway.js +166 -0
  35. package/src/google/gmail.js +249 -0
  36. package/src/google/id.js +1 -0
  37. package/src/google/oauth.js +273 -0
  38. package/src/google/source.js +25 -0
  39. package/src/hook.js +376 -0
  40. package/src/host.js +121 -0
  41. package/src/house-main.js +79 -0
  42. package/src/house.js +1275 -0
  43. package/src/inbound.js +175 -0
  44. package/src/instance/google.js +1 -0
  45. package/src/kit-work-stream.js +84 -0
  46. package/src/launcher-main.js +19 -0
  47. package/src/launcher.js +276 -0
  48. package/src/local-files.js +113 -0
  49. package/src/login.js +274 -0
  50. package/src/mcp.js +71 -0
  51. package/src/migrator.js +248 -0
  52. package/src/mount.js +582 -0
  53. package/src/operation.js +20 -0
  54. package/src/original.js +631 -0
  55. package/src/peer.js +124 -0
  56. package/src/plan.js +57 -0
  57. package/src/question.js +316 -0
  58. package/src/refusal.js +79 -0
  59. package/src/repository.js +521 -0
  60. package/src/session-bin/house +2 -0
  61. package/src/settlement.js +57 -0
  62. package/src/setup.js +190 -0
  63. package/src/source-originals.js +116 -0
  64. package/src/source.js +290 -0
  65. package/src/starter-set/grilling/LICENSE +21 -0
  66. package/src/starter-set/grilling/SKILL.md +30 -0
  67. package/src/starter-set/grilling/agents/openai.yaml +3 -0
  68. package/src/starter-set.js +17 -0
  69. package/src/state.js +157 -0
  70. package/src/sync-library.js +293 -0
  71. package/src/sync-state.js +120 -0
  72. package/src/sync.js +482 -0
  73. package/src/telegram.js +1061 -0
  74. package/src/terminal.js +57 -0
  75. package/src/tmux.js +119 -0
  76. package/src/worker.js +928 -0
  77. package/src/workspace.js +500 -0
package/README.md CHANGED
@@ -1,3 +1,1777 @@
1
- # @agentshouse/kit
1
+ # House Kit
2
2
 
3
- Placeholder. Releases are published from https://github.com/agentshouse/kit.
3
+ House Kit is a public package repository. Product development is planned and
4
+ coordinated in the [Agents House repository](https://github.com/trof3039/agents.house);
5
+ this repository contains only the package source and its operator documentation.
6
+
7
+ ## Connect a Linux computer
8
+
9
+ On Linux `amd64` or `arm64` with a working Docker Engine, run the published,
10
+ versioned bootstrap once:
11
+
12
+ ```sh
13
+ bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.10/connect-linux.sh)
14
+ ```
15
+
16
+ The release asset pins one immutable multi-architecture image digest. It creates
17
+ owner-protected `~/.house-kit` and `~/AgentsHouse`, mounts those as the complete
18
+ Kit home and `/agents/house`, enrolls through House Login in the host browser,
19
+ and starts the container with Docker's `unless-stopped` lifecycle. Pass
20
+ `--workspace /absolute/path` for one other workspace root or `--manual` for
21
+ the existing URL and one-time-code login route. `--house <origin>` selects
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.
25
+
26
+ The versioned `bootstrap.json` is the machine-readable command source for this
27
+ release. It is exported from the npm package as `@agentshouse/kit/bootstrap`
28
+ (JSON import) and published byte-for-byte beside `connect-linux.sh` in the
29
+ GitHub release. Its schema version is `1`, its `version` is the Kit package
30
+ version, and `hosts` contains only bootstrap scripts published in that release.
31
+ The package exports only the `./bootstrap` subpath; `./package.json` and source
32
+ subpaths are not exported. For this release, only `hosts.linux` exists. In this
33
+ schema example, `<version>` stands for the package's version:
34
+
35
+ ```json
36
+ {
37
+ "schemaVersion": 1,
38
+ "version": "<version>",
39
+ "hosts": {
40
+ "linux": {
41
+ "command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v<version>/connect-linux.sh)",
42
+ "asset": {
43
+ "name": "connect-linux.sh",
44
+ "url": "https://github.com/agentshouse/kit/releases/download/v<version>/connect-linux.sh"
45
+ },
46
+ "workspace": { "argument": "--workspace", "quoting": "posix" }
47
+ }
48
+ }
49
+ }
50
+ ```
51
+
52
+ `command` is complete for the default workspace. To show a selected absolute
53
+ workspace, append the declared `workspace.argument` and the path as one POSIX
54
+ shell-quoted argument. For example, `/home/user/My Agents' House` becomes
55
+ `--workspace '/home/user/My Agents'\'' House'`. Quote the entire path before
56
+ adding it to a copyable shell command; never interpolate it as shell syntax.
57
+ The bootstrap validates an absolute workspace root and encodes it as one Docker
58
+ `--mount` CSV source field, including commas and double quotes in the name.
59
+ The package manifest contains no image digest: the release workflow
60
+ fills the immutable image digest into the published script after the package
61
+ has been built.
62
+
63
+ The bootstrap does not install native Kit on an ordinary computer. The native
64
+ package remains for managed Nodes and an explicitly selected eligible Kit host.
65
+
66
+ The same bootstrap is the host's entry point for the existing `kit` commands:
67
+ append `kit` and the command to the connect command above, as in
68
+ `bash <(curl -fsSL …/connect-linux.sh) kit apply` or
69
+ `bash <(curl -fsSL …/connect-linux.sh) kit telegram connect`. It forwards the
70
+ command and its arguments unchanged into the installed container through the
71
+ host's Docker client and ends with the command's own output and exit status.
72
+ Standard input stays attached and a terminal stays a terminal, so a hidden
73
+ prompt such as the Telegram bot token is read inside the container and never
74
+ becomes an argument or printed output. With the output redirected, the
75
+ command's output and errors stay separate and the terminal stops echoing typed
76
+ input until the command ends. A running container runs the command beside its
77
+ resident Kit; a stopped one runs it in a one-off container over the same Kit
78
+ home and workspace and stays stopped. `kit login` takes the bootstrap's own
79
+ login route, a one-off container on the host network so the host browser can
80
+ return to its loopback listener, or `--manual` for the URL and one-time code.
81
+ Forwarding installs no host-native Kit and gives the container no Docker socket
82
+ or further mount; it refuses when no Kit is installed or the installed one
83
+ differs from this bootstrap's.
84
+
85
+ ## Building the package
86
+
87
+ The repository checkout is the development source. Build the package
88
+ that can be published by supplying House's dedicated installed-application
89
+ Google OAuth client, its id and its client secret:
90
+
91
+ ```sh
92
+ HOUSE_KIT_GOOGLE_OAUTH_CLIENT_ID=kit-client.apps.googleusercontent.com \
93
+ HOUSE_KIT_GOOGLE_OAUTH_CLIENT_SECRET=GOCSPX-kit-installed-app \
94
+ npm run build
95
+ ```
96
+
97
+ The command writes the publishable package to `dist/package`. Both values are
98
+ public instance content fixed into that package at build time: Google issues an
99
+ installed application's client secret without treating it as confidential, and
100
+ still requires it in every token request. The installed Kit does not read either
101
+ from its runtime environment and never requests them from House.
102
+
103
+ Runtime dependencies stay minimal and exactly pinned in `package.json` and
104
+ `package-lock.json`: House's own `@agentshouse/mdmodel`, `yaml`, which reads
105
+ House's text answers, and the Agent Client Protocol SDK
106
+ `@agentclientprotocol/sdk` with its peer `zod`, which is this Kit's one protocol
107
+ client for the compiled adapter contracts, and `ws` for the authenticated Kit
108
+ control stream. The SDK's approval
109
+ receipt is [`docs/agents/acp-sdk-dependency-approval.json`](docs/agents/acp-sdk-dependency-approval.json),
110
+ which the User wrote once this adoption's proof had passed. The adapter
111
+ packages themselves are Environment prerequisites, not Kit dependencies, and
112
+ carry no record.
113
+
114
+ The bootstrap invokes bare `kit login`. The first login names its new
115
+ Environment after the host computer and stores its Environment id in Kit home;
116
+ an explicit later login reconnects that retained id. `--environment <uuid>`
117
+ selects a different retained Environment for explicit recovery. A malformed
118
+ id or an answer naming a different Environment is refused before installation.
119
+
120
+ Use `--manual` when the browser cannot reach the Kit's loopback listener. The
121
+ command stores the issued Kit User credential in `$HOUSE_KIT_HOME/credential`
122
+ or `~/.house-kit/credential` with owner-only permissions, replacing the previous
123
+ file in one rename and leaving configuration, checkpoints, source state and
124
+ Agent relationships untouched. Reconnecting installs authority only: it starts
125
+ no Run and resumes no work, and the next `kit worker run` reconciles this
126
+ installation's bound sessions under its ordinary control rules.
127
+
128
+ If House never answers the exchange, the command refuses with
129
+ `kit_credential_not_installed` and no credential is written; the remedy is a
130
+ fresh owner-authorized `kit login` for the same Environment. The credential is
131
+ never printed by Kit.
132
+
133
+ ## Configuration
134
+
135
+ `$HOUSE_KIT_HOME/kit.json` is the one declarative configuration file. Use
136
+ `--config <path>` to name it elsewhere.
137
+
138
+ ```json
139
+ {
140
+ "availability": "continuous",
141
+ "execution_authority": "allow",
142
+ "migrators": [],
143
+ "instances": [
144
+ {
145
+ "id": "acme",
146
+ "preset": "custom",
147
+ "services": [
148
+ {
149
+ "service": "notes",
150
+ "source_ref": "src_acme_notes",
151
+ "rooms": ["work-room"],
152
+ "backfill": { "days": 30 },
153
+ "connector": {
154
+ "command": ["/usr/local/bin/acme-export", "--ndjson"],
155
+ "timeout_ms": 120000,
156
+ "environment": { "ACME_REGION": "eu" },
157
+ "secrets": {
158
+ "ACME_TOKEN": { "environment": "ACME_TOKEN_SOURCE" },
159
+ "ACME_KEY": { "file": "/run/secrets/acme-key" }
160
+ }
161
+ }
162
+ }
163
+ ]
164
+ }
165
+ ],
166
+ "clients": [
167
+ {
168
+ "kind": "claude-code",
169
+ "settings": "/home/ada/.claude/settings.json"
170
+ }
171
+ ],
172
+ "host": null
173
+ }
174
+ ```
175
+
176
+ This file is the whole desired state, so every section it declares is
177
+ reconciled and every section it omits is refused rather than assumed. The
178
+ agent CLI list is not local configuration: House owns it for this Environment,
179
+ and every controlled pass reads, installs, updates, signs in and reports exactly
180
+ the kinds House names. House also owns every Agent, its Profile and its route,
181
+ so this file declares neither Agents nor agent CLIs. The Kit runs whatever Agent
182
+ House assigns on one of House's selected kinds and refuses any other kind. The required
183
+ top-level `execution_authority` is this Environment's one answer to every agent
184
+ permission request, `"allow"` or `"refuse"`; the Agents sharing the Environment
185
+ share it. The
186
+ required top-level `availability` declares this Environment's availability
187
+ mode: `"intermittent"` for a laptop or other normally offline placement,
188
+ `"continuous"` for a server expected to stay online. Omitting it or declaring
189
+ any other value refuses as `kit_configuration_invalid`; `kit apply` sends the
190
+ declared mode with the rest of the desired state. An
191
+ instance declares one or more services, and under the `custom` preset each
192
+ service owns the command that produces it.
193
+
194
+ `host` declares the Linux host this installation measures for
195
+ [host capacity reporting](#host-capacity). `null` declares that this Kit reports
196
+ no capacity; the section is required like the others, so a host is measured only
197
+ where the Environment owner says so.
198
+
199
+ `clients` is the local Agent client catalogue and never leaves the installation.
200
+ Each entry names one client whose lifecycle hook configuration this Kit
201
+ reconciles: `kind` is `claude-code`, the one client this Kit installs a hook
202
+ into, and `settings` is the absolute path of that client's own settings
203
+ document. The section is required like the others and declares `[]` when this
204
+ Environment runs no such client. An unknown `kind`, a relative path, and two
205
+ entries naming the same document each refuse as `kit_configuration_invalid`.
206
+
207
+ A secret is never written into this file. `secrets` names either an
208
+ environment variable the Kit reads at run time or a mounted file it reads from
209
+ disk; the resolved value reaches the connector's environment and nothing else.
210
+
211
+ `backfill` is chosen once and honored on the first capture: `{ "days": 30 }`
212
+ never delivers anything observed earlier, `{ "count": 500 }` bounds the first
213
+ capture to 500 items and then continues from its cursor unbounded, and
214
+ `{ "all": true }` takes every history the connector offers. The boundary is
215
+ fixed when the service first captures, not each time a capture is attempted, so
216
+ a run that fails and is repeated days later still starts where it would have.
217
+
218
+ `timeout_ms` may shorten the connector's 120000 ms bound but never raise it. The
219
+ Kit holds this installation's Environment control lease across the connector run
220
+ and the batch it produces, and the lease has to outlive both.
221
+
222
+ Apply the declared source state to House:
223
+
224
+ ```sh
225
+ kit apply --house https://agents.house
226
+ ```
227
+
228
+ Only the state House declares - instances, services, `source_ref`, and Room
229
+ routes - leaves the installation. The connector, its secret references, and the
230
+ backfill boundary stay local.
231
+
232
+ ## The SessionStart hook
233
+
234
+ House answers `kit apply` with its current SessionStart hook: one
235
+ `{id, version, event, instruction}` document whose `instruction` tells the Agent
236
+ which Pulse and continuation reads to perform itself. The document is House's
237
+ own text under a stable `id`, versioned with the Kit contract version, and it
238
+ carries no Room content, open item, count, checkpoint, or identity.
239
+
240
+ `kit apply` writes that instruction to
241
+ `$HOUSE_KIT_HOME/state/hooks/<hook id>.txt` and installs one `SessionStart`
242
+ command hook printing it in every declared client's settings document. The
243
+ installed hook is that read and nothing else: it calls no House operation, marks
244
+ nothing seen, embeds no Room material, and infers no chat Session, so the Agent
245
+ decides what to read from the guidance it receives.
246
+
247
+ `state/hooks.json` records exactly which settings documents this installation
248
+ has written and the exact command it installed in each, so ownership is that
249
+ record and the command it names rather than any resemblance to one. An entry
250
+ whose command merely mentions the instruction is another owner's and is left
251
+ alone. `clients` is the desired state of that record: applying the same served
252
+ document again leaves each settings document byte-identical, dropping a client
253
+ from the catalogue removes this installation's entry from the document it named,
254
+ and an empty catalogue removes every entry, the record, and `state/hooks/`
255
+ without touching a hook, group, or setting the installation does not own.
256
+
257
+ One apply reaches every client or restores the ones it already wrote. Every
258
+ declared and previously recorded document is canonicalized, checked, and parsed
259
+ before House is contacted at all - including the `hooks` object and the
260
+ `SessionStart` array this Kit edits, so a document shaped differently refuses as
261
+ `kit_client_settings_unreadable` without changing anything remotely. The counters
262
+ House answers with are validated before the first local change. Each write then
263
+ re-resolves the whole declared path and re-checks the document it prepared,
264
+ refusing as `kit_client_settings_refused` when any part of the path, or the
265
+ document itself, was replaced meanwhile - a parent directory turned into a link
266
+ after preparation receives no write at all. A failed write or a cancellation
267
+ part-way restores every document already written to its previous bytes, removes
268
+ one this installation created, and names as `kit_client_settings_unrestored` any
269
+ it could not restore.
270
+ The previous instruction stays readable until the last document is written and
271
+ only then is it pruned, so a client left holding an older command still reads an
272
+ instruction that is there.
273
+
274
+ A path is canonicalized through `..` and symlinks before it is used: the Kit
275
+ writes through a symlinked settings document instead of replacing the link, and
276
+ refuses as `kit_client_settings_refused` a path that is not a regular document,
277
+ that two entries share, that resolves into this installation's own directory,
278
+ or - for a document already in the record - that no longer resolves to itself,
279
+ rather than writing through a link that replaced it. An existing document keeps
280
+ its file mode, one the client does not have yet is created owner-only, and one
281
+ the Kit cannot parse refuses as `kit_client_settings_unreadable` rather than
282
+ being replaced. Preparation reads before the Environment control lease is taken;
283
+ every write happens under that renewed lease with the apply it belongs to.
284
+
285
+ Continuity does not depend on the hook. An Agent working through any client
286
+ receives the same guidance in the server instructions and the tool descriptions,
287
+ and an installation whose catalogue is empty neither installs a hook nor needs
288
+ one served. A declared client is the opposite: an absent or malformed document
289
+ refuses as `kit_hook_rejected` instead of leaving whatever was installed before.
290
+
291
+ ## The Google preset
292
+
293
+ The published Kit is built with one House-owned Google installed-application
294
+ OAuth client. Its id and client secret are public build-time instance content,
295
+ not House secrets and not values served by House at run time. Google refresh
296
+ and access tokens remain in the installation's local credential store.
297
+
298
+ Each Google instance represents one Google account, and one Kit installation
299
+ may declare as many as the User has. Each instance owns its own local Google
300
+ authorization, selectors, Source references, Room routes, checkpoints and
301
+ health under `state/<instance>/`; the installation's one `kit login` covers
302
+ them all, and no instance can read another's provider credential.
303
+
304
+ ```json
305
+ {
306
+ "availability": "intermittent",
307
+ "execution_authority": "refuse",
308
+ "migrators": [],
309
+ "instances": [
310
+ {
311
+ "id": "google-account",
312
+ "preset": "google",
313
+ "scopes": ["https://www.googleapis.com/auth/contacts.readonly"],
314
+ "services": [
315
+ {
316
+ "service": "gmail",
317
+ "source_ref": "src_google_mail",
318
+ "rooms": ["work-room"],
319
+ "backfill": { "days": 30 },
320
+ "selection": { "labels": ["INBOX"] }
321
+ },
322
+ {
323
+ "service": "calendar",
324
+ "source_ref": "src_google_calendar",
325
+ "rooms": ["work-room"],
326
+ "backfill": { "all": true },
327
+ "selection": { "calendars": ["primary"] }
328
+ },
329
+ {
330
+ "service": "drive",
331
+ "source_ref": "src_google_drive",
332
+ "rooms": ["work-room"],
333
+ "backfill": { "count": 500 },
334
+ "selection": { "files": ["file-id"], "folders": ["folder-id"] }
335
+ }
336
+ ]
337
+ }
338
+ ],
339
+ "clients": [],
340
+ "host": null
341
+ }
342
+ ```
343
+
344
+ Google authorization is incremental. Enabling Gmail requests only Gmail's
345
+ read-only scope; Calendar and Drive request their own read-only scopes only
346
+ when enabled. Each service scope set has its own local provider grant, so a
347
+ later authorization never widens or replaces a sibling service's grant.
348
+ Additional Google API scopes are declared once on the preset instance and
349
+ remain local; a declared service names only scopes that instance already
350
+ holds. Drive requires at least one explicit file or folder selection and never
351
+ defaults to the whole account.
352
+
353
+ A Google API outside the three curated services may use the closed declared
354
+ service schema. The declaration is data only: no script or executable transform
355
+ is loaded. Its host must be a Google API domain and it runs under scopes the
356
+ instance already holds.
357
+
358
+ ```json
359
+ {
360
+ "service": "people",
361
+ "source_ref": "src_google_people",
362
+ "rooms": ["work-room"],
363
+ "backfill": { "all": true },
364
+ "declaration": {
365
+ "host": "people.googleapis.com",
366
+ "path": "/v1/people/me/connections",
367
+ "query": { "personFields": "names,emailAddresses,metadata" },
368
+ "page_size": "pageSize",
369
+ "scopes": ["https://www.googleapis.com/auth/contacts.readonly"],
370
+ "page_token": {
371
+ "request": "pageToken",
372
+ "response": "nextPageToken",
373
+ "checkpoint_request": "syncToken",
374
+ "checkpoint_response": "nextSyncToken"
375
+ },
376
+ "items": "connections",
377
+ "fields": {
378
+ "native_id": "resourceName",
379
+ "native_version": "etag",
380
+ "provider_modified_at": "metadata.sources.0.updateTime",
381
+ "observed_at": "metadata.sources.0.updateTime",
382
+ "body": ["names.0.displayName", "emailAddresses.0.value"],
383
+ "container": "metadata.sources.0.type"
384
+ }
385
+ }
386
+ }
387
+ ```
388
+
389
+ A Google instance signs in only through [Environment setup](#environment-setup),
390
+ on House's owner-authenticated page, so the owner's browser never has to reach
391
+ this Environment. For each enabled service whose scopes the instance does not
392
+ hold yet, the page links to Google's consent for exactly that service's scopes.
393
+ Google then sends the browser to a `http://127.0.0.1:<port>/` address that
394
+ nothing answers: the owner copies that whole address from the address bar and
395
+ pastes it into the page. The Kit accepts it only when it carries the state its
396
+ own consent issued, exchanges the code with its PKCE verifier and the built
397
+ client, and stores the grant locally. An address that answers another consent,
398
+ or a code Google refuses to exchange, fails the login and stores nothing; a
399
+ declined consent cancels it. Grants already completed for earlier services stay.
400
+
401
+ `kit source sign-in <instance>` signs in a custom connector instance through
402
+ its own command, described below. It skips an instance whose local
403
+ authorization is already valid and answers `ready`, `skipped`, or
404
+ `needs_attention`. A Google instance refuses it as `kit_connector_login_guided`
405
+ and names House setup.
406
+
407
+ ## Environment setup
408
+
409
+ House requests one setup when it creates a managed Environment and again when
410
+ its owner asks it to renew what the Environment holds. The Kit acts on the
411
+ request it acquires, never on its own:
412
+
413
+ ```sh
414
+ kit setup --house https://agents.house
415
+ ```
416
+
417
+ `kit worker run` performs the same step at the start of a pass, before it
418
+ hands out any work, so a managed Environment needs no terminal. The walk first
419
+ prepares the absolute `/agents/house` base, then visits every declared agent
420
+ CLI and then every connector instance, in configuration order:
421
+
422
+ - an agent CLI is installed or kept at its served release as
423
+ [the declared agent CLIs](#the-declared-agent-clis) describe; a kind no
424
+ release could be installed for needs attention as `install_failed`;
425
+ - a module whose own status check already answers signed in reports
426
+ `authenticated` to `/kit/logins/report`, which closes any renewal episode an
427
+ earlier attempt left open, and is `skipped`;
428
+ - otherwise the Kit runs the module's guided login. It reports the login to
429
+ House as `renewal_required` for `/kit/logins/report`, which opens or returns
430
+ one renewal episode, and presents the provider's steps on House's
431
+ owner-authenticated page by holding `/kit/secret-input/<episode>` until the
432
+ owner submits. Collected values go only to the waiting sign-in process; the
433
+ Kit sends none of them, and none of the sign-in's output, to any other act. A
434
+ sign-in whose status check then answers signed in reports `authenticated`
435
+ and is `ready`;
436
+ - a module without a guided route in this Kit, or one House does not advertise
437
+ (`kit_login_renewal_unsupported`), needs attention as `login_unavailable`
438
+ and opens no ceremony; a sign-in that exits without authenticating needs
439
+ attention as `login_failed`, and one that outlives its ten-minute bound as
440
+ `login_cancelled`. Either leaves its renewal episode open.
441
+
442
+ One module's failure never stops the walk. The Kit reports what it now holds
443
+ for every declared agent CLI and then the whole summary, one entry per module
444
+ with its `category`, `name`, `status` and `reason`, to `/kit/setup/report`
445
+ under the request it served, and releases its control lease. A cancelled
446
+ command reports no partial summary and releases its lease once the walk has
447
+ stopped.
448
+
449
+ Each compiled adapter contract carries the guided route its provider's own
450
+ headless sign-in offers, read from that command's output:
451
+
452
+ - `codex-acp` runs Codex's device sign-in. The page links to the provider's
453
+ device page and shows the one-time code the command printed; the owner signs
454
+ in with ChatGPT there and enters the code, and the command completes on its
455
+ own. Nothing is written back to it.
456
+ - `claude-agent-acp` runs Claude Code's sign-in. The page links to the authorize
457
+ page the command printed and collects the authentication code Claude shows
458
+ afterwards, which the Kit writes to that command alone. Claude Code answers a
459
+ code it refuses by asking for another, so the Kit ends that sign-in as soon as
460
+ the command prints `Invalid code.` and it needs attention as `login_failed`
461
+ rather than waiting out its ten-minute bound.
462
+ - `grok-build` runs Grok Build's device sign-in. The page links to the Grok
463
+ device URL and shows the anti-phishing one-time code printed by the command;
464
+ authorization completes in the browser and no secret is written back to the
465
+ process.
466
+
467
+ House offers only a route its provider evidence qualifies; one it does not
468
+ advertise stays `login_unavailable`.
469
+
470
+ ## The custom connector
471
+
472
+ Each custom service names one User-owned command. The Kit invokes it through an
473
+ argument vector, never a shell, with an environment holding only `PATH`, the
474
+ declared `environment` entries, and the resolved secrets. No House credential
475
+ and no Kit User credential is ever placed there. The request carries no service
476
+ name because the command is the service.
477
+
478
+ Stdin carries one JSON request line:
479
+
480
+ ```json
481
+ { "contract": "2026-08-22", "instance": "acme", "cursor": null, "limit": 100 }
482
+ ```
483
+
484
+ `cursor` is the opaque cursor the connector last returned, or `null` on the
485
+ first capture and after a reset. Stdout carries NDJSON: zero or more `item`
486
+ lines and at most one terminal `cursor` line.
487
+
488
+ ```
489
+ {"type":"item","namespace":"acme/notes","native_object_id":"note-91","native_version":"7","observed_at":"2026-08-20T09:15:00.000Z","body":"the note","author":"ada","url":"https://acme.example/notes/91","attachments":[{"url":"https://acme.example/files/1","name":"plan.pdf"}]}
490
+ {"type":"cursor","cursor":"page-2"}
491
+ ```
492
+
493
+ `namespace`, `native_object_id`, `observed_at`, and `body` are required;
494
+ `native_version`, `author`, `url`, and `attachments` are optional. There is no
495
+ metadata bag, no executable field, and no embedded credential. A malformed line
496
+ anywhere rejects the whole run: nothing is delivered and no checkpoint moves. A
497
+ non-zero exit status, a timeout, or output beyond the bound rejects the run the
498
+ same way.
499
+
500
+ An attachment names exactly one target. `url` is a verbatim reference House never
501
+ fetches. `file` names one work file the connector already acquired into this
502
+ Environment and declares its exact version:
503
+
504
+ ```
505
+ {"file":{"path":"/srv/acme/files/plan.pdf","bytes":48213,"sha256":"<hex>","media_type":"application/pdf"},"name":"plan.pdf"}
506
+ ```
507
+
508
+ `path` is absolute and never leaves the installation; `bytes` and `sha256`
509
+ describe the accepted version even when the connector could not download it,
510
+ and `media_type` is optional. The Kit publishes that descriptor as a retained
511
+ original on the Capture item, under an accepted version derived from the
512
+ delivery key, the attachment position and the descriptor, so reoffering the same
513
+ item always names the same version and a different descriptor under the same
514
+ delivery key is House's delivery-key conflict.
515
+
516
+ The Kit derives each item's delivery key from the instance, the service, the
517
+ declared namespace, the native object id, and the native version when present.
518
+ An object whose content changes without its native version changing keeps its
519
+ key and is refused by House as a delivery-key conflict rather than silently
520
+ overwriting the earlier Record.
521
+
522
+ The same command owns its module authorization. `kit source sign-in` and the
523
+ setup walk's status check send one JSON line
524
+ with `action: "authorization"` and `mode: "status"` or `mode: "login"`; the
525
+ command returns exactly one JSON object such as `{ "authorized": true }`.
526
+
527
+ ## The Worker runtime
528
+
529
+ ```sh
530
+ kit worker run --house https://agents.house
531
+ ```
532
+
533
+ Worker-runtime readiness validates the tmux this Kit declares support for,
534
+ reopens any already-bound live WorkerSession, brings every declared agent CLI
535
+ to its current release, starts each installed declared kind's command adapter
536
+ long enough to verify its compiled contract and read the options its own
537
+ session offers, reports what it holds to House, and only then acquires another
538
+ WorkerRun. This Kit compiles three MVP adapter kinds: `codex-acp` contract `2` selects
539
+ `@agentclientprotocol/codex-acp` `1.12.0` or a later `1.x` release, and
540
+ `claude-agent-acp` contract `1` selects
541
+ `@agentclientprotocol/claude-agent-acp` `0.79.0` or a later `0.x` release, while
542
+ `grok-build` contract `1` selects `@xai-official/grok` `1.0.41` or a later
543
+ `1.x` release and launches its native supervised ACP stdio mode. Each
544
+ contract names the adapter package, the minimum release the `initialize` answer
545
+ must reach, the executable it installs, the install recipe's own settings, the
546
+ CLI's headless sign-in command and the command that answers whether it is
547
+ signed in, the Agent Client Protocol version it speaks, its capability
548
+ descriptor - the admitted `elicitation/create` field kinds, whether the adapter
549
+ admits a declined form, and the stop-reason table of that adapter - whether the
550
+ adapter loads a session, and the identifiers of the session configuration
551
+ options the route's model and effort reach, or `null` for a field whose
552
+ adapter offers no such option. An acquired route carrying any other descriptor for
553
+ that kind refuses as `kit_adapter_contract_mismatch` before any spawn
554
+ commitment.
555
+ The `codex-acp` and `grok-build` contracts pin session loading and the
556
+ `claude-agent-acp` contract does not. The same readiness asserts that the initialize exchange Kit
557
+ wrote carried its elicitation declaration, because without it Claude Code
558
+ disables its question tool and Codex answers nothing.
559
+ An adapter whose running release is older than the contract's minimum, of
560
+ another major, mismatched, or unresponsive refuses `kit worker run` within 10
561
+ seconds per installed kind and names what it found, after the pass has reported
562
+ what it holds, and every other Kit command keeps working.
563
+
564
+ Once `initialize` passes, the Kit opens one Agent Client Protocol session with
565
+ `session/new` in its own home directory and no MCP server, reads that session's
566
+ configuration options, and stops the adapter; no prompt is sent. For each field
567
+ the contract names, it takes the `select` option carrying that identifier and
568
+ reports every value it offers with the display name the adapter gave it,
569
+ flattening option groups and keeping nothing else. A field whose identifier the
570
+ contract declares `null`, or whose option the session does not expose as a
571
+ `select`, reports none. An adapter that refuses the session with the Agent
572
+ Client Protocol's `auth_required` error, as `codex-acp` does before it is signed
573
+ in, reports no options at all and its kind stays served. A session that fails
574
+ for any other reason, does not open within 30 seconds, answers without a session
575
+ id, or offers options the Kit cannot read as values and names reports no options
576
+ either and refuses `kit worker run` as `kit_adapter_contract_mismatch`, like any
577
+ other contract failure. The Kit keeps no catalogue of these values and no copy
578
+ of them between passes: every report reads them from the adapter again.
579
+
580
+ ## Agent working directories
581
+
582
+ Connecting an installation creates `/agents/house`, the absolute base every
583
+ default Agent working directory lives under. Connecting again keeps whatever is
584
+ already under it. An installation that cannot create that directory refuses
585
+ with `kit_launch_base_unprepared` and names it, rather than substituting a
586
+ home-relative location: the Environment owner creates it once and gives the
587
+ installation write access to it.
588
+
589
+ Every acquired WorkerRun carries its Agent's absolute working directory and its
590
+ Agent id in its execution plan, and Kit launches the supervised session and its
591
+ adapter in exactly that directory. Kit recognises the Agent's own default
592
+ directory by exact equality with `/agents/house/<agent id>`, never by a
593
+ directory's mere position beneath the base: a sibling directory one level
594
+ inside `/agents/house` is somebody else's default or an owner's own workspace,
595
+ never this Agent's. Every path that is not this exact equality, one deeper
596
+ inside `/agents/house` included, must
597
+ already exist in this Environment as a directory the installation can enter;
598
+ Kit never creates or seeds it.
599
+
600
+ When the Agent's own default directory is absent, Kit builds the starter set in
601
+ a temporary directory beneath the base and renames it into place as one atomic
602
+ step, so a partly seeded directory is never visible at the default path. Two
603
+ concurrent first launches race that rename; the loser discards its temporary
604
+ directory and uses the directory the winner put there, so the pair still leaves
605
+ one complete directory and nothing partial. A present default directory -
606
+ including one whose starter files the owner edited or removed - launches
607
+ exactly as it is, and a deleted default directory is recreated with a fresh
608
+ starter set on its next launch.
609
+
610
+ The starter set is `AGENTS.md` with the single line ``The agents.house `house`
611
+ CLI is available: run `house --help`.``, `CLAUDE.md` as a relative symbolic link to it, `.agents/skills/grilling/`
612
+ vendored from [`mattpocock/skills`](https://github.com/mattpocock/skills)
613
+ `skills/productivity/grilling` at a pinned commit with its MIT license and a
614
+ provenance header naming that commit, and `.claude/skills` as a relative
615
+ symbolic link to `.agents/skills`. Once seeded the files belong to the owner:
616
+ Kit never syncs, refreshes, or restores them, and refreshing the vendored skill
617
+ for new Agents is an ordinary Kit release that never touches an already seeded
618
+ directory.
619
+
620
+ A relative, missing, inaccessible, or non-directory working directory refuses
621
+ with `kit_launch_directory_unavailable` before any provider process starts. Kit falls back to no other location - not the
622
+ base, not `$HOUSE_KIT_HOME`, and not its own current directory - and neither
623
+ connecting an installation nor launching a session moves, copies, overwrites,
624
+ or removes anything a working directory holds.
625
+
626
+ ## The declared agent CLIs
627
+
628
+ The Environment owner installs no adapter binary by hand. `kit agents update`
629
+ and every `kit worker run` bring each declared kind to the latest release its
630
+ contract serves - no older than the contract's minimum and inside that
631
+ minimum's major - under
632
+ `$HOUSE_KIT_HOME/agents/<kind>/releases/<release>/`, never touching what the
633
+ owner installed globally, and an acquired route's command is that installed
634
+ executable rather than a path anyone writes into `kit.json`.
635
+ The resident Kit also listens for an `agents` availability pointer on its
636
+ control stream. It reads the whole wanted list from House over HTTPS, removes
637
+ Kit-owned CLI directories that are no longer wanted, installs newly wanted
638
+ kinds, and reports its held state immediately, including while another Agent
639
+ run remains active. A newly wanted kind that needs provider authorization uses
640
+ the existing guided House login ceremony; Kit reports readiness again when it
641
+ ends. A reconnect reads the list again, so a pointer missed during a stream
642
+ outage changes no authority.
643
+ Removal waits for an active worker pass or any unresolved live WorkerSession
644
+ binding to stop using a CLI directory; the Kit keeps reporting that kind until
645
+ it removes the directory.
646
+
647
+ Every candidate release is proved before it serves: the Kit installs it beside
648
+ the current release, runs the contract's own conformance check - the protocol's
649
+ `initialize` against the minimum release, the protocol version and the
650
+ capability descriptor the contract requires, and no provider-billed turn - and
651
+ switches to it only on green. On red it keeps the release it already has,
652
+ removes the candidate, and holds that kind with the refused release named in
653
+ its report until a newer candidate appears. Non-major releases advance this way
654
+ on their own; a major release waits for a contract revision. The candidate
655
+ check runs at most every six hours per kind, and `kit agents update` runs it at
656
+ once. A kind whose release lookup or installation fails keeps the release it
657
+ already has, or none, and is named with its failure; the other kinds go on,
658
+ `kit agents update` then exits non-zero, and a worker pass verifies and serves only the kinds with an installed release, so
659
+ one CLI the Kit could not install leaves only that kind's work unavailable.
660
+
661
+ The install recipe carries what the CLI needs beside its binary: for
662
+ `codex-acp` the Kit enables user-input questions in Codex's own configuration
663
+ document, because a Codex that disables them raises no `elicitation/create` and
664
+ answers nothing, and the Kit adds that setting only where the owner declared
665
+ none.
666
+
667
+ `kit agents sign-in <kind>` runs that CLI's own headless sign-in command in the
668
+ terminal it was started from, so the owner only completes it; House opens that
669
+ terminal and runs nothing itself. Both commands hold one control lease, release
670
+ it once they are done, and report to House afterwards what
671
+ this Environment now holds: per declared kind its compiled contract version,
672
+ the installed release or none, whether the contract's native readiness evidence
673
+ shows a signed-in provider, the candidate release a red conformance check refused, and the model and
674
+ effort options the installed adapter's own session offered, or `null` when no
675
+ release is installed or its session did not open. Every report, the worker pass's
676
+ and `kit setup`'s included, reads those options the same way and goes to
677
+ `POST /kit/agents/report` with one entry per declared kind:
678
+
679
+ ```json
680
+ {"agents": [{"kind": "claude-agent-acp", "contract_version": "1", "release": "0.81.2",
681
+ "signed_in": true, "hold": null,
682
+ "options": {"model": [{"value": "haiku", "name": "Haiku 4.5"}], "effort": []}}]}
683
+ ```
684
+
685
+ House knows about an agent CLI only
686
+ what this Kit reported: it probes no binary, version or login of its own, and
687
+ it refuses an Agent, an Agent Session and Agent-runtime acquisition for a kind
688
+ this Environment does not declare or this Kit has not reported installed and
689
+ signed in.
690
+
691
+ This installation owns one private tmux server namespace derived from
692
+ `$HOUSE_KIT_HOME`, so House never joins, lists, or destroys an ordinary tmux
693
+ session. Each acquired WorkerRun becomes one WorkerSession in one opaque exact
694
+ tmux session of that namespace, created from a Kit-owned configuration that
695
+ disables terminal retention. tmux owns the terminal lifecycle and nothing else:
696
+ the pane, its scrollback, and anything the adapter writes to its own stderr are
697
+ never read, stored, parsed, or answered, and no key is ever injected into a
698
+ pane.
699
+
700
+ Inside that session the Kit starts the launcher for the acquired route's
701
+ declared command adapter and hands it, over a Kit-private local channel, the
702
+ canonical request House gave the Kit and the adapter argument vector. Routine assignments carry their Routine Request.
703
+ Direct conversation assignments carry the sealed turn and its ordered messages,
704
+ and the Kit seals the reply itself on its own ingress.
705
+ An Agent resolves exactly one Execution route, so the acquisition envelope's
706
+ execution plan carries one `route` object and this Kit selects, orders, and
707
+ hands off nothing. The Kit launches that route as House admitted it and keeps
708
+ no local copy of it: an acquired route naming a kind `agents` does not declare
709
+ refuses as `kit_agent_undeclared`, and one whose kind has no installed release
710
+ as `kit_agent_not_installed`. The route names `adapter.kind` and
711
+ `adapter.contract_version`. The installed release of that kind plus the route's
712
+ `arguments` are the exact argument vector, so no route names a binary path.
713
+ The route's `model` and its `effort` or `null` are House-visible route facts in
714
+ the CLI's own vocabulary, which House stores as given and never validates; the
715
+ Kit hands them to the adapter at session start as its contract names, and a
716
+ value the CLI refuses fails the attempt with the CLI's own cause as
717
+ `kit_adapter_failed`. A route whose `effort` is `null` sets no effort, so the
718
+ CLI's own default effort applies and a model that offers no effort option
719
+ starts.
720
+ The adapter inherits only `PATH` and `HOME`, so the provider's own local
721
+ authorization stays in the Environment where its CLI already keeps it. The Kit
722
+ adds the Session's `house` CLI in front of that `PATH` and names the Session's
723
+ bridge socket and nonce in `HOUSE_KIT_BRIDGE` and `HOUSE_KIT_BRIDGE_NONCE`.
724
+
725
+ The launcher speaks the Agent Client Protocol through the protocol's official
726
+ TypeScript SDK, `@agentclientprotocol/sdk`, as this Kit's one protocol client;
727
+ the Kit frames no protocol JSON-RPC of its own. It opens the session with `session/new`
728
+ in the Environment's working directory with no MCP server, or reloads the
729
+ retained one with `session/load`, then sets the
730
+ route's model, and its effort when the route names one, through the protocol's
731
+ `session/set_config_option` with the option identifiers the adapter contract
732
+ names: `model` and
733
+ `reasoning_effort` for `codex-acp`, `model` and `effort` for
734
+ `claude-agent-acp`. Neither pinned adapter reads a model or effort flag of its
735
+ own. It carries the turn with one `session/prompt`, and maps that
736
+ prompt's stop reason through the contract's own table to one of four turn
737
+ outcomes: completed, interrupted, refused by the provider, or exhausted. A stop
738
+ reason the contract does not name, including a vendor-specific one a different
739
+ contract names, fails the turn as `kit_adapter_contract_mismatch` rather than
740
+ being guessed. It sends the request
741
+ as the first prompt block, followed by current House guidance fetched through
742
+ the Run connection. Missing or unreadable guidance refuses the turn; the
743
+ launcher does not copy the private Routine instruction into the acquisition
744
+ request. It forwards only adapter-classified User-visible assistant
745
+ text back over the local channel, and leaves reasoning, tool traces, and
746
+ terminal output where they are. The supervised process's own House calls travel
747
+ that same channel through the `house` CLI. The Kit holds the WorkerRunToken, replaces it in place when
748
+ it nears expiry without the child noticing, attaches a bridge-generated UUIDv7
749
+ to every Session mutation it forwards, and never places a House
750
+ credential in any child environment.
751
+
752
+ The Kit is the single source of assistant text: it observes the adapter, streams
753
+ what it has seen so far, and seals the turn itself. No Agent is asked to report
754
+ text a deterministic observer already sees, and no agent-facing tool admits final
755
+ text.
756
+
757
+ While the adapter generates, the Kit posts the whole User-visible assistant text
758
+ of the turn so far to `sessions/<session>/drafts` as an ephemeral draft. Only
759
+ adapter-classified assistant message text enters that stream; reasoning, tool
760
+ calls, plans, and terminal output never do. The Kit maps the structure it can
761
+ read in that text - paragraphs, headings, fenced code, ordered and unordered
762
+ lists, pipe tables, and `$$` formulas - into House's admitted blocks, and leaves
763
+ anything it did not parse as the paragraph it observed. It invents no structure
764
+ and renders nothing.
765
+
766
+ The Kit coalesces and paces that stream rather than mirroring every chunk. It
767
+ sends the first observed view at once, then at most one view every 500
768
+ milliseconds, and one more the moment the adapter completes a message; one call
769
+ is in flight at a time, so a newer view replaces the view it overtook and the
770
+ view observed last is the view House sees last. A view whose text exceeds
771
+ House's 4096-byte bound is not sent. A draft persists nowhere - not in this
772
+ Kit's local state, not as House history - is never retried, and can neither
773
+ delay nor prove turn completion: a refused, lost, or overtaken draft changes no
774
+ Run, WorkerSession, or delivery state. Ending the stream waits for no draft the
775
+ transport is still holding, so a slow or unanswered draft call cannot delay the
776
+ seal or the Run behind it.
777
+
778
+ When the adapter completes the turn, the Kit seals it with one
779
+ `sessions/<session>/assistant-messages` call carrying the same blocks and a
780
+ UUIDv7 `operation_id`. The exact request - that identity and those blocks - is
781
+ written into the WorkerSession binding before the call leaves, so a recovered
782
+ call returns the outcome House recorded and never seals a turn twice, and
783
+ House's answer names the turn it closed and the delivery it queued. An answer
784
+ the Kit never received leaves the Run unsettled with the request retained: the
785
+ next `kit worker run` replays exactly that operation and those blocks before
786
+ settling anything, rather than reporting a failure it cannot prove. A refusal
787
+ House did decide is the Run's reported failure.
788
+
789
+ That seal is the one durable record of the turn, and only a seal House accepted
790
+ completes a turn: the Kit reports whole-turn completion to House after the seal
791
+ succeeds and never otherwise. A turn whose adapter produced no User-visible text
792
+ seals nothing and claims no completed turn; the Run's own terminal outcome still
793
+ follows what the adapter reported.
794
+
795
+ The Agent reaches House only through the `house` CLI. Its verbs are the Tools
796
+ House lists to the Session, and its help is House's own: `house --help` prints
797
+ each listed Tool's name and description, and `house <verb> --help` prints that
798
+ Tool's description and input schema, all read from House's `tools/list` at the
799
+ call, so a catalogue change needs no Kit release. `house <verb>
800
+ '<arguments as one JSON object>'` calls that Tool, or `house <verb> -` with the
801
+ object on standard input for arguments longer than one command-line argument
802
+ holds. It prints House's text answer, or a refusal on standard error with a
803
+ nonzero exit status. The CLI holds
804
+ no credential: it calls over its Session's bridge, which attaches the Session
805
+ credential and refuses a caller without that Session's nonce as
806
+ `kit_bridge_unauthorized`. House network calls use only the selected
807
+ `2026-07-28` profile.
808
+
809
+ Two verbs name files on the Session's own computer: `upload_attachment` takes
810
+ one `path`, and `append_record` takes `attachments` as local paths. The CLI
811
+ resolves each path against its working directory. The bridge reads each regular
812
+ file, computes its size and SHA-256 and the media type an image's leading bytes
813
+ show, declares them beside the call, and uploads the exact bytes through the
814
+ one-use grant House answers before the call completes, so the answer carries the
815
+ attachment's `at_` reference. `append_record` uploads every file into its Room
816
+ first and names their references in the Record; a file that cannot be read or
817
+ saved appends nothing. The bridge reads House's YAML answers to
818
+ `upload_attachment`. A Room or `/private` upload ends at the byte upload rather
819
+ than at a House answer, so the bridge prints that saved answer in the same YAML;
820
+ every other answer passes through as House wrote it. With `conversation: true`
821
+ the file becomes the owner's conversation original, and the bridge calls again
822
+ and waits for House's delivery outcome. House derives the presentation from the
823
+ declared media type: an image is uploaded inline beside a link to the unmodified
824
+ original, and every other file is offered as that link alone.
825
+
826
+ The bridge retains the exact pending House request in memory before transfer.
827
+ Within the same live Kit and supervised WorkerSession, a lost response or
828
+ infrastructure failure resends that request with the same identifier. A local
829
+ socket reconnect delivers the retained outcome to the process.
830
+ A second deliberate call gets a new identifier; read-only calls carry none.
831
+ House refusals pass through to the CLI without automatic resend. [R193](https://github.com/agentshouse/core/blob/main/docs/concept/implementation/08-routine-requests-human-notifications-and-workers.md#r193)
832
+ owns recovery authority, lifetime, and refusal semantics.
833
+
834
+ The launcher speaks only what the adapter contract fixes for this build. Every
835
+ session initializes with the same fixed client capabilities: no file-system
836
+ methods, no terminal methods, and elicitation in form mode and not in URL mode.
837
+ It offers the adapter no filesystem or terminal surface of its own, so an adapter
838
+ that needs a decision the Kit cannot carry fails its turn instead of receiving a
839
+ guessed answer. Execution permission is the one decision the Environment already
840
+ settled: `kit.json` declares the Environment's `execution_authority` as `allow`
841
+ or `refuse`, and the launcher answers every adapter permission request with that
842
+ declared outcome and nothing else. The authority is a local Environment fact - it never reaches
843
+ House, and no permission request is ever projected to a human.
844
+
845
+ A task question is the decision the Environment cannot settle, so the launcher
846
+ carries it to the owner instead of answering it. The Interaction is the
847
+ protocol's own form end to end: an adapter's `elicitation/create` request in
848
+ form mode reaches House's interaction act as its `message` and its
849
+ `requested_schema`, unchanged in meaning, with the exact correlation of the
850
+ provider turn and of the adapter request that raised it. House alone judges
851
+ whether the schema is inside the flat subset it can ask; a form it admits as
852
+ unsupported, and a URL-mode request the Kit never forwards, cancel the
853
+ adapter's request with the protocol's `cancel` and ask the owner nothing.
854
+
855
+ Nothing of an adapter's native wire crosses: the Kit strips every native
856
+ `_meta` from the request, the schema, its properties and their options, and
857
+ gives a property the adapter left untitled its own name as title. Two facts
858
+ cross as House-namespaced marks instead. The batch carries
859
+ `_meta["agents.house/secret"]` when the adapter classifies the request as
860
+ secret in its own meta, as `codex-acp` does with `isSecret`; House records it
861
+ as unsupported and raises an interrupt rather than a card, so no secret prompt
862
+ is projected to a human and no secret answer travels back through House. A
863
+ choice carries `_meta["agents.house/free-text"]` when the adapter's own
864
+ convention admits the owner's words beside the listed options:
865
+ `claude-agent-acp` pairs a question with a `_askUserQuestionCustomAnswer`
866
+ companion field, and `codex-acp` pairs it with a `"None of the above"` option
867
+ and a `<id>_note` field tagged `user_note`. The companion field and the
868
+ synthetic option never reach House.
869
+
870
+ The owner's answer arrives as the `answer` command carrying the protocol's own
871
+ response, `accept` with content keyed by property name or `decline` for the
872
+ whole form, and is applied to that exact pending request; an answer naming
873
+ another turn or another correlation has no effect, so a stale or cross-Session
874
+ answer cannot reach a live adapter. A typed answer to a free-text choice goes
875
+ back through the adapter's own convention: into the companion field for
876
+ `claude-agent-acp`, and as `"None of the above"` with the typed text in the note
877
+ field for `codex-acp`. The turn waits on the adapter's own request while the
878
+ owner decides, and the Kit reports what became of each Interaction, so a
879
+ Session that ends with one outstanding expires it instead of leaving it live.
880
+
881
+ Before new execution, `kit worker run` reconciles House-authorized inbound cache
882
+ cleanup and materializes every pending conversation file House offers, ten at a
883
+ time, under `$HOUSE_KIT_HOME/conversation-inbound/<run>/<attachment>/<name>`.
884
+ House seals due input itself. The Kit uses the existing Environment lease and
885
+ byte endpoint, limits each file to 32 MiB, and reports materialization after
886
+ receiving a complete byte stream, validating any reported size, and writing the
887
+ file. A lost acknowledgement preserves the completed local file for House's
888
+ idempotent admission and later cleanup. Failed files remain pending; House's
889
+ all-files-ready gate keeps their turn unprocessed.
890
+ The provider receives absolute local paths beside the owning messages, both in
891
+ its initial prompt and in `session_get_request`. The cache persists across
892
+ turns; only House-authorized terminal Run cleanup removes its Run directory.
893
+
894
+ The accepted attachment's retained original is separate from that cache. The Kit
895
+ records a local note for the attachment before it acknowledges materialization,
896
+ then saves the exact bytes it received through House's conversation-original
897
+ door, naming only the attachment; a download that fails reports
898
+ `source_unavailable` for the same version.
899
+
900
+ A save that fails leaves one note in `state/conversation-originals/` naming the
901
+ attachment, its Run, the local file and the descriptor it declared. Each
902
+ later `kit worker run` retries it from that file without materializing or
903
+ acknowledging anything again, and reports `source_unavailable` when
904
+ the file is gone or changed rather than saving different bytes. While a
905
+ WorkerSession stays supervised the same retry runs at the heartbeat interval. The note leaves once
906
+ House records the save, once House answers that the Run is over, or when House
907
+ authorizes that Run's cache cleanup; House accepts no save for a Run that has
908
+ settled, so an unsaved original keeps its failed outcome from then on. The owner
909
+ reads every saved original through House afterwards, whether or not this
910
+ Environment still exists. `kit worker run` prints every conversation original
911
+ outcome it handled, and exits non-zero while any of them is unsaved.
912
+
913
+ One `kit worker run` then takes one House-authorized resumption, or otherwise
914
+ acquires one waiting Run, and supervises exactly that WorkerSession. A resumed
915
+ conversation loads its retained native provider session through ACP
916
+ `session/load`, receives the current input from House through
917
+ `session_get_request`, and executes one turn. Historical output replayed during
918
+ loading is not emitted as new assistant output. Missing or rejected provider
919
+ continuation never starts a replacement provider conversation in the same Run.
920
+
921
+ Continuation is per adapter. A contract that pins session loading retains the
922
+ adapter's opaque session handle between turns and resumes through it, as
923
+ `codex-acp` and `grok-build` do. A contract without it, as `claude-agent-acp`, retains nothing:
924
+ its turn reports no provider continuation, the Agent Session ends with the
925
+ reported cause `kit_provider_continuation_unavailable`, and the owner starts the
926
+ next one.
927
+
928
+ Between turns, Kit retains the opaque provider session identifier and the Run's
929
+ Worker, working directory, and route references in an owner-only local record.
930
+ The record carries no transcript or Session credential and grants no authority:
931
+ House must authorize each successor WorkerSession. A reported terminal state and
932
+ owner stops remove the record. [R199](https://github.com/agentshouse/core/blob/main/docs/concept/implementation/08-routine-requests-human-notifications-and-workers.md#r199)
933
+ owns conversation continuity.
934
+
935
+ Launcher exit is the terminal process observation. The Kit closes the local
936
+ channel, grants the launcher a fixed local exit grace, destroys the exact tmux
937
+ session if it is still there, and then reports one stopped Session at
938
+ `POST /kit/sessions/:session/stopped` and the adapter's own continuation facts at
939
+ `POST /kit/sessions/:session/continuation`, whose body is exactly
940
+ `{"provider_continuation": <bool>, "provider_capacity_exhausted": <bool>}`. No
941
+ adapter-supplied reset instant and no handoff availability is sent, and nothing
942
+ schedules a wake.
943
+
944
+ Kit, not the Agent, then reports the terminal state the execution reached at
945
+ `POST /kit/runs/:run/outcome`. No agent-facing tool posts it, and settling writes
946
+ no Record, Posting or Capture by itself: whatever the instruction wanted written
947
+ is written through the ordinary authorized Run mutations first. The body is
948
+ `{"operation_id": <uuidv7>, "outcome": "completed"}` or
949
+ `{"operation_id": <uuidv7>, "outcome": "failed", "cause": "<1-200 chars>"}`, and
950
+ the reported cause is exactly what the adapter gave. A finished turn that needed
951
+ no further Provider continuation is `completed`. A known execution failure, a
952
+ provider capacity exhaustion, a positively lost Provider continuation, and a
953
+ rejected native resume command are each `failed` with their own cause; House
954
+ notifies the owner and nothing is retried. A confirmed-stopped Session whose
955
+ Provider continuation remains available reports no terminal state at all and
956
+ leaves the Run active for an owner-originated resumption.
957
+
958
+ The Kit decides that terminal state once, records it beside its stable operation
959
+ identity before it reports it, and reports it from that record. A Run with a
960
+ supervised session records it in that session's durable local binding; a Run
961
+ whose terminal state was decided without one — a resume command this Kit rejects
962
+ because it retains no Provider continuation for the Run, or an acquired Run it
963
+ refuses before any provider process starts, which it commits and fails with the
964
+ refusal as the cause so no Run stays acquired behind it — records it in a
965
+ durable local settlement note of its own, named by the Run. A lost response is recovered by reporting that
966
+ identical body under the same operation identity on the next `kit worker run`;
967
+ House answers the committed settlement verbatim and the Agent is never executed
968
+ again. A Run House has already settled answers `worker_run_settled`, which the
969
+ Kit accepts as settled and drops the record for.
970
+
971
+ Every other refusal leaves that record exactly as it is. The Run stays bound to
972
+ this installation, `kit worker run` names the refusal House answered with and
973
+ exits non-zero, and the same decided body is reported again before anything is
974
+ acquired. Nothing is dropped locally that House did not accept, and a refused
975
+ report is never presented to the owner as a delivered one.
976
+
977
+ While a WorkerSession is supervised the Kit keeps one durable local binding
978
+ naming this installation, the WorkerRun, the WorkerSession, the exact tmux
979
+ session, and the local channel that reaches it. The binding carries no terminal
980
+ content, no prompt, and no resolved secret, and Session authority is always
981
+ reissued by House rather than recovered from disk. It is evidence for
982
+ reconciliation, never permission to resume or recreate work, and nothing keeps
983
+ it once House has answered for the Run: a settled terminal state, a confirmed
984
+ owner stop, and a retained Provider continuation each drop it.
985
+
986
+ A restarted `kit worker run` reconciles the records it kept before it acquires
987
+ anything new. It reports every retained settlement note first, because those
988
+ Runs have no session left to reconcile, and drops each note House accepts or
989
+ already settled. It next consumes the durable commands House holds for this
990
+ Environment, so a requested owner stop ends the exact tmux session before any
991
+ local channel reopens. It then considers only sessions of its own namespace
992
+ whose binding is complete, and never lists unknown sessions, scans host
993
+ processes, searches another namespace, or adopts an arbitrary process. A session
994
+ that is already gone is confirmed gone, reported as one stopped Session with no
995
+ Provider continuation, and settled as a terminal execution failure. A session
996
+ that is still running is restored only after House
997
+ positively replaces the Session authority for the same Run and Session; the Kit
998
+ then reconnects the typed adapter channel, refreshes the started observation,
999
+ and supervises that session to settlement instead of acquiring another Run. An
1000
+ explicit House refusal destroys the stale tmux session and exposes no channel.
1001
+ House that cannot be reached leaves the binding and the session exactly as they
1002
+ are, invents no stopped, active, or authorized state, and acquires nothing.
1003
+
1004
+ A restore of House's database starts a new log epoch. Until this Environment
1005
+ has reported in it, House refuses every act that touches a Run, acquisition and
1006
+ lease release included, as `epoch_report_required` naming the epoch. The first
1007
+ such refusal ends whatever the pass was doing. One raised inside a supervised
1008
+ Run's bridge, while it admits or settles a question, streams a draft, or
1009
+ replaces Session authority for a forwarded call, never becomes an answer to the
1010
+ Agent: it ends the bridge and the pass at once. The Kit then reports, under the
1011
+ lease it holds and at `POST /kit/sessions/report`, every Run it holds a spawn
1012
+ commitment or a terminal outcome for: each retained binding, settlement note,
1013
+ and Provider continuation, as `{"session": "<run>", "outcome": null}` while its
1014
+ process runs, while it holds only its continuation, or while its outcome is
1015
+ unknown, or with the held outcome it would have reported at
1016
+ `POST /kit/runs/:run/outcome`, `completed` or `failed` with its cause. One
1017
+ report carries at most 256 Runs, so the Kit sends as many as it needs, all
1018
+ before it acquires anything. House settles every reported Run and never offers
1019
+ it again, so once it has answered every report the Kit destroys each reported
1020
+ tmux session still running, drops each reported Run's binding, settlement note,
1021
+ and retained Provider continuation, and runs the pass again from its start. A
1022
+ refused act is never a Run failure, nothing is acquired in the new epoch before
1023
+ the report, and a refusal that follows the report ends the command naming it.
1024
+
1025
+ The launcher inside the supervised session outlives the Kit process that started
1026
+ it. Its local channel reconnects to the same socket with a bounded backoff and
1027
+ announces that its adapter session already exists, so a restored bridge
1028
+ acknowledges the resumption instead of starting a second turn and the supervised
1029
+ process never sees a second request. A call already transferred when the Kit
1030
+ process ends is never automatically replayed by its successor, even when the
1031
+ same local channel reopens. Pending mutation requests and identifiers are not
1032
+ written into the durable binding. Reconciliation must obtain current House
1033
+ authority before the live process can make further calls.
1034
+
1035
+ An owner stop requested through House reaches this Kit as a durable command,
1036
+ both at startup and while a Session is supervised. The Kit destroys the exact
1037
+ tmux session, confirms that it is gone, and reports the stopped Session. It
1038
+ reports no continuation for a Run stopped by its owner and never reopens the
1039
+ local channel for that Session.
1040
+
1041
+ ```sh
1042
+ kit worker attach # the one supervised session
1043
+ kit worker attach <session> # the exact session
1044
+ ```
1045
+
1046
+ Attachment is a read-only diagnostic for the Environment owner. It reaches only
1047
+ the exact tmux session of a live binding in this installation's namespace, gives
1048
+ the Worker no input, makes no House call, causes no lifecycle transition, and
1049
+ leaves the namespace configuration and its disabled terminal retention as they
1050
+ are. Once the Session has settled or its tmux session is gone, attaching is
1051
+ refused and names why.
1052
+
1053
+ ## Paired proof
1054
+
1055
+ Paired installed-CLI proof against the merged House producer is a labelled
1056
+ validation build, not an npm release. From Linux Compose:
1057
+
1058
+ ```sh
1059
+ docker build -f test/pair/producer.dockerfile -t kit-original-pair:latest .
1060
+ HOUSE_CORE_ROOT=<agents.house checkout> \
1061
+ docker compose -f test/pair/compose.yaml up --abort-on-container-exit --force-recreate producer
1062
+ ```
1063
+
1064
+ It boots the real producer against a real PostgreSQL, installs the exact
1065
+ candidate package, and runs `test/pair/*.vitest.ts`: the attachment doors,
1066
+ a Source adapter retaining a declared file into Rooms of two owners, and an
1067
+ installed Worker retaining an incoming attachment through failure, retry, Run
1068
+ settlement and cache disposal. `KIT_PAIR_SPECS`
1069
+ names the specs to run, and `HOUSE_CORE_REVISION` and `KIT_REVISION` are copied
1070
+ into the evidence each spec writes under `tmp/delivery-evidence/`.
1071
+
1072
+ ## Capturing
1073
+
1074
+ ```sh
1075
+ kit source capture acme --house https://agents.house # every declared service
1076
+ kit source capture acme notes --house https://agents.house # one service
1077
+ ```
1078
+
1079
+ Each run reads the checkpoint, acquires one bounded page, submits one batch,
1080
+ and advances the checkpoint only once every item in that batch is accepted or
1081
+ an idempotent duplicate at the House door. Every batch names the service's exact
1082
+ Source reference and its complete Room destination set, so House admits all of
1083
+ them or none: one destination the delivering User cannot currently write refuses
1084
+ the whole publication before any content changes. A refused item leaves the
1085
+ checkpoint where it was, so the next run reoffers the whole batch and converges
1086
+ on the door's own deduplication. When House stops a batch early, its answer
1087
+ settles only the prefix it reached, and the unsettled tail is reoffered with the
1088
+ rest. The loop repeats while the connector still returns items and no Room
1089
+ destination is blocked, and every run ends by writing the service's health into
1090
+ this installation's own state. House holds no preset, service, route, checkpoint
1091
+ or health of yours.
1092
+
1093
+ A Capture item that declares a `file` attachment answers one Record per Room
1094
+ destination: Rooms of one owner share that owner's Record, and a Room of another
1095
+ owner holds its own Record of the same delivery. The Kit records one retained
1096
+ reference for every destination Room and attachment in
1097
+ `state/<instance>/originals/<service>.json` before it saves anything, then
1098
+ saves that batch's references through House's captured-original door before it
1099
+ asks the connector for the next page: it reads the declared path, and uploads
1100
+ the bytes only when they still match the declared descriptor. References still
1101
+ outstanding from earlier runs are retried once the run's pages are delivered.
1102
+ A missing or changed file reports `source_unavailable` instead, so House never
1103
+ receives a substitute version. House, not the Kit, chooses where each Room
1104
+ owner's copy is kept. Every run prints each reference's save outcome beside its
1105
+ Room and Record and exits non-zero while any stays unsaved; acquisition health
1106
+ stays about acquisition, so a failed save never changes it.
1107
+
1108
+ ```sh
1109
+ kit source retry-files acme --house https://agents.house # every declared service
1110
+ kit source retry-files acme notes --house https://agents.house # one service
1111
+ ```
1112
+
1113
+ `retry-files` saves only the references still outstanding. It runs no connector,
1114
+ submits no Capture, moves no checkpoint, and keeps each reference's Record and
1115
+ accepted version; with nothing outstanding it contacts no House at all. A saved
1116
+ reference, or one whose Posting House no longer holds, leaves the local state. A
1117
+ Room whose write authority is gone answers `room_not_found` and stays outstanding
1118
+ until that authority returns.
1119
+
1120
+ ```sh
1121
+ kit source status acme # every declared service
1122
+ kit source status acme notes # one service
1123
+ ```
1124
+
1125
+ `kit source status` reads only local state: the current health and its exact
1126
+ category, the stored cursor, every open coverage gap with the recovery reference
1127
+ House returned for it, the items this installation dropped permanently, and the
1128
+ retained references still waiting to be saved.
1129
+
1130
+ Every command that changes or reads installation state takes this
1131
+ installation's exclusive Environment control lease for the length of its run
1132
+ and renews it before it expires, so two Kit commands never work against the
1133
+ same installation at once. A command that ends, or is interrupted once its work
1134
+ has stopped, releases that exact lease so the next command acquires at once; a
1135
+ refused command leaves it to expire. A `kit login` credential is presented only to take
1136
+ that lease.
1137
+
1138
+ While `kit resident` holds that lease, a command runs through it instead: the
1139
+ command presents no credential, the resident confirms its current lease with
1140
+ House and sends the command's House requests under it, an interrupted command
1141
+ cancels its request there, and the lease stays held when the command ends. The
1142
+ resident serves one such command at a time and refuses another with
1143
+ `environment_control_lease_active`, so two commands still never work at once.
1144
+ A resident whose lease House refuses answers the command with that refusal; the
1145
+ command never acquires a lease of its own while a resident serves it.
1146
+
1147
+ Interrupting the command cancels the running connector without advancing the
1148
+ checkpoint.
1149
+
1150
+ ## Repository capture
1151
+
1152
+ Publish one local Git checkout as a Repository Source without giving House any
1153
+ Git credential:
1154
+
1155
+ ```sh
1156
+ kit repository capture /srv/acme \
1157
+ --room work-room \
1158
+ --source-handle acme-repository \
1159
+ --house https://agents.house
1160
+ ```
1161
+
1162
+ The command runs ordinary `git pull` in the checkout with the local User's Git
1163
+ configuration, credential helper, and SSH agent. After pull succeeds, Kit asks
1164
+ Git for tracked files plus untracked non-ignored files, hashes each selected
1165
+ file, and opens a Publication by posting that manifest - every selected path
1166
+ with its content fingerprint and byte length - to the authenticated Kit door.
1167
+ Git remains the only ignore owner.
1168
+
1169
+ House answers the Publication reference, the paths it needs, and the chunk
1170
+ bound it accepts. Kit delivers only those paths, in House's order, in chunks
1171
+ under that bound, streaming each file from disk at delivery time; it carries no
1172
+ bound, no delta, no counter and no retained archive of its own. House owns
1173
+ every tombstone, orders the Publication, and completes it in the request that
1174
+ leaves no needed path outstanding.
1175
+
1176
+ A file whose bytes disagree with the manifest House holds is refused as that
1177
+ item and the rest of the chunk stands. When a delivery round ends with refused
1178
+ items and the Publication is still active, Kit opens a new manifest of the
1179
+ current checkout and delivers again, at most three manifests per run. An
1180
+ interrupted run is never resumed: the next run opens a new manifest and House
1181
+ answers with what is still needed. The command reports the Publication
1182
+ reference, the paths delivered, the items refused and the ending.
1183
+
1184
+ ## Explicit synchronization
1185
+
1186
+ Beside the reconciling Working Copy, Kit has one explicit synchronization mode.
1187
+ It selects prefixes over the User root, materializes them into a named local
1188
+ directory, enumerates whole content and removals after the recorded positions,
1189
+ and submits a resolved exact-base change set the caller prepared. Kit does no
1190
+ local reconciliation. The mode shares no Working Copy manifest, never infers a
1191
+ removal from a locally absent file, and writes authored mutations only within
1192
+ House's declared write scopes: a selected Room's Library and, where granted,
1193
+ `ROOM.md`. `/private` selects the Private Room by its Room reference, records an independent Room
1194
+ position, and keeps its `agents/` cards and Room-specific private `AGENTS.md`
1195
+ beside `library/**`.
1196
+
1197
+ ```sh
1198
+ kit sync bootstrap ./replica \
1199
+ --house https://agents.house \
1200
+ --prefix /rooms/work/library \
1201
+ --prefix /rooms/work/capture \
1202
+ --prefix /private/library
1203
+ kit sync pull ./replica --house https://agents.house
1204
+ kit sync submit ./replica --house https://agents.house --changes ./changes.json
1205
+ kit sync rebootstrap ./replica --house https://agents.house
1206
+ ```
1207
+
1208
+ `bootstrap` writes `ROOM.md`, Library text, House-derived projections, and
1209
+ Capture text with its provenance sidecar, and records one position per selected
1210
+ authority from the bundle, with the log epoch House issued it in, together with
1211
+ the positive operation scopes House declared for it: `read`, `sync`, `write`,
1212
+ and `delete`, each a list of paths relative to the authority under which that
1213
+ operation is admitted. Managed replication needs `read` plus `sync` on a Room;
1214
+ a Room the User can only read answers `bootstrap`, `pull`, and `rebootstrap`
1215
+ with `operation_denied`. Generated `_provenance` files delivered beside authored
1216
+ Library paths are House metadata, not authored Library documents. `pull` presents
1217
+ each recorded position with its log epoch, applies each enumerated path's whole
1218
+ content or removal, and advances those positions. A position older than House's
1219
+ declared retention, or one of a log epoch before House's database was restored,
1220
+ is reported as `position_expired` with rematerialization as the remedy; only
1221
+ `rebootstrap` rebuilds the directory.
1222
+
1223
+ `submit` sends field, section, and preamble operations with that value's prior
1224
+ content as base, and whole-file, rename, and removal operations with the file
1225
+ revision. A rename of a populated directory instead carries a `base` map from
1226
+ every relative member path to its exact revision, such as
1227
+ `{"plan.md": "<revision>", "deep/log.md": "<revision>"}`, and House refuses it
1228
+ unless the map names exactly the directory's current members at their current
1229
+ revisions. Kit admits the rename at its source and destination and each named
1230
+ member at both, as House does.
1231
+ House alone validates the resulting Library: a batch it refuses answers with
1232
+ House's own file path, field, rule, and version. A refusal is reported with the
1233
+ current material House returned and the local directory is left for the caller.
1234
+ An acceptance updates the recorded position, its log epoch, and the affected
1235
+ files from the returned final content.
1236
+
1237
+ Pending submissions are written before transfer. A lost response, reconnect, or
1238
+ lease renewal recovers that exact request under the same identifier. A domain
1239
+ refusal is reported without automatic retry and without minting a replacement
1240
+ identifier. A second deliberate submit is a new identifier. After a restore of
1241
+ House's database, a request House holds no receipt for and whose identifier
1242
+ dates earlier than five minutes after the new log epoch began is answered
1243
+ `position_expired`: it wrote nothing and never will under that identifier, so
1244
+ the Kit drops it like any other refusal, `rebootstrap` rebuilds the directory,
1245
+ and a change still wanted is a new deliberate submit against the rebuilt bases.
1246
+ Once House has answered any request for a replica `position_expired`, the
1247
+ replica refuses `submit` as `position_expired` until `rebootstrap` rebuilds it.
1248
+ `rebootstrap` drops a submission still pending against the directory it
1249
+ replaces.
1250
+
1251
+ `--changes` names a JSON array of exact-base operations, or an object with a
1252
+ `changes` array. Omitting it derives field, section, preamble, create, and whole-file
1253
+ operations from local writable files against the last confirmed snapshot and
1254
+ still never infers a removal. A local edit of a selected protected path is
1255
+ refused before House is contacted as `protected_path`; a change outside the
1256
+ authority's `write` scope, or a removal outside its `delete` scope, is refused
1257
+ before House is contacted as `operation_denied`, the same code House answers
1258
+ for a batch that reaches it.
1259
+
1260
+ ## Attachments
1261
+
1262
+ Upload one local file as an attachment to a Room or to your Private Room. Name
1263
+ the place explicitly. Kit sends the descriptor and the selected bytes; it does
1264
+ not scan a directory, convert a URL, or attach sibling files, credentials, or
1265
+ absolute paths.
1266
+
1267
+ ```sh
1268
+ kit attachment upload ./notes.bin \
1269
+ --house https://agents.house \
1270
+ --room room:work \
1271
+ --media-type application/pdf
1272
+
1273
+ kit attachment upload ./notes.bin \
1274
+ --house https://agents.house \
1275
+ --private
1276
+
1277
+ kit attachment get \
1278
+ --house https://agents.house \
1279
+ --attachment at_1x.q \
1280
+ --room room:work \
1281
+ --output ./notes.bin
1282
+ ```
1283
+
1284
+ `--room` and `--private` are mutually exclusive and one is required; `--name`
1285
+ defaults to the file's base name. The upload prints the attachment reference
1286
+ (`at_…`) House answered. That reference is what text links to: an attachment is
1287
+ meaningful only because a document links it, and House records those links from
1288
+ the accepted text. Retrying the same file after a lost response or failed upload
1289
+ reuses the stored version under the identical descriptor, so it completes the
1290
+ same attachment instead of creating a second one; uploading the same file to the
1291
+ same place again answers that same attachment. A changed file is a new
1292
+ attachment. HTTP 200 is not retention: only `save.status: saved` plus an
1293
+ independent download whose length and SHA-256 match counts. A failed local read
1294
+ or failed House save is not reported as saved. Transfer uses the current control
1295
+ lease and the one reusable byte client; the durable credential is presented only
1296
+ to `POST /kit/lease`. Sync commands do not fetch attachment bytes; Source and
1297
+ Worker retention save through the same byte client.
1298
+
1299
+ The same paired installed-CLI proof covers these doors; see
1300
+ [Paired proof](#paired-proof) for the command that runs it.
1301
+
1302
+ ## Persistent container Environments
1303
+
1304
+ An Always-on Linux host can run the persistent container Environments its owner
1305
+ creates in House. House owns the target: which host, the workspace name, and the
1306
+ CPU, memory and persistent-disk ceilings. This Kit owns the local lifecycle and
1307
+ reports only what it proved.
1308
+
1309
+ The supported configuration is one Linux host with
1310
+
1311
+ - cgroup v2 delegating the `cpu` and `memory` controllers, read from
1312
+ `/sys/fs/cgroup/cgroup.controllers`;
1313
+ - a reachable Docker Engine daemon;
1314
+ - `/dev/loop-control` present, so a workspace disk can carry an enforced size;
1315
+ - `host.workspace` pointing at a directory this Kit may create workspaces under;
1316
+ - `host.image` naming an image that carries Node, this Kit, `mkfs.ext4` and
1317
+ `nsenter`, and whose own entrypoint starts `kit worker run` from
1318
+ `HOUSE_KIT_HOME`.
1319
+
1320
+ Any missing prerequisite refuses the create before House ever presents the
1321
+ workspace as constrained, naming the exact missing piece.
1322
+
1323
+ ```sh
1324
+ kit workspace host --house https://agents.house
1325
+ ```
1326
+
1327
+ The command acquires this host's workspace work under its Environment control
1328
+ lease and settles each operation with what it observed:
1329
+
1330
+ - **create** formats one ext4 image of exactly the configured disk ceiling,
1331
+ mounts it as the workspace's only writable filesystem, writes that
1332
+ Environment's own Kit credential and a `kit.json` declaring the selected agent
1333
+ CLIs with `execution_authority` `allow` inside it, since nobody holds a
1334
+ terminal there to answer otherwise, and creates the container with
1335
+ `--cpus`, `--memory` and `--memory-swap` at the configured ceilings. The
1336
+ workspace is reported `stopped` with the runtime and the enforcement it
1337
+ proved. This host's own credential never enters the workspace: the only
1338
+ credential written there belongs to the Environment running inside it.
1339
+ - **start** mounts the workspace disk if the host rebooted, refuses when
1340
+ starting would leave less than the host reserve, starts the container, and
1341
+ reports `ready` only after the exact workspace proved it accepts execution.
1342
+ - **stop** stops the container and keeps the disk mounted, so ordinary files,
1343
+ installed tools and login state survive to the next start.
1344
+ - **delete** removes that exact container, unmounts that exact workspace disk and
1345
+ removes that exact directory. No other workspace, and nothing else on this
1346
+ host, is touched.
1347
+
1348
+ Every operation names one workspace this Kit created. A workspace the runtime no
1349
+ longer has is reported `absent` rather than recreated, so House ends the
1350
+ Environment visibly instead of inventing a process observation. On every pass
1351
+ this Kit reconciles its own labelled containers against the exact inventory
1352
+ House authorizes: a managed workspace House no longer authorizes to run is
1353
+ stopped, and a container this Kit did not create is never adopted, started or
1354
+ removed.
1355
+
1356
+ Ceilings are shared ceilings. The ceilings configured across one host may exceed
1357
+ that host, nothing is reserved for any workspace, and no workspace is promised
1358
+ its maximum while another one runs.
1359
+
1360
+ ## The Telegram gateway
1361
+
1362
+ House can project one owner's conversations through that owner's own Telegram
1363
+ bot. In Kit mode the BotFather token stays in one installed Environment, and
1364
+ this Kit is the only thing that calls the Bot API. House never receives the
1365
+ token: it appears in no request body, no refusal and no printed summary.
1366
+
1367
+ Exactly one Environment may be the gateway for one bot. House records that
1368
+ binding when the gateway registers, and refuses a second Environment claiming
1369
+ the same bot. Worker Environments stay unchanged: they still acquire Runs and
1370
+ materialize inbound conversation files.
1371
+ The gateway never runs a Worker's provider and a Worker never holds the token;
1372
+ the two exchange bytes only through House, which relays an inbound attachment
1373
+ from the gateway to the Worker that needs it and serves the gateway an outbound
1374
+ image from the original the Session stored.
1375
+
1376
+ Bind the bot to this Environment:
1377
+
1378
+ ```sh
1379
+ kit telegram connect --house https://agents.house
1380
+ ```
1381
+
1382
+ The command asks for the token on a hidden terminal prompt, or reads one line
1383
+ from standard input when it is not attached to a terminal. It asks Telegram `getMe`, and
1384
+ refuses a bot whose Threaded Mode is off, because House projects each
1385
+ conversation into its own forum topic. It then asks House whether this
1386
+ Environment may become the gateway, before it writes anything locally. Once
1387
+ House admits the preflight it deletes any webhook, so long polling is the only
1388
+ receiver, and proves the owner by reading one ordinary private message outside
1389
+ any topic through `getUpdates`, advancing the stored offset past it. Only then
1390
+ is the token written, and only then is the gateway registered with House. A
1391
+ setup House refuses removes the written token again, so a refused connect leaves
1392
+ no custody behind. The command prints the bot username, the bot user id and the
1393
+ proven owner's Telegram user id.
1394
+
1395
+ Run the gateway:
1396
+
1397
+ ```sh
1398
+ kit telegram serve --house https://agents.house
1399
+ ```
1400
+
1401
+ The command holds one Environment control lease for the whole run and performs
1402
+ three activities inside it, each paced on its own so none of them blocks
1403
+ another:
1404
+
1405
+ - **ingress** reads updates from the stored offset, normalizes each one into the
1406
+ update House accepts, submits it, and advances the stored offset only after
1407
+ House has taken it. A submission that fails leaves the offset where it was, so
1408
+ the same update is offered again rather than lost.
1409
+ - **inbound files** acquires the attachments House still needs, refuses anything
1410
+ reported above the 20 MiB download ceiling before calling `getFile`, downloads
1411
+ the file with the local token, and pushes the bytes to House for the Worker
1412
+ Environment that will materialize them. That push waits for the Worker to pull
1413
+ and a single failed fetch never ends the pass.
1414
+ - **outbound queue** acquires the per-bot queue in its exact order, marks each
1415
+ delivery attempted before it sends, and reports what Telegram answered:
1416
+ `delivered` with the exact message or topic Telegram returned, `failed` when
1417
+ Telegram refuses outright, and `delivery_unknown` when
1418
+ the answer is absent or unreadable. File items are pulled back from House,
1419
+ refused as `file_too_large` above the 10 MiB image ceiling, uploaded as
1420
+ `sendPhoto` with the caption House composed around the link to the retained
1421
+ original, and reported the same way; a pull that fails reports
1422
+ `transfer_failed` and an ambiguous upload answer reports `delivery_unknown`.
1423
+ Only an image ever becomes a file item, because House offers every other file
1424
+ as a `conversation_file_link` message this queue delivers like any other. Live drafts ride that same queue as their own item and are
1425
+ never reported back, because a draft has no settlement: House has already
1426
+ coalesced each conversation to its latest view, and the gateway sends the
1427
+ drafts of a pass after its durable deliveries with the Bot API's native
1428
+ `sendRichMessageDraft` on the bound forum topic thread, under the same bot-wide
1429
+ `retry_after` gate. The gateway coalesces nothing again and holds no draft for
1430
+ a later pass: a draft taken in a pass that gate closed is dropped, and the next
1431
+ view House coalesces takes its place. Pacing lives here because this is where
1432
+ the Bot API is called.
1433
+
1434
+ Each serve run first asks House to settle the attempts an earlier run abandoned,
1435
+ so a restarted gateway never leaves a delivery attempted forever. A Telegram
1436
+ `retry_after` opens one bot-wide gate: nothing is sent, fetched or polled until
1437
+ the delay Telegram supplied has passed. `--passes <count>` bounds each activity
1438
+ to that many passes and is what tests and one-shot runs use; without it the
1439
+ command is resident and stops on SIGINT or SIGTERM, printing what it settled.
1440
+
1441
+ Release the bot:
1442
+
1443
+ ```sh
1444
+ kit telegram disconnect --house https://agents.house
1445
+ ```
1446
+
1447
+ The command writes a local disconnect marker first, deletes the webhook, removes
1448
+ the token and the stored offset, tells House this Environment is no longer the
1449
+ gateway, and removes the marker last. Local custody is therefore always gone
1450
+ before House admits the change. A disconnect interrupted after the marker was
1451
+ written finishes on a later run even though the token is already gone, and a
1452
+ `kit telegram serve` that finds the marker refuses to act.
1453
+
1454
+ ## Local state
1455
+
1456
+ ```
1457
+ $HOUSE_KIT_HOME/
1458
+ credential
1459
+ kit.json
1460
+ conversation-inbound/<run>/<attachment>/<name>
1461
+ secrets/personal-telegram-bot.token
1462
+ state/telegram/get-updates-offset.json
1463
+ state/telegram/disconnecting.json
1464
+ state/hooks.json
1465
+ state/hooks/<hook id>.txt
1466
+ state/repositories/<repository identity>.json
1467
+ state/sync/<replica identity>.json
1468
+ state/sync/<replica identity>.pending.json
1469
+ state/attachments/<upload identity>.json
1470
+ state/conversation-originals/<attachment identity>.json
1471
+ state/sessions/<tmux session>.json
1472
+ state/continuations/<run identity>.json
1473
+ state/<instance>/credentials/google.json
1474
+ state/<instance>/checkpoints/<service>.json
1475
+ state/<instance>/health/<service>.json
1476
+ state/<instance>/gaps/<service>.json
1477
+ state/<instance>/dropped/<service>.json
1478
+ state/<instance>/originals/<service>.json
1479
+ ```
1480
+
1481
+ Every file is written atomically and readable only by its owner. A file the Kit
1482
+ cannot read is refused rather than treated as a service that never captured.
1483
+ Disabling a service or an instance leaves its state in place, so re-enabling
1484
+ resumes from the stored cursor instead of importing again.
1485
+
1486
+ ```sh
1487
+ kit source reset acme --house https://agents.house # every service
1488
+ kit source reset acme notes --house https://agents.house # one service
1489
+ ```
1490
+
1491
+ Reset is the only deletion of captured state. It removes the named checkpoints
1492
+ and their outstanding retained references so the next capture restarts exactly at
1493
+ the configured backfill boundary. It takes the same
1494
+ Environment control lease a capture takes, so it cannot delete a checkpoint a
1495
+ running capture is about to write back.
1496
+
1497
+ ## Host capacity
1498
+
1499
+ An owned Linux host can report what it actually has, so its owner reads real
1500
+ capacity instead of guessing from Run counts. Declare the host section:
1501
+
1502
+ ```json
1503
+ "host": {
1504
+ "workspace": "/var/lib/house-kit/workspaces",
1505
+ "maxima": {
1506
+ "cpu_millicores": 2000,
1507
+ "memory_bytes": 4294967296,
1508
+ "disk_bytes": 53687091200
1509
+ },
1510
+ "image": null
1511
+ }
1512
+ ```
1513
+
1514
+ `image` is the workspace image this host runs persistent container Environments
1515
+ from, and is only read by `kit workspace host` below; leave it `null` on a host
1516
+ that reports capacity and hosts no container. `workspace` is the absolute path of the persistent workspace directory; the
1517
+ report measures the filesystem holding it, not the root filesystem by
1518
+ assumption. `maxima` are the ceilings this host is configured to apply to
1519
+ Kit-managed workloads, and House keeps them apart from what it measured. Declare
1520
+ `"maxima": null` when this host configures no ceiling; an empty object is
1521
+ refused rather than read as no limit. A ceiling is a shared ceiling, never a
1522
+ reservation, and House infers no supported Agent count from any of these values.
1523
+
1524
+ ```sh
1525
+ kit host report --house https://agents.house
1526
+ ```
1527
+
1528
+ The command reads `/proc/stat` twice a second apart, reads `/proc/meminfo`, and
1529
+ asks the kernel for the workspace filesystem's block counts. It sends the host
1530
+ totals, what is in use, and what is available for each of processors, memory and
1531
+ that filesystem, with the instant it measured them. Nothing else is collected:
1532
+ no process list, no command line, no environment value, and no credential. A
1533
+ native source this Kit cannot read or parse refuses as
1534
+ `kit_host_unmeasurable` and sends nothing, because a host that cannot be
1535
+ measured is unknown to its owner rather than empty.
1536
+
1537
+ The report is one observation, not a stream. Run it on a timer the host owner
1538
+ owns:
1539
+
1540
+ ```
1541
+ [Unit]
1542
+ Description=Report measured host capacity to House
1543
+
1544
+ [Service]
1545
+ Type=oneshot
1546
+ User=house-kit
1547
+ ExecStart=/usr/bin/kit host report --house https://agents.house
1548
+ ```
1549
+
1550
+ ```
1551
+ [Timer]
1552
+ OnBootSec=1min
1553
+ OnUnitActiveSec=5min
1554
+ ```
1555
+
1556
+ House reads an observation as fresh only while three instants - the one this
1557
+ Kit says it measured, the one House received the report, and now - all fall
1558
+ inside one fifteen-minute window. A host that stops reporting reads as aged from
1559
+ then on, a report that was already stale when it arrived reads as aged
1560
+ immediately, and a host that never reported reads as unknown; none of them ever
1561
+ reads as a measured zero. Keep this host's clock in step with real time - a
1562
+ clock more than fifteen minutes out of step in either direction makes every
1563
+ report it sends read as aged, which is the honest answer House can give about a
1564
+ reading it cannot date, and dating a reading into the future buys no freshness
1565
+ because it widens the same window. A smaller skew is not free either: it is
1566
+ spent from the same fifteen minutes, so a clock a few minutes out of step leaves
1567
+ the timer above correspondingly less room to miss a run before House reads this
1568
+ host as aged.
1569
+
1570
+ ## Migrating authored text and linked originals
1571
+
1572
+ `kit migrate` runs under the Environment User's control lease. Its module
1573
+ category is `migrator`, declared in the required `migrators` array in `kit.json`
1574
+ (use `"migrators": []` when none are installed):
1575
+
1576
+ ```json
1577
+ {
1578
+ "id": "my-import",
1579
+ "category": "migrator",
1580
+ "command": ["/home/ada/tools/my-import"],
1581
+ "timeout_ms": 600000,
1582
+ "environment": { "EXPORT_REGION": "eu" },
1583
+ "secrets": { "EXPORT_TOKEN": { "file": "/run/secrets/export-token" } }
1584
+ }
1585
+ ```
1586
+
1587
+ The example is a user-installed module. Kit ships no concrete external-system
1588
+ migrator. Secret references resolve only in the Environment and are passed only
1589
+ to that module's process environment. The module receives neither House's
1590
+ credential nor its lease. Module output and stderr are not published or echoed;
1591
+ a module must never write an external credential into its emitted content,
1592
+ manifest, URLs, or attachments.
1593
+
1594
+ ```sh
1595
+ kit migrate emit my-import --package /home/ada/exports/project
1596
+ kit migrate mount /home/ada/exports/project --house https://agents.house \
1597
+ --subtree /rooms/work/library/project
1598
+ # Or emit and mount in one maintained mount session:
1599
+ kit migrate run my-import --package /home/ada/exports/another-project \
1600
+ --house https://agents.house --subtree /private/library/another-project
1601
+ ```
1602
+
1603
+ Emission refuses an existing output directory. The module reads one JSON line
1604
+ on stdin: `{"contract":"kit-migrator/1","migrator":"my-import","package":"/absolute/output"}`.
1605
+ It must finish writing this tree before exiting successfully:
1606
+
1607
+ ```text
1608
+ project/
1609
+ migrator.json
1610
+ model/note.md # optional independently prepared type definition
1611
+ content/page.md # UTF-8 authored Markdown
1612
+ attachments/image.png # exact downloaded original bytes
1613
+ attachments/notes.txt
1614
+ ```
1615
+
1616
+ The manifest is closed and versioned:
1617
+
1618
+ ```json
1619
+ {
1620
+ "contract": "kit-migrator/1",
1621
+ "category": "migrator",
1622
+ "migrator": "my-import",
1623
+ "attachments": [
1624
+ {
1625
+ "path": "image.png",
1626
+ "source": "https://export.example/image.png",
1627
+ "media_type": "image/png",
1628
+ "download": "downloaded",
1629
+ "referrers": ["page.md"]
1630
+ },
1631
+ {
1632
+ "source": "https://export.example/unavailable",
1633
+ "download": "unavailable",
1634
+ "failure": "not_found",
1635
+ "referrers": ["page.md"]
1636
+ }
1637
+ ]
1638
+ }
1639
+ ```
1640
+
1641
+ `path` is relative to `attachments/`; `referrers` are relative to `content/`.
1642
+ `name` optionally supplies the attachment's file name. Downloaded entries require a
1643
+ local path and forbid `failure`. Unavailable or skipped entries require
1644
+ `failure` and forbid a local path. Every downloaded attachment must be linked
1645
+ from a declared referring document, for example
1646
+ `![Image](../attachments/image.png)` in `content/page.md`. Unavailable ordinary
1647
+ Markdown URLs remain visible; they do not satisfy required typed references.
1648
+ Only Markdown enters the Library. Symlinks, invalid UTF-8, binary content, type
1649
+ aliases under `content/types/`, and generated `_provenance` are refused.
1650
+ `content/MIGRATION.md` is reserved for Kit's report.
1651
+
1652
+ Before creating the text subtree, Kit checks the House-declared MD model version
1653
+ against its exact `@agentshouse/mdmodel` dependency, which rewrites the
1654
+ package's links. House alone validates the resulting Library: a mount it
1655
+ refuses answers with House's own file path, field, rule and version, and no
1656
+ content lands. Definitions in `model/` require their own write
1657
+ grant at `/rooms/<handle>/settings/model/<type>.md` or
1658
+ `/private/settings/model/<type>.md`; a content write grant cannot prepare them.
1659
+ Kit adopts an identical existing definition and refuses a different one.
1660
+ Preparation uses the existing authored batch producer before the empty-subtree
1661
+ mount, so a missing model grant leaves no partial content tree. Independently
1662
+ prepared model definitions survive a later content-mount refusal or unmount.
1663
+
1664
+ The atomic text mount includes a report with pending transfer rows. Kit then
1665
+ uploads each downloaded file once as an attachment to the mount's Room or
1666
+ Private Room, independently retrieves and compares the saved bytes, and only
1667
+ after that proof succeeds rewrites every link to it, in every referring
1668
+ document, to its attachment reference, such as `at_1x.q`. The reference is an
1669
+ identity House resolves, not an HTTP download URL. A link's query and fragment
1670
+ are kept after the reference, as in `at_1x.q#page=3`. Failed or unverified
1671
+ uploads remain source links in the authored text; their failure details appear
1672
+ only in the report. Use `kit attachment get --attachment <at_ref> --room
1673
+ <room-ref> --output <file>` (or `--private`) to retrieve the exact bytes. Binary
1674
+ bytes never enter the text replica. Migrators create no Capture, Source
1675
+ instance, or provenance.
1676
+
1677
+ The mounted `MIGRATION.md` separates local downloads from saved, pending,
1678
+ failed, and skipped attachment uploads, with source URLs, referring documents
1679
+ and the attachment reference of each saved upload.
1680
+ `MIGRATION.json` beside the emitted package preserves local pending and final
1681
+ outcomes. A failed upload leaves usable mounted text and returns a nonzero
1682
+ exit status. A failure to publish the final report also returns nonzero and
1683
+ leaves the pending mounted report plus the local outcomes. Re-migration is
1684
+ explicit unmount followed by mount.
1685
+
1686
+ ```sh
1687
+ kit migrate unmount /rooms/work/library/project --house https://agents.house
1688
+ kit migrate unmount /rooms/work/library/project --house https://agents.house \
1689
+ --confirm /rooms/work/library/project
1690
+ ```
1691
+
1692
+ The first command shows the subtree destruction summary and removes nothing.
1693
+ Confirmation must repeat the exact subtree. Kit checks discovered write/delete
1694
+ authority and protected paths for every selected target, then makes House's one
1695
+ unmount call. House rechecks authority and subtree state at commit. Each
1696
+ command requires the ordinary available Kit control lease; an already held lease
1697
+ is refused by the existing Kit rail.
1698
+
1699
+ ### Linux Compose validation
1700
+
1701
+ The test service uses the host Docker daemon, loop device availability and a
1702
+ shared `/tmp` mount because `npm test` includes Kit's real workspace-volume
1703
+ proof. It runs with an init process, because the supervised-path proofs kill a
1704
+ tmux pane whose adapter is then orphaned and has to be reaped rather than left
1705
+ as a zombie. Set `KIT_TEST_UID`, `KIT_TEST_GID`, and `KIT_TEST_DOCKER_GID` for
1706
+ the host when they differ from 1001, 1001, and 112. It starts only test-owned
1707
+ workspaces.
1708
+
1709
+ ```sh
1710
+ docker compose -f test/compose.yaml build kit
1711
+ docker compose -f test/compose.yaml run --rm kit npm test
1712
+ docker compose -f test/compose.yaml run --rm \
1713
+ -e HOUSE_KIT_GOOGLE_OAUTH_CLIENT_ID=kit-client.apps.googleusercontent.com \
1714
+ -e HOUSE_KIT_GOOGLE_OAUTH_CLIENT_SECRET=GOCSPX-kit-installed-app \
1715
+ kit npm run build
1716
+ ```
1717
+
1718
+ `test/pair/run.sh` boots real Core, Auth and PostgreSQL and runs the installed
1719
+ package against them. `HOUSE_CORE_ROOT` names the Core checkout under proof and
1720
+ `HOUSE_AUTH_ROOT` names an Auth checkout; the run exports the exact Auth revision
1721
+ Core's own `scripts/auth-pair-source.mjs` pins into `tmp/auth-pinned` and pairs
1722
+ that revision rather than whatever the checkout's working tree carries. Set
1723
+ `HOUSE_AUTH_REVISION` to name another revision when Core's pin trails the Auth
1724
+ contract that Core revision already requires. Both producer databases are
1725
+ dropped and made again before either migrates, so a run never inherits the
1726
+ schema another Core revision left behind. Every run prints the three revisions
1727
+ it paired and writes them into each evidence document.
1728
+
1729
+ The installed fixture in `test/fixtures/migrator.mjs` uses only local material
1730
+ and is excluded from the published package. Its paired proof runs from Kit:
1731
+
1732
+ ```sh
1733
+ HOUSE_CORE_ROOT=<committed-core-snapshot> HOUSE_AUTH_ROOT=<committed-auth-snapshot> \
1734
+ KIT_PAIR_SPECS=/kit/test/pair/migrator.vitest.ts bash test/pair/run.sh
1735
+ ```
1736
+
1737
+ The pair boots real Core, Auth, and PostgreSQL. It covers Room and private
1738
+ mounts, separately authorized models, House's typed refusal, exact
1739
+ attachment retrieval, failed storage, mounted reports, occupied mounts and the
1740
+ confirmed unmount. As in the other installed-CLI pair tests, the fixture expires
1741
+ the previous control lease before starting the next isolated command. Evidence
1742
+ is written to `tmp/delivery-evidence/migrator-pair.json` with producer revisions
1743
+ and the installed package digest.
1744
+
1745
+ The retained-original consumers have their own paired proofs:
1746
+
1747
+ ```sh
1748
+ HOUSE_CORE_ROOT=<core checkout> HOUSE_AUTH_ROOT=<auth checkout> \
1749
+ KIT_PAIR_SPECS="/kit/test/pair/source-originals.vitest.ts \
1750
+ /kit/test/pair/conversation-originals.vitest.ts \
1751
+ /kit/test/pair/conversation-house-storage.vitest.ts" bash test/pair/run.sh
1752
+ ```
1753
+
1754
+ `source-originals` drives a controlled local Source adapter through one
1755
+ installed Kit: it proves that the connector receives its own authorization,
1756
+ selection and cursor and never a House credential, that an unreachable House
1757
+ leaves the checkpoint where it was and converges on reconnect, that two Room
1758
+ owners hold independently owned retained copies of one delivery while one
1759
+ owner's two Rooms share a Record, that a failed save is truthful and a later
1760
+ byte retry publishes no second Capture and no substitute version, that an
1761
+ unauthorized Room destination stays outstanding while acquisition health stays
1762
+ its own, that an explicit backfill bound still holds in the installed package,
1763
+ and that interrupting the Environment stops the receiver without advancing the
1764
+ checkpoint or being woken again. After the Environment's authority is revoked
1765
+ its next capture is refused.
1766
+
1767
+ `conversation-originals` and `conversation-house-storage` drive the same
1768
+ installed personal-bot gateway and Worker over House storage. Both prove an
1769
+ incoming accepted attachment retained through the shared byte client
1770
+ independently of transport, a failed save and its retry leaving every accepted
1771
+ message identity unchanged, the Run settling and its temporary media cache being
1772
+ disposed, the owner downloading the exact original through House afterwards, and
1773
+ the Environment's next protected act being refused once its authority is gone.
1774
+ A file a Session sends to its owner's conversation through `house
1775
+ upload_attachment` is proven by Core's pair lane. Evidence is written
1776
+ to `tmp/delivery-evidence/source-originals-pair.json`,
1777
+ `conversation-originals-pair.json` and `conversation-house-storage-pair.json`.