@intentic/machine 1.344.0 → 1.346.0

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 (231) hide show
  1. package/README.md +185 -154
  2. package/dist/autostart/autostart.d.ts +0 -1
  3. package/dist/autostart/autostart.d.ts.map +1 -1
  4. package/dist/autostart/autostart.js +3 -9
  5. package/dist/autostart/autostart.js.map +1 -1
  6. package/dist/commands.d.ts.map +1 -1
  7. package/dist/commands.js +4 -21
  8. package/dist/commands.js.map +1 -1
  9. package/dist/device/commands.d.ts +8 -2
  10. package/dist/device/commands.d.ts.map +1 -1
  11. package/dist/device/commands.js +23 -9
  12. package/dist/device/commands.js.map +1 -1
  13. package/dist/device/connection.d.ts.map +1 -1
  14. package/dist/device/connection.js +27 -9
  15. package/dist/device/connection.js.map +1 -1
  16. package/dist/device/grant.d.ts +244 -1
  17. package/dist/device/grant.d.ts.map +1 -1
  18. package/dist/device/mcp.d.ts.map +1 -1
  19. package/dist/device/mcp.js +25 -8
  20. package/dist/device/mcp.js.map +1 -1
  21. package/dist/device/router.d.ts +469 -226
  22. package/dist/device/router.d.ts.map +1 -1
  23. package/dist/device/router.js +18 -10
  24. package/dist/device/router.js.map +1 -1
  25. package/dist/device/sandbox-rounds/keeper.d.ts +4 -1
  26. package/dist/device/sandbox-rounds/keeper.d.ts.map +1 -1
  27. package/dist/device/sandbox-rounds/keeper.js +13 -7
  28. package/dist/device/sandbox-rounds/keeper.js.map +1 -1
  29. package/dist/device/sandbox-rounds/sleep.d.ts +15 -2
  30. package/dist/device/sandbox-rounds/sleep.d.ts.map +1 -1
  31. package/dist/device/sandbox-rounds/sleep.js +68 -13
  32. package/dist/device/sandbox-rounds/sleep.js.map +1 -1
  33. package/dist/device/tools/agent.d.ts +15 -0
  34. package/dist/device/tools/agent.d.ts.map +1 -1
  35. package/dist/device/tools/agent.js +75 -19
  36. package/dist/device/tools/agent.js.map +1 -1
  37. package/dist/device/tools/android.d.ts +1 -1
  38. package/dist/device/tools/android.d.ts.map +1 -1
  39. package/dist/device/tools/android.js +17 -8
  40. package/dist/device/tools/android.js.map +1 -1
  41. package/dist/device/tools/browser.d.ts.map +1 -1
  42. package/dist/device/tools/browser.js +3 -5
  43. package/dist/device/tools/browser.js.map +1 -1
  44. package/dist/device/tools/ic-binary.d.ts +2 -1
  45. package/dist/device/tools/ic-binary.d.ts.map +1 -1
  46. package/dist/device/tools/ic-binary.js +34 -4
  47. package/dist/device/tools/ic-binary.js.map +1 -1
  48. package/dist/device/tools/os/settings.js +3 -3
  49. package/dist/device/tools/os/settings.js.map +1 -1
  50. package/dist/device/tools/programs/app-wait-tools.d.ts +12 -0
  51. package/dist/device/tools/programs/app-wait-tools.d.ts.map +1 -0
  52. package/dist/device/tools/programs/app-wait-tools.js +60 -0
  53. package/dist/device/tools/programs/app-wait-tools.js.map +1 -0
  54. package/dist/device/tools/programs/app-wait.d.ts +19 -0
  55. package/dist/device/tools/programs/app-wait.d.ts.map +1 -0
  56. package/dist/device/tools/programs/app-wait.js +89 -0
  57. package/dist/device/tools/programs/app-wait.js.map +1 -0
  58. package/dist/device/tools/programs/file-changes.d.ts +2 -0
  59. package/dist/device/tools/programs/file-changes.d.ts.map +1 -0
  60. package/dist/device/tools/programs/file-changes.js +19 -0
  61. package/dist/device/tools/programs/file-changes.js.map +1 -0
  62. package/dist/device/tools/programs/programs.d.ts +12 -0
  63. package/dist/device/tools/programs/programs.d.ts.map +1 -1
  64. package/dist/device/tools/programs/programs.js +13 -12
  65. package/dist/device/tools/programs/programs.js.map +1 -1
  66. package/dist/device/tools/programs/stop-all.d.ts +10 -0
  67. package/dist/device/tools/programs/stop-all.d.ts.map +1 -0
  68. package/dist/device/tools/programs/stop-all.js +52 -0
  69. package/dist/device/tools/programs/stop-all.js.map +1 -0
  70. package/dist/device/tools/sandboxes.d.ts +2 -0
  71. package/dist/device/tools/sandboxes.d.ts.map +1 -1
  72. package/dist/device/tools/sandboxes.js +11 -3
  73. package/dist/device/tools/sandboxes.js.map +1 -1
  74. package/dist/device/tools/shell.d.ts +1 -1
  75. package/dist/device/tools/shell.d.ts.map +1 -1
  76. package/dist/install.d.ts +15 -1
  77. package/dist/install.d.ts.map +1 -1
  78. package/dist/install.js +35 -6
  79. package/dist/install.js.map +1 -1
  80. package/dist/keyring/app-commands.d.ts +15 -0
  81. package/dist/keyring/app-commands.d.ts.map +1 -0
  82. package/dist/keyring/app-commands.js +247 -0
  83. package/dist/keyring/app-commands.js.map +1 -0
  84. package/dist/keyring/commands.d.ts +1 -1
  85. package/dist/keyring/commands.d.ts.map +1 -1
  86. package/dist/keyring/commands.js +26 -32
  87. package/dist/keyring/commands.js.map +1 -1
  88. package/dist/keyring/git-helper.d.ts +8 -0
  89. package/dist/keyring/git-helper.d.ts.map +1 -0
  90. package/dist/keyring/git-helper.js +60 -0
  91. package/dist/keyring/git-helper.js.map +1 -0
  92. package/dist/keyring/keyring-status.d.ts +8 -1
  93. package/dist/keyring/keyring-status.d.ts.map +1 -1
  94. package/dist/keyring/keyring-status.js +39 -4
  95. package/dist/keyring/keyring-status.js.map +1 -1
  96. package/dist/keyring/native-host-install.d.ts +28 -0
  97. package/dist/keyring/native-host-install.d.ts.map +1 -0
  98. package/dist/keyring/native-host-install.js +99 -0
  99. package/dist/keyring/native-host-install.js.map +1 -0
  100. package/dist/keyring/relay.d.ts +5 -1
  101. package/dist/keyring/relay.d.ts.map +1 -1
  102. package/dist/keyring/relay.js +16 -0
  103. package/dist/keyring/relay.js.map +1 -1
  104. package/dist/keyring/sandbox-push.d.ts +4 -0
  105. package/dist/keyring/sandbox-push.d.ts.map +1 -0
  106. package/dist/keyring/sandbox-push.js +21 -0
  107. package/dist/keyring/sandbox-push.js.map +1 -0
  108. package/dist/keyring/service.d.ts +11 -5
  109. package/dist/keyring/service.d.ts.map +1 -1
  110. package/dist/keyring/service.js +34 -51
  111. package/dist/keyring/service.js.map +1 -1
  112. package/dist/resident/resident.d.ts +1 -0
  113. package/dist/resident/resident.d.ts.map +1 -1
  114. package/dist/resident/resident.js +9 -1
  115. package/dist/resident/resident.js.map +1 -1
  116. package/dist/status.d.ts.map +1 -1
  117. package/dist/status.js +1 -8
  118. package/dist/status.js.map +1 -1
  119. package/dist/sync/binary/mutagen-binary.d.ts +4 -0
  120. package/dist/sync/binary/mutagen-binary.d.ts.map +1 -0
  121. package/dist/sync/binary/mutagen-binary.js +65 -0
  122. package/dist/sync/binary/mutagen-binary.js.map +1 -0
  123. package/dist/sync/commands.d.ts +1 -9
  124. package/dist/sync/commands.d.ts.map +1 -1
  125. package/dist/sync/commands.js +49 -84
  126. package/dist/sync/commands.js.map +1 -1
  127. package/dist/sync/config.d.ts +21 -25
  128. package/dist/sync/config.d.ts.map +1 -1
  129. package/dist/sync/config.js +76 -35
  130. package/dist/sync/config.js.map +1 -1
  131. package/dist/sync/endpoint.d.ts +1 -1
  132. package/dist/sync/endpoint.d.ts.map +1 -1
  133. package/dist/sync/endpoint.js +2 -3
  134. package/dist/sync/endpoint.js.map +1 -1
  135. package/dist/sync/folders.d.ts.map +1 -1
  136. package/dist/sync/folders.js.map +1 -1
  137. package/dist/sync/forget-command.js +1 -1
  138. package/dist/sync/forget-command.js.map +1 -1
  139. package/dist/sync/gone-watch.d.ts +1 -1
  140. package/dist/sync/gone-watch.d.ts.map +1 -1
  141. package/dist/sync/gone-watch.js +16 -40
  142. package/dist/sync/gone-watch.js.map +1 -1
  143. package/dist/sync/holds.d.ts +30 -0
  144. package/dist/sync/holds.d.ts.map +1 -0
  145. package/dist/sync/holds.js +117 -0
  146. package/dist/sync/holds.js.map +1 -0
  147. package/dist/sync/low-disk.d.ts +2 -9
  148. package/dist/sync/low-disk.d.ts.map +1 -1
  149. package/dist/sync/low-disk.js +10 -45
  150. package/dist/sync/low-disk.js.map +1 -1
  151. package/dist/sync/mirror.d.ts +2 -3
  152. package/dist/sync/mirror.d.ts.map +1 -1
  153. package/dist/sync/mirror.js +61 -160
  154. package/dist/sync/mirror.js.map +1 -1
  155. package/dist/sync/mutagen-env.d.ts +1 -3
  156. package/dist/sync/mutagen-env.d.ts.map +1 -1
  157. package/dist/sync/mutagen-env.js +2 -12
  158. package/dist/sync/mutagen-env.js.map +1 -1
  159. package/dist/sync/mutagen-move.d.ts +3 -1
  160. package/dist/sync/mutagen-move.d.ts.map +1 -1
  161. package/dist/sync/mutagen-move.js +31 -22
  162. package/dist/sync/mutagen-move.js.map +1 -1
  163. package/dist/sync/mutagen.d.ts +6 -12
  164. package/dist/sync/mutagen.d.ts.map +1 -1
  165. package/dist/sync/mutagen.js +17 -144
  166. package/dist/sync/mutagen.js.map +1 -1
  167. package/dist/sync/project/project-commands.d.ts +0 -3
  168. package/dist/sync/project/project-commands.d.ts.map +1 -1
  169. package/dist/sync/project/project-commands.js +5 -3
  170. package/dist/sync/project/project-commands.js.map +1 -1
  171. package/dist/sync/project/project-delivery.d.ts.map +1 -1
  172. package/dist/sync/project/project-delivery.js +5 -4
  173. package/dist/sync/project/project-delivery.js.map +1 -1
  174. package/dist/sync/project/project-transfer.d.ts +4 -0
  175. package/dist/sync/project/project-transfer.d.ts.map +1 -1
  176. package/dist/sync/project/project-transfer.js +9 -2
  177. package/dist/sync/project/project-transfer.js.map +1 -1
  178. package/dist/sync/report.d.ts.map +1 -1
  179. package/dist/sync/report.js +18 -11
  180. package/dist/sync/report.js.map +1 -1
  181. package/dist/sync/retire.d.ts.map +1 -1
  182. package/dist/sync/retire.js +1 -2
  183. package/dist/sync/retire.js.map +1 -1
  184. package/dist/sync/tunnel.d.ts +4 -3
  185. package/dist/sync/tunnel.d.ts.map +1 -1
  186. package/dist/sync/tunnel.js +107 -42
  187. package/dist/sync/tunnel.js.map +1 -1
  188. package/dist/sync/uninstall.d.ts.map +1 -1
  189. package/dist/sync/uninstall.js +4 -3
  190. package/dist/sync/uninstall.js.map +1 -1
  191. package/dist/sync/wake.d.ts +3 -0
  192. package/dist/sync/wake.d.ts.map +1 -0
  193. package/dist/sync/wake.js +43 -0
  194. package/dist/sync/wake.js.map +1 -0
  195. package/dist/uninstall/command.d.ts +3 -0
  196. package/dist/uninstall/command.d.ts.map +1 -0
  197. package/dist/uninstall/command.js +40 -0
  198. package/dist/uninstall/command.js.map +1 -0
  199. package/dist/uninstall/distros.d.ts +8 -0
  200. package/dist/uninstall/distros.d.ts.map +1 -0
  201. package/dist/uninstall/distros.js +103 -0
  202. package/dist/uninstall/distros.js.map +1 -0
  203. package/dist/uninstall/plan.d.ts +23 -0
  204. package/dist/uninstall/plan.d.ts.map +1 -0
  205. package/dist/uninstall/plan.js +71 -0
  206. package/dist/uninstall/plan.js.map +1 -0
  207. package/dist/uninstall/purge.d.ts +3 -0
  208. package/dist/uninstall/purge.d.ts.map +1 -0
  209. package/dist/uninstall/purge.js +79 -0
  210. package/dist/uninstall/purge.js.map +1 -0
  211. package/dist/uninstall/steps.d.ts +12 -0
  212. package/dist/uninstall/steps.d.ts.map +1 -0
  213. package/dist/uninstall/steps.js +200 -0
  214. package/dist/uninstall/steps.js.map +1 -0
  215. package/dist/uninstall/sweep.d.ts +12 -0
  216. package/dist/uninstall/sweep.d.ts.map +1 -0
  217. package/dist/uninstall/sweep.js +90 -0
  218. package/dist/uninstall/sweep.js.map +1 -0
  219. package/dist/uninstall/tally.d.ts +14 -0
  220. package/dist/uninstall/tally.d.ts.map +1 -0
  221. package/dist/uninstall/tally.js +25 -0
  222. package/dist/uninstall/tally.js.map +1 -0
  223. package/package.json +9 -9
  224. package/dist/sync/attach-commands.d.ts +0 -39
  225. package/dist/sync/attach-commands.d.ts.map +0 -1
  226. package/dist/sync/attach-commands.js +0 -163
  227. package/dist/sync/attach-commands.js.map +0 -1
  228. package/dist/sync/swap-pause.d.ts +0 -10
  229. package/dist/sync/swap-pause.d.ts.map +0 -1
  230. package/dist/sync/swap-pause.js +0 -29
  231. package/dist/sync/swap-pause.js.map +0 -1
package/README.md CHANGED
@@ -108,8 +108,12 @@ flowchart LR
108
108
  sandbox's, in this environment or another of this PC's (see [What folder sync owns](#what-folder-sync-owns)), and a
109
109
  set-up-again that would change where a paired sandbox's folder syncs. It is **copy-first** unless
110
110
  its owner opted into two-way: see [Copy-first projects](#copy-first-projects).
111
- Attaching the same folder again keeps its session and the watcher's pause and outage records, so an automatic pause
112
- still resumes when its reason ends. The host's enrollment and transport settings are refreshed.
111
+ Setting the same folder up again keeps its session and every fact that holds its sync (see
112
+ [File sync runs or holds still by one rule](#file-sync-runs-or-holds-still-by-one-rule)). A project folder also takes
113
+ its agents' landed work by itself (`deliver: "auto"`, see [Delivering landed work](#delivering-landed-work)), and is
114
+ refused where it is a whole disk, a home folder or one holding it, or the system's own (`folderRefusal` in
115
+ [`sync/folders.ts`](src/sync/folders.ts), as the desktop app refuses them). `setup` wakes the resident watcher
116
+ (`sync/wake.ts`) once it has recorded the pairing, so the session is made at once rather than at the next poll.
113
117
  - **Through Docker** ([`sync/endpoint.ts`](src/sync/endpoint.ts)): with `setup --transport auto`, the default, a
114
118
  project's sandbox is reached through this machine's own Docker engine when the container `ic` named after the
115
119
  pairing URL's slug is running there with that URL as its `SANDBOX_PUBLIC_URL`. Anything else stays on ssh: a hosted
@@ -124,10 +128,10 @@ flowchart LR
124
128
  - (2026-10-05) The transport is no longer decided once: the container is asked after on every prepare and every
125
129
  minute (`checkContainers` in [`sync/gone-watch.ts`](src/sync/gone-watch.ts)). A stopped container, or an engine that
126
130
  does not answer, changes nothing. A container the engine no longer holds is looked up in ic: still listed (a swap
127
- moving it) changes nothing; in ic's trash pauses the folder's sync with that reason. When the sandbox still answers
128
- at its address, whether ic holds it nowhere or cannot say, it lives elsewhere now and the pairing moves onto ssh
129
- (paused instead without an enrollment to ride); in neither and silent, the sandbox is gone (below); otherwise its
130
- sync is paused until the container is back.
131
+ moving it) changes nothing; in ic's trash holds the folder's sync still (`containerAway: "trashed"`). When the
132
+ sandbox still answers at its address, whether ic holds it nowhere or cannot say, it lives elsewhere now and the
133
+ pairing moves onto ssh (held instead without an enrollment to ride); in neither and silent, the sandbox is gone
134
+ (below); otherwise its sync holds still until the container is back (`containerAway: "missing"`).
131
135
 
132
136
  (2026-10-01) Measured on Docker Desktop from Windows and from WSL: a session was up in 2–4 s, against up to 90 s
133
137
  through the tunnel. Bind-mounting the folder into the container was rejected: a land or an agent's `rm -rf` would
@@ -141,10 +145,11 @@ flowchart LR
141
145
  (`settlesFirst` in [`sync/mutagen.ts`](src/sync/mutagen.ts) says why for each). Every session and forward is created
142
146
  with three Mutagen labels, `intentic-owner=<machineId>-<environment>`, `intentic-sandbox=<sanitized id>` and
143
147
  `intentic-kind=sync|state|forward`; see [What folder sync owns](#what-folder-sync-owns).
144
- - **This computer's own sandbox takes folders** (see [Folders attached to this computer's sandbox](#folders-attached-to-this-computers-sandbox)):
145
- `sync setup --projects-host` enrolls it with no folder of its own, and each folder the owner opens attaches to it as
146
- `/work/<name>` (`sync attach`), copy-first, with the land of a conversation written back into it by itself
147
- (`deliverProject`).
148
+ - **Each folder has a sandbox of its own** (`sync setup --project`, see [Copy-first projects](#copy-first-projects)):
149
+ copied one way into its `/work/<name>`, with the land of a conversation written back into it by itself
150
+ (`deliverProject`, see [Delivering landed work](#delivering-landed-work)). _(2026-10-21) Folders used to attach to one
151
+ sandbox per computer (`sync setup --projects-host`, `sync attach`); that went, see
152
+ [One sandbox per computer, retired](#one-sandbox-per-computer-retired)._
148
153
  - **What file sync costs this device is cycles, and the sandbox's side decides how many.** Mutagen's agent in the
149
154
  sandbox has no recursive watcher, and both sessions force it to poll (`--watch-mode-<side> force-poll`): every 2 s
150
155
  for the workspace, which is how late an agent's edit reaches the folder, and every 60 s for the state backup, which
@@ -170,21 +175,25 @@ flowchart LR
170
175
  host key the enrollment carries when it carries one (a sandbox reads its sshd's public key off its history volume and
171
176
  answers with it, `SyncEnrollmentAnswerSchema` in the contract), else with nothing, so `accept-new` records the key
172
177
  the sandbox presents now, as it does for an older sandbox. Outside an enrollment a changed key is still refused. A sandbox whose ports poll has failed for ten
173
- minutes has its forwards taken off localhost, and they come back with its first answer; after an hour its sessions
174
- are paused, an hour counted from `unreachableSince` in `sync.json` (2026-10-05: it was counted in memory, so an agent
175
- restarted more often than hourly never paused a dead sandbox's sessions). A sandbox that no longer exists is another
178
+ minutes has its forwards taken off localhost, and they come back with its first answer; after an hour its sync holds
179
+ still, an hour counted from `unreachableSince` in `sync.json` (2026-10-05: it was counted in memory, so an agent
180
+ restarted more often than hourly never held a dead sandbox's sessions). A sandbox that no longer exists is another
176
181
  matter: see [Sandboxes that are gone](#sandboxes-that-are-gone). `sync uninstall` ends every session and forward of
177
182
  this agent's and stops its Mutagen daemon, which serves this agent alone (below), starting it first if it was down so
178
- every session is really ended; the synced folders keep their files. `sync uninstall --folders` (what the desktop app's
179
- uninstaller runs) unpairs only the pairings that sync a folder here: mirror-only pairings and the projects host keep
180
- their ports and their enrollments.
183
+ every session is really ended; the synced folders keep their files. It runs the agent's own Mutagen in `bin/` and
184
+ downloads none: without that copy the session steps are skipped (2026-10-18). `sync uninstall --folders` (what the desktop app's
185
+ uninstaller runs) unpairs only the pairings that sync a folder here: mirror-only pairings keep their ports and their
186
+ enrollments.
181
187
  - Symbolic links travel only where the device can create them ([`sync/symlinks.ts`](src/sync/symlinks.ts)). A
182
188
  Windows PC without Developer Mode refuses every link, and Mutagen would try again on every cycle, so its sessions
183
189
  use `--symlink-mode ignore`. Turning Developer Mode on brings them back at the agent's next start.
184
190
  - **Which computer this is** ([`machine-id.ts`](src/machine-id.ts)): a `machineId` minted once at install and kept
185
- in `~/.intentic/machine/machine-id`, sent in the connect-time facts, the sync report and the sync enrollment. The
186
- Windows side hands its own to the agent it starts in each distro, so every OS install of one PC answers with one id;
187
- a sandbox joins enrollments, sync enrollments and device rows on it, never on a hostname.
191
+ in `~/.intentic/machine/machine-id`, sent in the connect-time facts, the sync report, the sync enrollment and the
192
+ device enrollment (`device setup` sends it with the environment when it redeems the pairing). The Windows side hands
193
+ its own to the agent it starts in each distro, so every OS install of one PC answers with one id; a sandbox joins
194
+ enrollments, sync enrollments and device rows on it, never on a hostname. (2026-10-20: `device setup` used to redeem
195
+ with no body, so the sandbox learned the id only once the agent connected, and the PC showed as two machine cards
196
+ until then.)
188
197
  - The features a device advertises (`set-shape`, `reshape-later`, `rollback-to`, `background-prepare`) are read off
189
198
  the `ic` under it, from that `ic`'s own help (`ic sandbox shape --set` for the first two, `ic sandbox rollback --to`
190
199
  for the third, `ic sandbox prepare --auto` for the fourth), rather than listed beside the code. The agent's own two,
@@ -356,97 +365,35 @@ folded where the platform folds it). With `--json` each prints exactly one JSON
356
365
  back included. Copy-first then carries the restored files to the sandbox, as it does any edit here, so the agent's
357
366
  versions they replace are gone from there too.
358
367
  - One bring-back, restore or delivery runs per folder at a time (`restore/<pairing key>/.operation.pid`, a pid of this
359
- boot, checked alive). A pause an operation made and never lifted (the process killed in between) is recorded in
360
- `restore/<pairing key>/.paused`, and the next command about that folder resumes the session.
361
-
362
- ### Folders attached to this computer's sandbox
363
-
364
- Each computer has one always-available local sandbox, and any number of folders attach to it, each as `/work/<name>`.
365
- A folder used to get a project sandbox of its own (`sync setup --project`, above), which still works.
366
-
367
- **The projects host.** `intentic-machine sync setup --url <sandboxUrl> --pair <token> --projects-host` (with
368
- `--transport` and `--sandbox-id` as for any setup) enrolls the machine sandbox exactly as a setup does, and records a
369
- folderless pairing: `{ sandboxUrl, sandboxId, mode: "sync", syncToken, projectsHost: true, transport?, container? }`.
370
- It has no Mutagen session, no git bridge and no state backup. It holds the sandbox's sync token, and it is the one
371
- pairing that mirrors the sandbox's ports to localhost. It reaches the sandbox through Docker when the sandbox's
372
- container runs on this machine's engine, so its forwards ride `docker://` too. Refused:
373
-
374
- - `--projects-host` with `--dir`, `--remote-dir` or `--project`.
375
- - Turning a sandbox that syncs a folder into the projects host, and the other way round. Unpair first.
376
- - An enrollment that comes back ports-only.
377
-
378
- Enrolling again rotates the sandbox's one token per machine key, so the new token is written into every pairing of that
379
- sandbox (`withPairing` in [`sync/config.ts`](src/sync/config.ts)).
380
-
381
- **Attaching and detaching.** Both are local: no enrollment, no network. With `--json` each prints exactly one JSON
382
- object, `{ "ok": false, "error": "<sentence>" }` and exit code 1 on failure, as the copy-first commands do.
383
-
384
- | Command | Success output |
385
- |---|---|
386
- | `sync attach --sandbox-url <url> --dir <folder> --name <name>` | `{ "ok": true, "pairing": "<sandboxId>~<name>", "remoteDir": "/work/<name>", "folder": "<folder as given, absolute>" }` |
387
- | `sync detach --dir <folder>` | `{ "ok": true, "pairing": "<key>", "folder": "<folder>" }` |
388
-
389
- - `attach` finds the projects host whose URL has the same host as `--sandbox-url`. Without one it answers "this
390
- computer's sandbox is not set up for folders yet".
391
- - It refuses:
392
- - a name `isProjectDirName` refuses (the sandbox's reserved names included);
393
- - a folder that is not there;
394
- - a whole disk, the home folder or one holding it, `/var/home` or a whole home in it, and the system's folders (on
395
- Windows, on whichever drive), as the desktop app does ([`folderRefusal`](src/sync/folders.ts)). (2026-10-06) A WSL
396
- distro's folder named from Windows (`\\wsl.localhost\<distro>\…`, `\\wsl$\…`) is held to the distro's rules: its
397
- `/home`, a whole home in it, `/root`, a mounted disk (`/mnt/c`) and its system's folders;
398
- - a folder that is, holds or sits inside any other pairing's, this environment's or (2026-10-05) another environment
399
- of this PC's ([`sync/siblings.ts`](src/sync/siblings.ts), as `setup` does);
400
- - a name already attached for another folder.
401
- - It records `{ key: "<sandboxId>~<name>", sandboxId, sandboxUrl, syncToken (the host's), mode: "sync", localDir,
402
- remoteDir: "/work/<name>", project: true, direction: "to-sandbox", deliver: "auto", transport?, container? }`.
403
- Docker is preferred, as for the host. Attaching the same folder under the same name again keeps the direction its
404
- owner chose. `localDir` is the folder as given, made absolute, as `setup` records one; links are resolved only to
405
- compare folders (2026-10-05: the desktop app finds its folder in `status --json` by the path it passed, and on Fedora
406
- Atomic `/home` is itself a link to `/var/home`, so a link-resolved path would never be found).
407
- - It returns once the pairing is recorded and the resident agent is running. The agent creates the session on its next
408
- pass, and `status --json` lists the folder with its `localDir`, `remoteDir`, `deliver` and `mutagenStatus`.
409
- `mutagenStatus` is absent until the session exists, then Mutagen's own words: `connecting-beta`, `scanning`,
410
- `reconciling`, `staging-beta`, `transitioning`, `saving` while the first copy runs, and `watching` once a whole cycle
411
- has finished. Read the first `watching` as "first copy done": later edits pass through the same words again.
412
- - `detach` removes the pairing first, so the agent recreates nothing, then ends its session and drops its listing record.
413
- Its restore points stay. A folder with a sandbox of its own is refused, with the `sync uninstall` that unpairs it.
414
- - `changes`, `bring-back`, `restore-points`, `restore` and `direction` work on an attached folder as on any project, by
415
- `--dir`.
416
-
417
- **A pairing key.** Every pairing is filed under `pairingKey` = `key ?? sandboxId`. Every pairing made before folders could
418
- attach has no `key`, so its key is its sandbox id and nothing of it moves: its session names, its restore points, its
419
- listing record, its lock. The key names what is a folder's own: the `upsertPairing`/`removePairing`/`set*` writes, the
420
- Mutagen session (`sessionName`), `restore/<key>/` with its lock and pause marker, and `hashes/<key>.json`. The sandbox
421
- id still names what is the sandbox's: the enrollment and its token, the ssh alias and tunnel (one block per sandbox),
422
- the container, the forwards, the swap pause, the report's `sandboxId`. `sync.json` is refused whole for:
423
-
424
- - two pairings under one key;
425
- - an attached key that is not `<sandboxId>~<name>` for its `/work/<name>`;
426
- - a projects host with a folder, remote dir, project flag or key.
427
-
428
- `--sandbox` (pause, resume, mirror, autoheal, uninstall) selects every pairing of the sandbox it names.
429
-
430
- **One poll, one mirror, one report per sandbox** ([`sync/mirror.ts`](src/sync/mirror.ts)):
431
-
432
- - The sandbox's own pairing polls its ports; with no host, the first folder attached to it does. The poll is also how the
433
- sandbox's liveness and a revoked enrollment are noticed. Every other folder follows that answer
434
- (`followSandbox`): paused after an hour of no answer, resumed on the first. A folder never mirrors a port, whatever
435
- `mirrorOff` says. It carries the sandbox's switch only so the report says the same of it.
436
- - A revoked enrollment drops every pairing of the sandbox, since they share its token.
437
- - The report goes to each sandbox once (`reportCarriers`), and `scopedReport` carries all of its pairings, host and
438
- folders, each with `remoteDir`, `projectsHost` and `deliver`: the daemon keeps one report per machine.
439
- - **A folder's first copy waits for its report** (`readyToPrepare`). The daemon makes `/work/<name>` a repository of its
440
- own once a report names it. So a folder whose session does not exist yet gets one only after a report naming it was
441
- taken, tried again each pass otherwise. A daemon with no report route (404) is not waited for.
442
-
443
- (2026-10-05) An attached folder's session is `intentic-<sandbox>--<name>-<8 hex of sha256(name)>`. A sanitized id never
444
- holds `--`, so the name can be no sandbox's session nor its `-state` backup (a folder called `state` included). The hash
445
- keeps `my.app` and `my_app` apart, which sanitize alike. Letting every folder poll was rejected: ten folders would cost
446
- the daemon eleven polls every five seconds, and three rejected polls counted per sandbox would revoke after one tick.
447
-
448
- **Delivering landed work** ([`sync/project/project-delivery.ts`](src/sync/project/project-delivery.ts)). When a conversation's work
449
- lands in `/work/<name>`, the daemon calls `deliverProject` on the device link (`ProjectDeliverySchema` in the contract),
368
+ boot, checked alive). Its pause is a hold like any other (`transferSince`, below): recorded on the pairing before
369
+ the session is paused and lifted as the operation lets go, so the watcher's pass keeps the session paused while it
370
+ writes. (2026-10-21: recorded nowhere, it read as a pause nothing explained, and the next pass resumed the session in
371
+ the middle of the write.) A pause an operation made and never lifted (the process killed in between) is recorded in
372
+ `restore/<pairing key>/.paused`, and the next command about that folder resumes the session; the watcher lets go of
373
+ its hold by itself once the folder's lock names no live process.
374
+
375
+ ### One sandbox per computer, retired
376
+
377
+ (2026-10-21) Each computer used to have one always-available sandbox that every folder attached to as `/work/<name>`:
378
+ `sync setup --projects-host` enrolled it with no folder of its own, and `sync attach` / `sync detach` filed each
379
+ folder under a key of its own (`<sandboxId>~<name>`), polled and reported once per sandbox and announced to the daemon
380
+ before its first copy. The owner retired it: a central sandbox for everything was the wrong shape, and each folder now
381
+ gets a sandbox of its own, made for it (`sync setup --project`, [Copy-first projects](#copy-first-projects)), which
382
+ takes landed work by itself. So there is one pairing per sandbox again, filed under its sandbox id, and `sync.json` is
383
+ refused whole for two pairings of one sandbox, as before.
384
+
385
+ A `sync.json` an older agent wrote is never refused for what that design left in it. Reading leaves out its folderless
386
+ pairing (`projectsHost: true`) and every attached folder (`key`), so nothing syncs them; the watcher's start writes
387
+ the file without them and says each in one line (`retireLegacyPairings` in [`sync/config.ts`](src/sync/config.ts)),
388
+ and the orphan sweep ends their Mutagen sessions and forwards. Nothing is deleted on either side: each folder keeps its
389
+ files, and that sandbox keeps its copies and its agents' history, carrying on as an ordinary sandbox. The sync scripts
390
+ refuse `SYNC_PROJECTS_HOST` from a desktop app older than this, rather than reading it as a folder for that
391
+ sandbox's whole `/work`.
392
+
393
+ ### Delivering landed work
394
+
395
+ ([`sync/project/project-delivery.ts`](src/sync/project/project-delivery.ts)) When a conversation's work lands in a
396
+ project sandbox's `/work/<name>`, the daemon calls `deliverProject` on the device link (`ProjectDeliverySchema` in the contract),
450
397
  and this agent writes the change into the folder. It is a procedure of the link, not an MCP tool, so no agent can call
451
398
  it, and it sits behind no switch of the grant: the folder's own `deliver: "auto"` is the permission. It is logged and
452
399
  appended to the audit file like every call.
@@ -506,11 +453,11 @@ Two witnesses can say a sandbox is gone ([`sync/gone.ts`](src/sync/gone.ts)); ab
506
453
  What follows is the same for both ([`sync/gone-watch.ts`](src/sync/gone-watch.ts)):
507
454
 
508
455
  - **Marked.** Every pairing of the sandbox gets `goneSince` (first heard), `goneCheckedAt` and `goneBy` (`edge` or
509
- `local`) in `sync.json`. Its sessions are paused with the watcher's own marker, `fileSyncPausedFor: "gone"`; a pause
510
- somebody else made is left alone. Its ports come off localhost, its tunnel listener closes, no report is posted to
511
- it, nothing is created for it, and its folders do nothing more. One line says so, with the day it will be retired.
512
- - **Asked again** at most hourly (one ports poll), whatever the outcome. An answer clears all three fields, lifts the
513
- pause, and the next pass puts the ports back. A sandbox ic listed again (restored from its trash) is cleared at once
456
+ `local`) in `sync.json`, which is what holds its file sync still. Its ports come off localhost, its tunnel listener
457
+ closes, no report is posted to it, nothing is created for it, and its folders do nothing more. One line says so, with
458
+ the day it will be retired.
459
+ - **Asked again** at most hourly (one ports poll), whatever the outcome. An answer clears all three fields, which lets
460
+ its sync go, and the next pass puts the ports back. A sandbox ic listed again (restored from its trash) is cleared at once
514
461
  when ic was the witness; the edge's word is withdrawn only by the sandbox answering.
515
462
  - **Retired** once seven days (the trash window) have passed since it was first said to be gone, on a verdict asked
516
463
  within the last hour, so an agent that was off for a week asks once first. Retiring ([`sync/retire.ts`](src/sync/retire.ts))
@@ -561,11 +508,16 @@ whose it is.
561
508
  environment the watcher (and `sync uninstall`) moves off the old directory
562
509
  ([`sync/mutagen-move.ts`](src/sync/mutagen-move.ts)): each of this agent's sync sessions there is carried, not made
563
510
  again. It is paused, its state files (`sessions/<id>` and `archives/<id>`, the last state both sides agreed on) are
564
- copied into the new directory, it is terminated in the old one, and this agent's daemon, started again, loads it and
565
- resumes what was running. Made again from nothing, a two-way session would bring back what one side deleted while it
511
+ copied into the new directory, it is terminated in the old one, and this agent's daemon, started again, loads it. A
512
+ session that was paused there is the owner's pause (`ownerPausedAt`), and the watcher's next pass resumes the rest.
513
+ _(2026-10-20)_ Which sessions were paused is written down at the move's first look (`mutagen-move-paused.json`, gone
514
+ once the move is recorded done) and declared before the move pauses anything; a later try reads that record, not a
515
+ listing in which the move's own pauses look like the owner's. Declared after the terminate instead, a move cut short
516
+ in between left nothing saying whose the pauses were, and its retry declared every session the owner's, so folders
517
+ nobody paused stayed paused. Made again from nothing, a two-way session would bring back what one side deleted while it
566
518
  could not sync and read a one-sided edit as a conflict; nothing could be flushed first, since the move runs before the
567
519
  transport is up. Forwards are terminated and made again. A session that cannot be carried is terminated and made again
568
- by the watcher, paused if it was (`mutagen-moved.json` holds the names until then). The old daemon is stopped only
520
+ by the watcher, paused if it was, since its pause was declared the same way. The old daemon is stopped only
569
521
  when nothing else is left in it or the move started it; one of another Mutagen version is stopped and started with
570
522
  this agent's copy for the move, then started again with the owner's own Mutagen where one is on PATH. Mutagen's own
571
523
  sign-in registration is removed only where it starts this agent's copy.
@@ -594,11 +546,10 @@ whose it is.
594
546
  up to a week. A clone the bridge started, or one already shallow, takes the sandbox's shallow boundary
595
547
  (`--update-shallow`), so a shallow sandbox repo no longer leaves a whole refused pack behind on every pass. Leftovers
596
548
  older than an hour from an earlier agent are cleared once per process. Holds are in `status` and the report.
597
- - **A nearly full drive pauses folder sync** (2026-10-18, [`sync/low-disk.ts`](src/sync/low-disk.ts)). Every pass
598
- measures the drive holding each folder whose session writes there (not a copy-first project, which only sends).
599
- Under 2% of the drive, clamped to 5–20 GiB, the pairing's sessions and git bridge pause (`fileSyncLowDisk`); they
600
- resume once the floor and 2 GiB more are free. While paused, no other pause's lifting resumes them, and the pause is
601
- put back once a minute in case one did. `status` and the device card say why, with the numbers.
549
+ - **A nearly full drive holds folder sync still** (2026-10-18, [`sync/low-disk.ts`](src/sync/low-disk.ts)). Every
550
+ pass measures the drive holding each folder whose session writes there (not a copy-first project, which only sends).
551
+ Under 2% of the drive, clamped to 5–20 GiB, the pairing's sync and git bridge hold still (`lowDisk`); they carry on
552
+ once the floor and 2 GiB more are free. `status` and the device card say why, with the numbers.
602
553
  - **Keys and enrollments per environment.** A newly made key is commented `<hostname>-<environment>`, so the sandbox
603
554
  files a new enrollment under that name. An existing key keeps its comment (a sandbox older than this agent would read
604
555
  a changed comment on the same key as a second machine holding sync); the sandbox tells existing ones apart by machine
@@ -606,7 +557,7 @@ whose it is.
606
557
  - **Tunnel ports per environment.** Under WSL's mirrored networking a distro's loopback is the Windows side's, so both
607
558
  sides pairing one sandbox derived one port twice. A distro now derives its ports in 20000-23999 from its name and the
608
559
  sandbox id; every native environment keeps 24000-27999 exactly as before, so nothing outside WSL moves.
609
- - **One folder, one sync, across the PC.** `setup` and `attach` read the other environments' `sync.json` (from the
560
+ - **One folder, one sync, across the PC.** `setup` reads the other environments' `sync.json` (from the
610
561
  Windows side each supervised distro's through `wsl.exe -d <distro> cat`; from a distro the Windows side's beside its
611
562
  agent) and compare after one spelling: `C:\code\app` and `/mnt/c/code/app` are `drive:c/code/app`, and
612
563
  `\\wsl.localhost\Ubuntu\home\ada\app` and Ubuntu's `/home/ada/app` are `wsl:ubuntu/home/ada/app`. An overlap is refused
@@ -641,12 +592,33 @@ whose it is.
641
592
  each side's sandboxes to that side, a sandbox made from WSL had nobody backing it up, preparing its update or tidying
642
593
  after it, so each environment now runs them for its own. Both back off from a slug that keeps failing, as auto-prepare does, and each has its switch
643
594
  beside the update ones (`intentic-machine updates --backups off`, `--tidy off`).
644
- - **File sync holds still while its sandbox is swapped here** ([`sync/swap-pause.ts`](src/sync/swap-pause.ts)). A
595
+ - **File sync holds still while its sandbox is swapped here** ([`sync/holds.ts`](src/sync/holds.ts) `holdsSync`). A
645
596
  pairing's local slug is the first label of its sandbox's public hostname (what ic names a sandbox with one) or the
646
- id in it (what ic names one from its connect token), so the mapping is exact. Its sessions are paused through the
647
- cutover and the first quarter hour of probation, and past that while the sandbox does not answer, then resumed; a
648
- pause somebody made is left alone. Only this environment's records count: a distro syncing a sandbox the Windows side
649
- swaps is not paused.
597
+ id in it (what ic names one from its connect token), so the mapping is exact. Its sync holds still through the
598
+ cutover and the first quarter hour after the cutover began, and past that while ic still watches the swap and the
599
+ sandbox does not answer. The quarter hour is measured from the pairing's own `swapSince`, the cutover's start as ic
600
+ recorded it, so it lasts after ic lets its record of the swap go. (2026-10-20: a swap with no probation, such as the dev
601
+ loop, leaves no record seconds after its cutover, and a hold read off the record alone let go after about thirty
602
+ seconds. Giving ic's record a longer life was rejected: the hold is the agent's fact, and ic's record answers whether a
603
+ swap is going on.) A later swap of the same sandbox starts the quarter hour again. Only this environment's records
604
+ count: a distro syncing a sandbox the Windows side swaps is not held.
605
+ - **File sync runs or holds still by one rule** (2026-10-20, [`sync/holds.ts`](src/sync/holds.ts)). Eight facts on a
606
+ pairing hold its sync still: the owner's `sync pause` (`ownerPausedAt`), the sandbox said to be gone (`goneSince`),
607
+ its container away or in ic's trash (`containerAway`), a swap (`swapSince`), an hour without an answer
608
+ (`unreachableSince`), a nearly full drive (`lowDisk`), and a bring-back, restore or delivery writing into the folder
609
+ (`transferSince`, 2026-10-21; that operation also pauses its session itself, at once, before it writes). Each is kept
610
+ by the one rule that observes it, and none of
611
+ them touches Mutagen. Every pass, after the rules have run, `holdSessions` reads every session in one listing and
612
+ makes Mutagen agree: what runs while anything holds it is paused, and what is paused while nothing does is resumed,
613
+ one session per call and read back (`resumeSessions`), tried again each pass until Mutagen took it. A session is made
614
+ paused while its pairing is held. `sync resume` lifts the owner's pause alone and says what else still holds it; the
615
+ report carries the holds as `heldBy`, so a page offers Resume only for the owner's pause. Before this, each reason
616
+ paused and resumed Mutagen itself behind a marker of its own (`fileSyncAutoPaused`, `fileSyncSwapPaused`,
617
+ `fileSyncPausedFor`, `fileSyncLowDisk`, translated on read into the facts above), set only when it found the session
618
+ running, and a person's pause had no record at all. A marker cleared before the resume took (fair-moth, 2026-10-19),
619
+ one a reattach dropped (swift-cedar, 2026-10-17), or a pause another reason found in place left sync paused with
620
+ nothing to lift it, or lifted one somebody still meant. The first pass of this build takes every paused session no
621
+ fact explains as the owner's, once (`sync-owner-pauses-adopted`).
650
622
  - (2026-10-15) **Nothing downloaded replaces a binary until the release vouches for it**
651
623
  ([`release-verify.ts`](src/release/release-verify.ts)). Before an upgrade runs the new agent (and the Windows launcher
652
624
  beside it), before ic is put in place ([`tools/ic-binary.ts`](src/device/tools/ic-binary.ts)), and before setup
@@ -766,10 +738,19 @@ The keeper now puts an unneeded sandbox to sleep and wakes it when somebody open
766
738
  - **Wake.** The platform never calls a machine. A browser that finds the sandbox asleep asks the platform to wake it,
767
739
  and this agent's wake round asks the platform for such requests (`ic sandbox wakes --json --source agent`, one request
768
740
  for every sleeper on this side, each under its own report key), starting each one asked for through `ic sandbox
769
- start`. It asks every fifteen seconds while anything sleeps, and every five minutes otherwise, to catch a sleep made
770
- by hand. It runs whatever the keeper's switch says, so a sandbox asleep when the keeper was switched off still wakes.
741
+ start`. While anything sleeps the ask is held (2026-10-19): `--wait 25` has the platform answer the moment somebody
742
+ asks for a wake, or after 25 s, and the next ask follows at once, so a wake no longer waits up to fifteen seconds for
743
+ the next poll. An ic or a platform from before the held ask is asked every fifteen seconds as before; an ic that
744
+ refuses the flag (clap's exit 2, no JSON) is asked again at once without it, and plainly for an hour. With nothing
745
+ asleep it asks every five minutes, to catch a sleep made by hand. It runs whatever the keeper's switch says, so a
746
+ sandbox asleep when the keeper was switched off still wakes.
771
747
  The desktop app on the same computer runs the same command the moment its workspace page has asked
772
748
  (`--source app`, the app's `sandbox_wakes.rs`), so a sandbox opened there need not wait for this round.
749
+ - **The link comes back with it** (2026-10-19). A sleep leaves this agent's link to the sandbox on its dial ladder,
750
+ which after twenty failures rests a quarter of an hour between tries: on rog a sandbox woken at 16:48 had its device
751
+ tools back at 17:12. Each start is acted on as ic prints it, and every link naming the slug is dialled at once and put
752
+ back on the ladder's first rung (peer-dial's `retryNow`); so is a sandbox somebody else woke, once ic counts nothing
753
+ asleep or the keeper's next listing shows it running.
773
754
  - **The setting** is this environment's `sandboxSleepMinutes` in `machine.json`: absent means 30, `0` means never. Set
774
755
  it with `intentic-machine sandbox sleep-after <minutes|off>` and read it with `sleep-after status`. One sandbox can
775
756
  be kept awake from its own editor instead (Overview, "Sleep when unused", settings `sleepWhenUnused`, 2026-10-18):
@@ -869,16 +850,28 @@ version, file time) and the `IC_*`/`INTENTIC_*` variables in the agent's environ
869
850
 
870
851
  ### The keyring
871
852
 
872
- [intentic-keyring](../keyring) is a process of its own beside this agent (2026-10-16): the agent installs it, keeps
873
- it running and relays to it, and holds no vault key and no value. `intentic-machine keyring setup` puts the
874
- `intentic-keyring` asset of this agent's own release into `bin/` (checked against the release's signed `SHA256SUMS`, as
875
- the launcher is; a from-source agent copies `_devices/keyring/dist-bin`'s build instead), makes `intentic-askpass` a
876
- hard link beside it, registers a login entry of its own (`keyring`, `IntenticKeyring` on Windows, running
877
- `intentic-keyring serve`), starts it, and sets up the vault with a passphrase typed twice without echo (or once on
878
- stdin with `--passphrase-stdin`), plus Windows Hello where there is one unless `--no-hello`. Nothing in the agent's
879
- own upgrade, its `bin/` sweep or its rollback touches the keyring's files: it moves to a new release only through
880
- `keyring upgrade`, which downloads and checks the new binary, asks the running keyring (which asks the person), and only
881
- then swaps it, keeping `.previous` and putting it back if the new one does not answer.
853
+ [intentic-keyring](../keyring), the vault behind Passwords, is a process of its own beside this agent (2026-10-16):
854
+ the agent installs it, keeps it running and relays to it, and holds no vault key and no value. Since 2026-10-19 it has
855
+ one mode, the protected service: the vault is held by a system account of its own (`LOCAL SERVICE` from a startup task
856
+ on Windows, the `intentic-keyring` user from the systemd unit `intentic-keyring@<uid>` on Linux), and what starts at
857
+ sign-in is its session helper. `intentic-machine keyring setup` puts the `intentic-keyring` asset of this agent's own
858
+ release into `bin/` (checked against the release's signed `SHA256SUMS`, as the launcher is; a from-source agent copies
859
+ `_devices/keyring/dist-bin`'s build instead), makes `intentic-askpass` a hard link beside it, asks for an
860
+ administrator's yes once (UAC on Windows, `pkexec` on Linux) to run `intentic-keyring service install`, registers the
861
+ helper's login entry (`keyring`, `IntenticKeyring` on Windows, running `intentic-keyring session`), starts it, and sets
862
+ up the vault: it generates a recovery key (six groups of five Crockford base32 characters, 150 bits), sends it as the
863
+ setup op's `passphrase`, and prints it once; it is never stored. Windows Hello is enrolled through the helper unless
864
+ `--no-hello`; on Linux the daily unlock is a polkit yes. Without the administrator's yes setup stops with `declined`
865
+ and "Passwords needs an administrator's approval once…", having changed nothing; on macOS, or a Linux without
866
+ systemd, polkit or setfacl, it stops with `unavailable` before installing anything. A computer still on the per-user
867
+ keyring from before (`intentic-keyring serve`, retired) is moved by `setup` or `upgrade`: the old keyring is stopped,
868
+ `service install` carries `~/.intentic/keyring/vault.json` and its files into the service's folder, and the login entry
869
+ becomes the helper's. Its old passphrase still opens it where the recovery key would.
870
+
871
+ Nothing in the agent's own upgrade, its `bin/` sweep or its rollback touches the keyring's files: it moves to a new
872
+ release only through `keyring upgrade`, which downloads and checks the new binary, asks the running keyring (which asks
873
+ the person), and only then swaps it, keeping `.previous` and putting it back if the new one does not answer; the new
874
+ binary's `service install` then runs as an administrator, so the service's own copy follows.
882
875
 
883
876
  - **Relayed for the sandbox** ([`keyring/relay.ts`](src/keyring/relay.ts)): the contract's `keyring*` procedures go
884
877
  to the keyring's pipe and back, with the sandbox named from the link the call came in on (`shares`, `copies`) and an
@@ -890,12 +883,9 @@ then swaps it, keeping `.previous` and putting it back if the new one does not a
890
883
  1.5 s, the answer kept for 30 s ([`keyring/feature.ts`](src/keyring/feature.ts)).
891
884
  - **Marked**: what `run_command` and `app_start` start carries `INTENTIC_AGENT_COMMAND=1`, for the keyring's record
892
885
  and prompts. A hint, not a boundary: the agent's command can unset it.
893
- - **Hardened** (Windows, `keyring harden`): the plain keyring is stopped and its login entry removed first, so the
894
- vault the service takes is the last one written; then one UAC prompt runs `intentic-keyring service install`, which
895
- moves the vault under the startup task that runs the keyring as `LOCAL SERVICE` (the keyring's README says how), and
896
- the login entry becomes `intentic-keyring session`. Hardened mode is read from the marker the install writes for this
897
- user (`%ProgramData%\Intentic\Keyring\<SID>.hardened`). `keyring upgrade` on a hardened keyring also runs the new
898
- binary's `service install`, elevated, so the service's own copy follows.
886
+ - **Installed** is read from the marker `service install` writes for this user, which every user may read:
887
+ `%ProgramData%\Intentic\Keyring\<SID>.hardened` on Windows (named from when the service was an optional "hardened
888
+ mode"), `/var/lib/intentic-keyring/<uid>.installed` on Linux ([`keyring/service.ts`](src/keyring/service.ts)).
899
889
  - `status` and `doctor` show one keyring row (not installed, not running, not set up, locked or unlocked), and
900
890
  `status --json` and `doctor --json` carry it as `keyring: { installed, running, setUp, locked?, unlockedUntil?, version? }`.
901
891
 
@@ -906,8 +896,8 @@ it reaches the pipe.
906
896
 
907
897
  | Command | Success output |
908
898
  |---|---|
909
- | `keyring setup [--passphrase-stdin] [--no-hello]` | `{ "ok": true, "path", "askpass": { "installed", "path"? }, "autostart", "setUp": true, "alreadySetUp", "state"? }` |
910
- | `keyring status` | `{ "ok": true, "installed", "running", "setUp", "state"?, "askpass": { "installed", "path"? }, "hardened" }` |
899
+ | `keyring setup [--no-hello]` | `{ "ok": true, "path", "askpass": { "installed", "path"? }, "autostart", "setUp": true, "alreadySetUp", "recoveryKey"?, "state"? }` (`recoveryKey` only when this run made the vault) |
900
+ | `keyring status` | `{ "ok": true, "installed", "running", "setUp", "state"?, "askpass": { "installed", "path"? }, "support": { "canSetUp", "why"?, "hello", "platform" } }` |
911
901
  | `keyring unlock [--minutes N]` / `keyring lock` | `{ "ok": true, "unlockedUntil" }` / `{ "ok": true }` |
912
902
  | `keyring list` | `{ "ok": true, "entries": [EntryInfo], "backends" }` |
913
903
  | `keyring add NAME [--stdin] [rules] [--backend B --ref R [--field F] [--attribute A]] [--replace]` | `{ "ok": true, "name" }` |
@@ -919,8 +909,7 @@ it reaches the pipe.
919
909
  | `keyring askpass install\|uninstall\|status` | `{ "ok": true, "installed", "path"?, "variables", "file"?, "profile"? }` |
920
910
  | `keyring sync status` | `{ "ok": true, "sync": { "key", "peers", "revision" } }` |
921
911
  | `keyring upgrade` | `{ "ok": true, "upgraded", "from", "to" }` |
922
- | `keyring harden` (Windows) | `{ "ok": true, "service": "IntenticKeyring", "autostart" }` |
923
- | `keyring uninstall` | `{ "ok": true, "removed", "vault", "hardened" }` |
912
+ | `keyring uninstall` | `{ "ok": true, "removed", "vault", "service" }` |
924
913
 
925
914
  The rule flags are `--kind password|text`, `--program P`, `--package P`, `--host H`, `--askpass M` (each repeatable;
926
915
  on `edit` a list flag replaces the whole list, and a lone `""` empties it), `--username U`, `--remember-minutes N`, and
@@ -928,17 +917,59 @@ on `edit` a list flag replaces the whole list, and a lone `""` empties it), `--u
928
917
  value. `askpass install` sets `SSH_ASKPASS`, `SSH_ASKPASS_REQUIRE=prefer`, `SUDO_ASKPASS` and `GIT_ASKPASS` for the
929
918
  user: through `[Environment]::SetEnvironmentVariable` on Windows, and on Linux and macOS in
930
919
  `~/.config/environment.d/intentic-askpass.conf`, printing the `export` lines a shell profile needs. `keyring
931
- uninstall` keeps the vault in `~/.intentic/keyring`.
920
+ uninstall` removes the service (an administrator's yes) and puts the vault back in `~/.intentic/keyring`, where it
921
+ stays; set up again, the same recovery key opens it.
922
+
923
+ ### Uninstall
924
+
925
+ `intentic-machine uninstall [--purge] [--delete-data]` ([`src/uninstall/`](src/uninstall/)). `ic uninstall` is the one
926
+ orchestrator of a whole-device uninstall and runs `--purge` here; the agent's own command is the same removal for a
927
+ person who runs it alone, and either way it never downloads anything.
928
+
929
+ - **No flag**: every sandbox link, every sync pairing and the login entry go, as they always have. The agent goes even
930
+ from a device that hosts sandboxes; on Windows it keeps running while WSL distros are attached, to keep them running.
931
+ - **`--purge`**: everything of the agent leaves this environment, in this order:
932
+ 1. On Windows, each WSL distro in `machine.json` is purged through its own agent (`wsl.exe`, five minutes at most
933
+ each; an agent older than `--purge` takes the plain uninstall, and the warning says how to finish), and the list
934
+ is emptied, so nothing is left for the Windows side's agent to keep running for.
935
+ 2. On Linux and macOS the binary is renamed to `bin/intentic-machine.uninstalling` before anything stops the agent.
936
+ The Windows side restarts a stopped distro agent a second later through a script that exits 64 when the binary is
937
+ missing, and then drops that distro from its list, so a purged distro does not come back.
938
+ 3. The programs sandboxes started with `app_start` are stopped, from every sandbox's `runs/<sandbox>/apps.json`.
939
+ 4. The plain uninstall runs (links, pairings, the login entry). Mutagen is the agent's own copy in `bin/` or none: a
940
+ missing copy skips the session steps instead of downloading one.
941
+ 5. Windows' start before sign-in is removed (Windows asks for permission), and the resident agent is stopped with its
942
+ login entry whatever it still served.
943
+ 6. The agent's Mutagen daemon is stopped and the ssh include removed, in case the sync step stopped early.
944
+ 7. The keyring goes with askpass (`keyring uninstall`). Its protected service needs an administrator's yes. If it is
945
+ declined, the service stays, and so do `bin/` and the PATH entry, so the purge can be run again to finish it.
946
+ 8. What the agents before this one left goes: the `intentic-host` and `intentic-sync-mirror` login entries,
947
+ `~/.intentic/host` and `~/.intentic/sync`, their `~/.local/bin` links, and the `intentic-link-watch` timer.
948
+ 9. The PATH entry goes: `bin` out of the per-user PATH on Windows (`removeFromWindowsPathValue`, written back under
949
+ the value's own kind), and the `~/.local/bin/intentic-machine` link where it points into `bin`.
950
+ 10. Lingering is turned off only where the agent turned it on. local-agent writes `linger-enabled` beside the agent's
951
+ log the one time `loginctl enable-linger` changes it from off to on. Lingering the user had already stays.
952
+ 11. The files go: `~/.intentic/machine` and `~/.intentic/keyring` except the person's data, then each folder once
953
+ empty, and `~/.intentic` once nothing is left in it. Everything else under `~/.intentic` is ic's and is never
954
+ touched. On Windows the running agent is moved to `%TEMP%` first, and a hidden PowerShell removes what was still
955
+ in use once this process has exited.
956
+ - **The data that stays** without `--delete-data`: the keyring's `vault.json`, `keys.json` and `audit.jsonl*`;
957
+ `machine/restore`; `machine/audit.jsonl*`; and in `machine/trash` the folders `trash_file` made for files a sandbox
958
+ moved aside on this device (named for the moment alone; the upkeep's own trash entries go). The narration says what
959
+ stayed and why.
960
+ - **`--delete-data`** implies `--purge` and removes the data too.
961
+ - A step that does not finish is a warning with the command that finishes it by hand (`fix:` on its own line), and the
962
+ verdict counts them. The run fails, exit 1, only when the agent is still running or its files could not be removed.
932
963
 
933
964
  ## Key files
934
965
 
935
966
  - [src/commands.ts](src/commands.ts) — the CLI: `device`, `sync`, `keyring`, `sandbox`, `run`, `status`, `doctor`, `upgrade`, `updates`, `uninstall`.
967
+ - [src/uninstall/purge.ts](src/uninstall/purge.ts) — `uninstall --purge`: the steps in `steps.ts`, the distros in `distros.ts`, the files in `sweep.ts`, every decision pure in `plan.ts`.
936
968
  - [src/resident/resident.ts](src/resident/resident.ts) — the resident process that holds links, pairings and WSL distros; `supervision.ts` beside it starts and stops it through whatever restarts this environment's agent.
937
969
  - [src/device/mcp.ts](src/device/mcp.ts) — every tool a sandbox can call on this device.
938
970
  - [src/device/policy.ts](src/device/policy.ts) — scope checks and the file-root boundary.
939
971
  - [src/sync/endpoint.ts](src/sync/endpoint.ts) — how a pairing reaches its sandbox: the tunnelled sshd (`tunnel.ts`), or Docker for a project on this machine.
940
- - [src/sync/attach-commands.ts](src/sync/attach-commands.ts) — `sync attach` and `sync detach`, folders on this computer's own sandbox.
941
- - [src/sync/project/project-delivery.ts](src/sync/project/project-delivery.ts) — landed work written into an attached folder (`deliverProject`).
972
+ - [src/sync/project/project-delivery.ts](src/sync/project/project-delivery.ts) — landed work written into a project folder (`deliverProject`).
942
973
  - [src/sync/gone.ts](src/sync/gone.ts) — when a paired sandbox counts as gone; `gone-watch.ts` acts on it, `retire.ts` retires its pairings, `forget-command.ts` is `sync forget`.
943
974
  - [src/environments/machine.ts](src/environments/machine.ts) — the Windows root and its WSL children.
944
975
  - [src/upkeep/manifest.ts](src/upkeep/manifest.ts) — what the device upkeep finds and puts right; `reconcile.ts` runs it.
@@ -1,5 +1,4 @@
1
1
  import type { AutostartSpec } from "@intentic/local-agent";
2
2
  export declare const MACHINE_AUTOSTART: AutostartSpec;
3
3
  export declare const KEYRING_AUTOSTART: AutostartSpec;
4
- export declare const KEYRING_SESSION_AUTOSTART: AutostartSpec;
5
4
  //# sourceMappingURL=autostart.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"autostart.d.ts","sourceRoot":"","sources":["../../src/autostart/autostart.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAM3D,eAAO,MAAM,iBAAiB,EAAE,aAc/B,CAAC;AAOF,eAAO,MAAM,iBAAiB,EAAE,aAW/B,CAAC;AAIF,eAAO,MAAM,yBAAyB,EAAE,aAKvC,CAAC"}
1
+ {"version":3,"file":"autostart.d.ts","sourceRoot":"","sources":["../../src/autostart/autostart.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAM3D,eAAO,MAAM,iBAAiB,EAAE,aAc/B,CAAC;AASF,eAAO,MAAM,iBAAiB,EAAE,aAW/B,CAAC"}
@@ -16,18 +16,12 @@ const keyringLog = keyringPaths().log;
16
16
  export const KEYRING_AUTOSTART = {
17
17
  id: "keyring",
18
18
  windowsRunValue: "IntenticKeyring",
19
- desktopName: "Intentic Keyring",
20
- desktopComment: "The password vault on this computer: types and hands over secrets for your intentic sandboxes, at your word",
19
+ desktopName: "Intentic Passwords",
20
+ desktopComment: "Passwords on this computer: its prompts and typing on this desktop, for the protected service that holds the vault",
21
21
  logPath: keyringLog,
22
22
  launchAgent: { label: "dev.intentic.keyring" },
23
- detachedArgs: ["serve"],
24
- foregroundArgs: ["serve"],
25
- failureNote: (reason) => `Couldn't register the keyring to start at login (${reason}); it runs until this computer restarts, and \`intentic-machine keyring setup\` starts it again. Its log is ${keyringLog}.`,
26
- };
27
- export const KEYRING_SESSION_AUTOSTART = {
28
- ...KEYRING_AUTOSTART,
29
- desktopComment: "The intentic keyring's prompts and typing on this desktop, for the IntenticKeyring service that holds the vault",
30
23
  detachedArgs: ["session"],
31
24
  foregroundArgs: ["session"],
25
+ failureNote: (reason) => `Couldn't register Passwords to start at login (${reason}); it runs until this computer restarts, and \`intentic-machine keyring setup\` starts it again. Its log is ${keyringLog}.`,
32
26
  };
33
27
  //# sourceMappingURL=autostart.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"autostart.js","sourceRoot":"","sources":["../../src/autostart/autostart.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAK1C,MAAM,CAAC,MAAM,iBAAiB,GAAkB;IAC5C,EAAE,EAAE,kBAAkB;IACtB,eAAe,EAAE,iBAAiB;IAClC,WAAW,EAAE,wBAAwB;IACrC,cAAc,EAAE,yFAAyF;IAEzG,OAAO,EAAE,UAAU;IACnB,WAAW,EAAE,EAAE,KAAK,EAAE,sBAAsB,EAAE;IAC9C,YAAY,EAAE,CAAC,KAAK,CAAC;IACrB,cAAc,EAAE,CAAC,KAAK,EAAE,cAAc,CAAC;IAEvC,gBAAgB,EAAE,CAAC,KAAK,EAAE,cAAc,EAAE,kBAAkB,CAAC;IAC7D,WAAW,EAAE,CAAC,MAAM,EAAE,EAAE,CACpB,4DAA4D,MAAM,iGAAiG,UAAU,GAAG;CACvL,CAAC;AAKF,MAAM,UAAU,GAAG,YAAY,EAAE,CAAC,GAAG,CAAC;AAEtC,MAAM,CAAC,MAAM,iBAAiB,GAAkB;IAC5C,EAAE,EAAE,SAAS;IACb,eAAe,EAAE,iBAAiB;IAClC,WAAW,EAAE,kBAAkB;IAC/B,cAAc,EAAE,6GAA6G;IAC7H,OAAO,EAAE,UAAU;IACnB,WAAW,EAAE,EAAE,KAAK,EAAE,sBAAsB,EAAE;IAC9C,YAAY,EAAE,CAAC,OAAO,CAAC;IACvB,cAAc,EAAE,CAAC,OAAO,CAAC;IACzB,WAAW,EAAE,CAAC,MAAM,EAAE,EAAE,CACpB,oDAAoD,MAAM,+GAA+G,UAAU,GAAG;CAC7L,CAAC;AAIF,MAAM,CAAC,MAAM,yBAAyB,GAAkB;IACpD,GAAG,iBAAiB;IACpB,cAAc,EAAE,iHAAiH;IACjI,YAAY,EAAE,CAAC,SAAS,CAAC;IACzB,cAAc,EAAE,CAAC,SAAS,CAAC;CAC9B,CAAC"}
1
+ {"version":3,"file":"autostart.js","sourceRoot":"","sources":["../../src/autostart/autostart.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAK1C,MAAM,CAAC,MAAM,iBAAiB,GAAkB;IAC5C,EAAE,EAAE,kBAAkB;IACtB,eAAe,EAAE,iBAAiB;IAClC,WAAW,EAAE,wBAAwB;IACrC,cAAc,EAAE,yFAAyF;IAEzG,OAAO,EAAE,UAAU;IACnB,WAAW,EAAE,EAAE,KAAK,EAAE,sBAAsB,EAAE;IAC9C,YAAY,EAAE,CAAC,KAAK,CAAC;IACrB,cAAc,EAAE,CAAC,KAAK,EAAE,cAAc,CAAC;IAEvC,gBAAgB,EAAE,CAAC,KAAK,EAAE,cAAc,EAAE,kBAAkB,CAAC;IAC7D,WAAW,EAAE,CAAC,MAAM,EAAE,EAAE,CACpB,4DAA4D,MAAM,iGAAiG,UAAU,GAAG;CACvL,CAAC;AAOF,MAAM,UAAU,GAAG,YAAY,EAAE,CAAC,GAAG,CAAC;AAEtC,MAAM,CAAC,MAAM,iBAAiB,GAAkB;IAC5C,EAAE,EAAE,SAAS;IACb,eAAe,EAAE,iBAAiB;IAClC,WAAW,EAAE,oBAAoB;IACjC,cAAc,EAAE,oHAAoH;IACpI,OAAO,EAAE,UAAU;IACnB,WAAW,EAAE,EAAE,KAAK,EAAE,sBAAsB,EAAE;IAC9C,YAAY,EAAE,CAAC,SAAS,CAAC;IACzB,cAAc,EAAE,CAAC,SAAS,CAAC;IAC3B,WAAW,EAAE,CAAC,MAAM,EAAE,EAAE,CACpB,kDAAkD,MAAM,+GAA+G,UAAU,GAAG;CAC3L,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AA0JnE,eAAO,MAAM,QAAQ,kDA4BnB,CAAC"}
1
+ {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAsInE,eAAO,MAAM,QAAQ,kDA4BnB,CAAC"}