@intentic/sandbox-contract 1.331.0 → 1.333.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 (416) hide show
  1. package/README.md +23 -1
  2. package/dist/chores/probes.d.ts.map +1 -1
  3. package/dist/chores/probes.js.map +1 -1
  4. package/dist/contracts/accounts.contract.d.ts +11 -0
  5. package/dist/contracts/accounts.contract.d.ts.map +1 -1
  6. package/dist/contracts/agent.contract.d.ts +38 -0
  7. package/dist/contracts/agent.contract.d.ts.map +1 -1
  8. package/dist/contracts/agent.contract.js +14 -3
  9. package/dist/contracts/agent.contract.js.map +1 -1
  10. package/dist/contracts/agents.contract.d.ts +29 -0
  11. package/dist/contracts/agents.contract.d.ts.map +1 -1
  12. package/dist/contracts/automations.contract.d.ts +3 -0
  13. package/dist/contracts/automations.contract.d.ts.map +1 -1
  14. package/dist/contracts/device.contract.d.ts +7 -0
  15. package/dist/contracts/device.contract.d.ts.map +1 -1
  16. package/dist/contracts/git.contract.d.ts +2 -0
  17. package/dist/contracts/git.contract.d.ts.map +1 -1
  18. package/dist/contracts/logs.contract.d.ts +10 -0
  19. package/dist/contracts/logs.contract.d.ts.map +1 -1
  20. package/dist/contracts/needs.contract.d.ts +36 -1
  21. package/dist/contracts/needs.contract.d.ts.map +1 -1
  22. package/dist/contracts/needs.contract.js +2 -2
  23. package/dist/contracts/needs.contract.js.map +1 -1
  24. package/dist/contracts/offload.contract.d.ts +70 -0
  25. package/dist/contracts/offload.contract.d.ts.map +1 -1
  26. package/dist/contracts/offload.contract.js +10 -1
  27. package/dist/contracts/offload.contract.js.map +1 -1
  28. package/dist/contracts/privacy.contract.d.ts +14 -0
  29. package/dist/contracts/privacy.contract.d.ts.map +1 -1
  30. package/dist/contracts/privacy.contract.js +11 -1
  31. package/dist/contracts/privacy.contract.js.map +1 -1
  32. package/dist/contracts/providers.contract.d.ts +3 -0
  33. package/dist/contracts/providers.contract.d.ts.map +1 -1
  34. package/dist/contracts/runner.contract.d.ts +103 -2
  35. package/dist/contracts/runner.contract.d.ts.map +1 -1
  36. package/dist/contracts/runner.contract.js +2 -2
  37. package/dist/contracts/runner.contract.js.map +1 -1
  38. package/dist/contracts/sessions.contract.d.ts +1 -0
  39. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  40. package/dist/contracts/settings.contract.d.ts +106 -94
  41. package/dist/contracts/settings.contract.d.ts.map +1 -1
  42. package/dist/contracts/settings.contract.js +19 -1
  43. package/dist/contracts/settings.contract.js.map +1 -1
  44. package/dist/contracts/system.contract.d.ts +17 -0
  45. package/dist/contracts/system.contract.d.ts.map +1 -1
  46. package/dist/contracts/translator.contract.d.ts +4 -0
  47. package/dist/contracts/translator.contract.d.ts.map +1 -1
  48. package/dist/events/agent-events.d.ts +46 -0
  49. package/dist/events/agent-events.d.ts.map +1 -1
  50. package/dist/events/agent-events.js +6 -0
  51. package/dist/events/agent-events.js.map +1 -1
  52. package/dist/events/agent-words.d.ts.map +1 -1
  53. package/dist/events/agent-words.js +1 -1
  54. package/dist/events/agent-words.js.map +1 -1
  55. package/dist/events/resume.d.ts +3 -0
  56. package/dist/events/resume.d.ts.map +1 -1
  57. package/dist/events/resume.js +17 -2
  58. package/dist/events/resume.js.map +1 -1
  59. package/dist/events/sandbox-notice.d.ts +18 -0
  60. package/dist/events/sandbox-notice.d.ts.map +1 -1
  61. package/dist/events/sandbox-notice.js +27 -1
  62. package/dist/events/sandbox-notice.js.map +1 -1
  63. package/dist/events/system-events.d.ts +2 -0
  64. package/dist/events/system-events.d.ts.map +1 -1
  65. package/dist/events/transcript.d.ts +64 -0
  66. package/dist/events/transcript.d.ts.map +1 -1
  67. package/dist/events/transcript.js +17 -1
  68. package/dist/events/transcript.js.map +1 -1
  69. package/dist/index.d.ts +427 -167
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +12 -0
  72. package/dist/index.js.map +1 -1
  73. package/dist/models/agent-catalog.d.ts.map +1 -1
  74. package/dist/models/agent-catalog.js +8 -7
  75. package/dist/models/agent-catalog.js.map +1 -1
  76. package/dist/models/agent-runtimes.d.ts +3 -1
  77. package/dist/models/agent-runtimes.d.ts.map +1 -1
  78. package/dist/models/agent-runtimes.js +10 -0
  79. package/dist/models/agent-runtimes.js.map +1 -1
  80. package/dist/models/model-order.d.ts +1 -0
  81. package/dist/models/model-order.d.ts.map +1 -1
  82. package/dist/models/model-order.js +9 -0
  83. package/dist/models/model-order.js.map +1 -1
  84. package/dist/models/model-pins.d.ts.map +1 -1
  85. package/dist/models/model-pins.js.map +1 -1
  86. package/dist/models/model-roles.d.ts +33 -18
  87. package/dist/models/model-roles.d.ts.map +1 -1
  88. package/dist/models/model-roles.js +31 -27
  89. package/dist/models/model-roles.js.map +1 -1
  90. package/dist/models/model-setups.d.ts +46 -0
  91. package/dist/models/model-setups.d.ts.map +1 -0
  92. package/dist/models/model-setups.js +86 -0
  93. package/dist/models/model-setups.js.map +1 -0
  94. package/dist/models/plan-pools.d.ts +3 -1
  95. package/dist/models/plan-pools.d.ts.map +1 -1
  96. package/dist/models/plan-pools.js +18 -6
  97. package/dist/models/plan-pools.js.map +1 -1
  98. package/dist/models/provider-specs.d.ts +20 -0
  99. package/dist/models/provider-specs.d.ts.map +1 -1
  100. package/dist/models/provider-specs.js +13 -1
  101. package/dist/models/provider-specs.js.map +1 -1
  102. package/dist/policy/command-classes.d.ts.map +1 -1
  103. package/dist/policy/command-classes.js +26 -3
  104. package/dist/policy/command-classes.js.map +1 -1
  105. package/dist/policy/control-scopes.d.ts +47 -0
  106. package/dist/policy/control-scopes.d.ts.map +1 -1
  107. package/dist/policy/control-scopes.js +16 -0
  108. package/dist/policy/control-scopes.js.map +1 -1
  109. package/dist/policy/credential-material.d.ts +6 -0
  110. package/dist/policy/credential-material.d.ts.map +1 -1
  111. package/dist/policy/credential-material.js +37 -15
  112. package/dist/policy/credential-material.js.map +1 -1
  113. package/dist/policy/device-programs.d.ts +28 -0
  114. package/dist/policy/device-programs.d.ts.map +1 -0
  115. package/dist/policy/device-programs.js +79 -0
  116. package/dist/policy/device-programs.js.map +1 -0
  117. package/dist/policy/identity-assertion.d.ts +27 -0
  118. package/dist/policy/identity-assertion.d.ts.map +1 -0
  119. package/dist/policy/identity-assertion.js +77 -0
  120. package/dist/policy/identity-assertion.js.map +1 -0
  121. package/dist/policy/sandbox-power.d.ts +34 -0
  122. package/dist/policy/sandbox-power.d.ts.map +1 -0
  123. package/dist/policy/sandbox-power.js +146 -0
  124. package/dist/policy/sandbox-power.js.map +1 -0
  125. package/dist/policy/search-globs.d.ts +3 -0
  126. package/dist/policy/search-globs.d.ts.map +1 -1
  127. package/dist/policy/search-globs.js +118 -0
  128. package/dist/policy/search-globs.js.map +1 -1
  129. package/dist/policy/secret-hosts.d.ts +1 -0
  130. package/dist/policy/secret-hosts.d.ts.map +1 -1
  131. package/dist/policy/secret-hosts.js +1 -0
  132. package/dist/policy/secret-hosts.js.map +1 -1
  133. package/dist/policy/turned-away.d.ts.map +1 -1
  134. package/dist/policy/turned-away.js +1 -0
  135. package/dist/policy/turned-away.js.map +1 -1
  136. package/dist/protocol/doors/runner-protocol.d.ts +45 -0
  137. package/dist/protocol/doors/runner-protocol.d.ts.map +1 -1
  138. package/dist/protocol/doors/runner-protocol.js +7 -0
  139. package/dist/protocol/doors/runner-protocol.js.map +1 -1
  140. package/dist/protocol/pairing-redeem.d.ts +23 -0
  141. package/dist/protocol/pairing-redeem.d.ts.map +1 -0
  142. package/dist/protocol/pairing-redeem.js +44 -0
  143. package/dist/protocol/pairing-redeem.js.map +1 -0
  144. package/dist/protocol/peer-mcp-server.d.ts.map +1 -1
  145. package/dist/protocol/peer-mcp-server.js +1 -1
  146. package/dist/protocol/peer-mcp-server.js.map +1 -1
  147. package/dist/protocol/raw/raw-json-routes.d.ts +855 -1
  148. package/dist/protocol/raw/raw-json-routes.d.ts.map +1 -1
  149. package/dist/protocol/raw/raw-json-routes.js +61 -1
  150. package/dist/protocol/raw/raw-json-routes.js.map +1 -1
  151. package/dist/protocol/raw/raw-routes.d.ts +17 -0
  152. package/dist/protocol/raw/raw-routes.d.ts.map +1 -1
  153. package/dist/protocol/raw/raw-routes.js +5 -0
  154. package/dist/protocol/raw/raw-routes.js.map +1 -1
  155. package/dist/protocol/sse.d.ts +20 -1
  156. package/dist/protocol/sse.d.ts.map +1 -1
  157. package/dist/protocol/sse.js +112 -17
  158. package/dist/protocol/sse.js.map +1 -1
  159. package/dist/schemas/agent.d.ts +35 -0
  160. package/dist/schemas/agent.d.ts.map +1 -1
  161. package/dist/schemas/agent.js +12 -1
  162. package/dist/schemas/agent.js.map +1 -1
  163. package/dist/schemas/automations.d.ts +7 -0
  164. package/dist/schemas/automations.d.ts.map +1 -1
  165. package/dist/schemas/automations.js +11 -3
  166. package/dist/schemas/automations.js.map +1 -1
  167. package/dist/schemas/device-swaps.d.ts +19 -0
  168. package/dist/schemas/device-swaps.d.ts.map +1 -0
  169. package/dist/schemas/device-swaps.js +11 -0
  170. package/dist/schemas/device-swaps.js.map +1 -0
  171. package/dist/schemas/devices.d.ts +17 -0
  172. package/dist/schemas/devices.d.ts.map +1 -1
  173. package/dist/schemas/devices.js +2 -1
  174. package/dist/schemas/devices.js.map +1 -1
  175. package/dist/schemas/engines.d.ts +8 -1
  176. package/dist/schemas/engines.d.ts.map +1 -1
  177. package/dist/schemas/engines.js +1 -1
  178. package/dist/schemas/engines.js.map +1 -1
  179. package/dist/schemas/environment.d.ts +7 -0
  180. package/dist/schemas/environment.d.ts.map +1 -1
  181. package/dist/schemas/environment.js +2 -0
  182. package/dist/schemas/environment.js.map +1 -1
  183. package/dist/schemas/git/git-scope.d.ts +20 -0
  184. package/dist/schemas/git/git-scope.d.ts.map +1 -0
  185. package/dist/schemas/git/git-scope.js +37 -0
  186. package/dist/schemas/git/git-scope.js.map +1 -0
  187. package/dist/schemas/git/git.d.ts +1 -0
  188. package/dist/schemas/git/git.d.ts.map +1 -1
  189. package/dist/schemas/git/git.js +9 -1
  190. package/dist/schemas/git/git.js.map +1 -1
  191. package/dist/schemas/hosts.d.ts +42 -0
  192. package/dist/schemas/hosts.d.ts.map +1 -1
  193. package/dist/schemas/hosts.js +3 -0
  194. package/dist/schemas/hosts.js.map +1 -1
  195. package/dist/schemas/keep-warm.d.ts +1 -0
  196. package/dist/schemas/keep-warm.d.ts.map +1 -1
  197. package/dist/schemas/keep-warm.js +1 -1
  198. package/dist/schemas/keep-warm.js.map +1 -1
  199. package/dist/schemas/logs.d.ts +21 -0
  200. package/dist/schemas/logs.d.ts.map +1 -1
  201. package/dist/schemas/logs.js +19 -0
  202. package/dist/schemas/logs.js.map +1 -1
  203. package/dist/schemas/members.d.ts +4 -0
  204. package/dist/schemas/members.d.ts.map +1 -1
  205. package/dist/schemas/members.js +5 -0
  206. package/dist/schemas/members.js.map +1 -1
  207. package/dist/schemas/metrics.d.ts +12 -0
  208. package/dist/schemas/metrics.d.ts.map +1 -1
  209. package/dist/schemas/metrics.js +26 -8
  210. package/dist/schemas/metrics.js.map +1 -1
  211. package/dist/schemas/needs.d.ts +97 -1
  212. package/dist/schemas/needs.d.ts.map +1 -1
  213. package/dist/schemas/needs.js +53 -4
  214. package/dist/schemas/needs.js.map +1 -1
  215. package/dist/schemas/offload.d.ts +154 -0
  216. package/dist/schemas/offload.d.ts.map +1 -1
  217. package/dist/schemas/offload.js +34 -0
  218. package/dist/schemas/offload.js.map +1 -1
  219. package/dist/schemas/privacy.d.ts +19 -0
  220. package/dist/schemas/privacy.d.ts.map +1 -1
  221. package/dist/schemas/privacy.js +17 -2
  222. package/dist/schemas/privacy.js.map +1 -1
  223. package/dist/schemas/project-bring-back.d.ts +172 -0
  224. package/dist/schemas/project-bring-back.d.ts.map +1 -0
  225. package/dist/schemas/project-bring-back.js +39 -0
  226. package/dist/schemas/project-bring-back.js.map +1 -0
  227. package/dist/schemas/providers/handoff.d.ts +77 -0
  228. package/dist/schemas/providers/handoff.d.ts.map +1 -0
  229. package/dist/schemas/providers/handoff.js +85 -0
  230. package/dist/schemas/providers/handoff.js.map +1 -0
  231. package/dist/schemas/providers/plan-limits.d.ts +27 -0
  232. package/dist/schemas/providers/plan-limits.d.ts.map +1 -1
  233. package/dist/schemas/providers/plan-limits.js +8 -1
  234. package/dist/schemas/providers/plan-limits.js.map +1 -1
  235. package/dist/schemas/providers/provider-oauth.d.ts +4 -0
  236. package/dist/schemas/providers/provider-oauth.d.ts.map +1 -1
  237. package/dist/schemas/providers/usage.d.ts +3 -1
  238. package/dist/schemas/providers/usage.d.ts.map +1 -1
  239. package/dist/schemas/providers/usage.js +7 -2
  240. package/dist/schemas/providers/usage.js.map +1 -1
  241. package/dist/schemas/restart-intent.d.ts +47 -0
  242. package/dist/schemas/restart-intent.d.ts.map +1 -0
  243. package/dist/schemas/restart-intent.js +19 -0
  244. package/dist/schemas/restart-intent.js.map +1 -0
  245. package/dist/schemas/run-targets.d.ts.map +1 -1
  246. package/dist/schemas/run-targets.js +5 -2
  247. package/dist/schemas/run-targets.js.map +1 -1
  248. package/dist/schemas/settings-history.d.ts +1 -1
  249. package/dist/schemas/settings-history.d.ts.map +1 -1
  250. package/dist/schemas/settings-history.js +21 -0
  251. package/dist/schemas/settings-history.js.map +1 -1
  252. package/dist/schemas/settings.d.ts +101 -100
  253. package/dist/schemas/settings.d.ts.map +1 -1
  254. package/dist/schemas/settings.js +43 -24
  255. package/dist/schemas/settings.js.map +1 -1
  256. package/dist/schemas/short-codes.d.ts +47 -0
  257. package/dist/schemas/short-codes.d.ts.map +1 -0
  258. package/dist/schemas/short-codes.js +30 -0
  259. package/dist/schemas/short-codes.js.map +1 -0
  260. package/dist/schemas/speech.d.ts +4 -0
  261. package/dist/schemas/speech.d.ts.map +1 -1
  262. package/dist/schemas/speech.js +1 -0
  263. package/dist/schemas/speech.js.map +1 -1
  264. package/dist/schemas/system.d.ts +176 -0
  265. package/dist/schemas/system.d.ts.map +1 -1
  266. package/dist/schemas/system.js +55 -2
  267. package/dist/schemas/system.js.map +1 -1
  268. package/dist/schemas/voice.d.ts +171 -0
  269. package/dist/schemas/voice.d.ts.map +1 -0
  270. package/dist/schemas/voice.js +99 -0
  271. package/dist/schemas/voice.js.map +1 -0
  272. package/dist/schemas/workspace/workspace-upload.d.ts +14 -0
  273. package/dist/schemas/workspace/workspace-upload.d.ts.map +1 -0
  274. package/dist/schemas/workspace/workspace-upload.js +6 -0
  275. package/dist/schemas/workspace/workspace-upload.js.map +1 -0
  276. package/dist/state/arrival.d.ts +3 -0
  277. package/dist/state/arrival.d.ts.map +1 -1
  278. package/dist/state/arrival.js +1 -0
  279. package/dist/state/arrival.js.map +1 -1
  280. package/dist/state/definition.d.ts +112 -40
  281. package/dist/state/definition.d.ts.map +1 -1
  282. package/dist/state/history-state.d.ts.map +1 -1
  283. package/dist/state/history-state.js +3 -0
  284. package/dist/state/history-state.js.map +1 -1
  285. package/dist/state/runtime-state.d.ts +5 -2
  286. package/dist/state/runtime-state.d.ts.map +1 -1
  287. package/dist/state/runtime-state.js +3 -2
  288. package/dist/state/runtime-state.js.map +1 -1
  289. package/dist/state/update-probation.d.ts +50 -0
  290. package/dist/state/update-probation.d.ts.map +1 -0
  291. package/dist/state/update-probation.js +81 -0
  292. package/dist/state/update-probation.js.map +1 -0
  293. package/dist/state/versions.d.ts +6 -0
  294. package/dist/state/versions.d.ts.map +1 -1
  295. package/dist/state/versions.js +47 -0
  296. package/dist/state/versions.js.map +1 -1
  297. package/dist/state/workspace-state.d.ts +1 -1
  298. package/dist/state/workspace-state.d.ts.map +1 -1
  299. package/dist/state/workspace-state.js +1 -1
  300. package/dist/state/workspace-state.js.map +1 -1
  301. package/dist/text/embed.d.ts +2 -1
  302. package/dist/text/embed.d.ts.map +1 -1
  303. package/dist/text/embed.js +10 -5
  304. package/dist/text/embed.js.map +1 -1
  305. package/dist/text/title.d.ts.map +1 -1
  306. package/dist/text/title.js +4 -7
  307. package/dist/text/title.js.map +1 -1
  308. package/dist/text/transcript-fold.d.ts +2 -1
  309. package/dist/text/transcript-fold.d.ts.map +1 -1
  310. package/dist/text/transcript-fold.js +21 -6
  311. package/dist/text/transcript-fold.js.map +1 -1
  312. package/package.json +39 -6
  313. package/src/chores/probes.ts +3 -1
  314. package/src/contracts/agent.contract.ts +24 -3
  315. package/src/contracts/needs.contract.ts +2 -2
  316. package/src/contracts/offload.contract.ts +21 -1
  317. package/src/contracts/privacy.contract.ts +13 -0
  318. package/src/contracts/runner.contract.ts +10 -2
  319. package/src/contracts/settings.contract.ts +23 -0
  320. package/src/events/agent-events.ts +13 -0
  321. package/src/events/agent-words.ts +2 -3
  322. package/src/events/resume.test.ts +14 -2
  323. package/src/events/resume.ts +32 -3
  324. package/src/events/sandbox-notice.ts +31 -1
  325. package/src/events/transcript.ts +27 -2
  326. package/src/ids/tunnel-ids.test.ts +30 -0
  327. package/src/index.ts +12 -0
  328. package/src/models/agent-catalog.test.ts +4 -3
  329. package/src/models/agent-catalog.ts +10 -7
  330. package/src/models/agent-runtimes.ts +30 -1
  331. package/src/models/capability-ledger.test.ts +4 -0
  332. package/src/models/model-order.ts +13 -0
  333. package/src/models/model-pins.ts +5 -4
  334. package/src/models/model-roles.test.ts +9 -31
  335. package/src/models/model-roles.ts +45 -50
  336. package/src/models/model-setups.test.ts +100 -0
  337. package/src/models/model-setups.ts +151 -0
  338. package/src/models/plan-pools.test.ts +58 -0
  339. package/src/models/plan-pools.ts +38 -7
  340. package/src/models/provider-specs.ts +20 -2
  341. package/src/policy/command-classes.ts +34 -3
  342. package/src/policy/control-scopes.ts +27 -0
  343. package/src/policy/credential-material.ts +58 -16
  344. package/src/policy/device-programs.ts +137 -0
  345. package/src/policy/identity-assertion.ts +130 -0
  346. package/src/policy/sandbox-power.ts +247 -0
  347. package/src/policy/search-globs.ts +135 -0
  348. package/src/policy/secret-hosts.ts +7 -0
  349. package/src/policy/tests/command-classes.test.ts +35 -0
  350. package/src/policy/tests/credential-material.test.ts +20 -0
  351. package/src/policy/tests/device-programs.test.ts +107 -0
  352. package/src/policy/tests/identity-assertion.test.ts +79 -0
  353. package/src/policy/tests/sandbox-power.test.ts +181 -0
  354. package/src/policy/tests/search-globs.test.ts +46 -1
  355. package/src/policy/tests/secret-hosts.test.ts +17 -1
  356. package/src/policy/turned-away.ts +2 -0
  357. package/src/protocol/doors/runner-protocol.ts +21 -1
  358. package/src/protocol/pairing-redeem.test.ts +86 -0
  359. package/src/protocol/pairing-redeem.ts +79 -0
  360. package/src/protocol/peer-mcp-server.ts +1 -2
  361. package/src/protocol/raw/raw-json-routes.test.ts +87 -0
  362. package/src/protocol/raw/raw-json-routes.ts +127 -5
  363. package/src/protocol/raw/raw-routes.ts +11 -0
  364. package/src/protocol/sse.test.ts +50 -1
  365. package/src/protocol/sse.ts +172 -26
  366. package/src/schemas/agent.ts +23 -2
  367. package/src/schemas/automations.ts +16 -5
  368. package/src/schemas/daemon-health.test.ts +28 -0
  369. package/src/schemas/device-swaps.test.ts +39 -0
  370. package/src/schemas/device-swaps.ts +26 -0
  371. package/src/schemas/devices.ts +11 -2
  372. package/src/schemas/engines.ts +1 -1
  373. package/src/schemas/environment.ts +4 -0
  374. package/src/schemas/git/git-scope.test.ts +91 -0
  375. package/src/schemas/git/git-scope.ts +71 -0
  376. package/src/schemas/git/git.ts +15 -1
  377. package/src/schemas/hosts.ts +7 -0
  378. package/src/schemas/keep-warm.ts +2 -1
  379. package/src/schemas/logs.ts +25 -0
  380. package/src/schemas/members.ts +8 -0
  381. package/src/schemas/metrics.test.ts +80 -0
  382. package/src/schemas/metrics.ts +31 -9
  383. package/src/schemas/needs.ts +80 -4
  384. package/src/schemas/offload.ts +76 -0
  385. package/src/schemas/privacy.ts +27 -3
  386. package/src/schemas/project-bring-back.test.ts +15 -0
  387. package/src/schemas/project-bring-back.ts +78 -0
  388. package/src/schemas/providers/handoff.test.ts +42 -0
  389. package/src/schemas/providers/handoff.ts +128 -0
  390. package/src/schemas/providers/plan-limits.ts +14 -1
  391. package/src/schemas/providers/usage.ts +13 -7
  392. package/src/schemas/restart-intent.test.ts +45 -0
  393. package/src/schemas/restart-intent.ts +47 -0
  394. package/src/schemas/run-targets.ts +5 -2
  395. package/src/schemas/settings-history.ts +28 -0
  396. package/src/schemas/settings.test.ts +74 -21
  397. package/src/schemas/settings.ts +80 -44
  398. package/src/schemas/short-codes.ts +59 -0
  399. package/src/schemas/speech.ts +4 -0
  400. package/src/schemas/system.ts +106 -5
  401. package/src/schemas/voice.ts +161 -0
  402. package/src/schemas/workspace/workspace-upload.ts +11 -0
  403. package/src/state/arrival.ts +3 -0
  404. package/src/state/history-state.ts +9 -0
  405. package/src/state/runtime-state.test.ts +13 -0
  406. package/src/state/runtime-state.ts +11 -5
  407. package/src/state/update-probation.test.ts +45 -0
  408. package/src/state/update-probation.ts +169 -0
  409. package/src/state/versions.test.ts +30 -1
  410. package/src/state/versions.ts +68 -0
  411. package/src/state/workspace-state.ts +4 -1
  412. package/src/text/embed.test.ts +14 -0
  413. package/src/text/embed.ts +19 -10
  414. package/src/text/title.ts +5 -9
  415. package/src/text/transcript-fold.test.ts +61 -0
  416. package/src/text/transcript-fold.ts +31 -6
@@ -1,36 +1,166 @@
1
- // Low-level SSE framing for the daemon's streamed routes (oRPC eventIterator over HTTP): frames separated by
2
- // a blank line, each carrying one `data: <JSON>` line. Protocol-only (no domain shapes), shared by every
3
- // consumer of the wire: the web's chat/intentic streams and the ACP bridge's daemon client.
1
+ // Server-Sent Events as the WHATWG event-stream grammar defines them, read off a byte stream that may be cut anywhere:
2
+ // inside a UTF-8 character, inside a `\r\n`, inside an event. Protocol-only (no domain shapes). Its readers: the ACP
3
+ // bridge and the extension SDK on the daemon's streamed routes, the privacy gateway's restorers, and the daemon's own
4
+ // MCP calls (an app's tool calls, a capability probe's `initialize`). Each had a parser of its own, and they disagreed:
5
+ // this one used to split on `\n\n` only and read the first `data:` line of an event, where the grammar allows `\r\n`
6
+ // and `\r` and joins every data line with `\n`. The webchat widget reads its replies with it too, through the `./sse`
7
+ // subpath: a bundle on customers' pages may take only modules with no imports, as this one is and must stay.
4
8
 
5
9
  // A live daemon writes to a stream at least this often (oRPC's keep-alive comment every 5 s, a heartbeat on the rest), so
6
10
  // this long with no bytes means the connection is dead even though nothing closed it.
7
11
  export const SSE_IDLE_MS = 120_000;
8
12
 
9
- // Yields each raw SSE frame (the text between blank-line separators) as it arrives, reassembling frames split
10
- // across network chunks. Ends, cancelling the reader, after `idleMs` of silence, so a reader of a daemon that went quiet
11
- // without closing (a sleeping laptop's tunnel, a wedged proxy) finishes instead of hanging.
12
- export async function* sseFrames(body: ReadableStream<Uint8Array>, idleMs: number = SSE_IDLE_MS): AsyncGenerator<string> {
13
+ export interface SseEvent {
14
+ // The `event:` field, undefined when the event named none (a listener reads that as "message").
15
+ readonly event: string | undefined;
16
+ // The data lines joined with `\n`; undefined for a block with none (a comment, a keep-alive, a stray blank line).
17
+ readonly data: string | undefined;
18
+ // The last event id the stream has set, as EventSource carries it: it persists across events until set again.
19
+ readonly id: string | undefined;
20
+ // The reconnection time an event asked for, in milliseconds.
21
+ readonly retry: number | undefined;
22
+ // The event's own text as it arrived, its terminating blank line included, for a reader that forwards it unchanged.
23
+ readonly raw: string;
24
+ }
25
+
26
+ export interface SseParser {
27
+ readonly feed: (text: string) => void;
28
+ // The stream ended: an event it ended inside is dispatched as it stands.
29
+ readonly end: () => void;
30
+ }
31
+
32
+ interface Pending {
33
+ raw: string;
34
+ name: string | undefined;
35
+ data: string[] | undefined;
36
+ retry: number | undefined;
37
+ }
38
+
39
+ const fresh = (): Pending => ({ raw: ``, name: undefined, data: undefined, retry: undefined });
40
+
41
+ // Fields per the grammar: `field: value` (one leading space dropped), `:` starts a comment, a blank line ends the event.
42
+ // Every block is dispatched, a comment-only one too, so a reader forwarding the stream loses nothing; a reader of
43
+ // messages skips the ones whose `data` is undefined.
44
+ export const createSseParser = (dispatch: (event: SseEvent) => void): SseParser => {
45
+ let buffer = ``;
46
+ // How far into `buffer` is known to hold no terminator, so a long line arriving in small chunks is scanned once.
47
+ let scanned = 0;
48
+ let started = false;
49
+ let lastId: string | undefined;
50
+ let pending = fresh();
51
+
52
+ const emit = (): void => {
53
+ dispatch({ event: pending.name, data: pending.data?.join(`\n`), id: lastId, retry: pending.retry, raw: pending.raw });
54
+ pending = fresh();
55
+ };
56
+
57
+ const field = (name: string, value: string): void => {
58
+ if (name === `event`) {
59
+ pending.name = value;
60
+ } else if (name === `data`) {
61
+ pending.data = [...(pending.data ?? []), value];
62
+ } else if (name === `id` && !value.includes(`\0`)) {
63
+ lastId = value;
64
+ } else if (name === `retry` && /^\d+$/.test(value)) {
65
+ pending.retry = Number(value);
66
+ }
67
+ };
68
+
69
+ const line = (text: string, terminator: string): void => {
70
+ pending.raw += text + terminator;
71
+ if (text === ``) {
72
+ emit();
73
+ return;
74
+ }
75
+ if (text.startsWith(`:`)) {
76
+ return;
77
+ }
78
+ const colon = text.indexOf(`:`);
79
+ field(colon < 0 ? text : text.slice(0, colon), colon < 0 ? `` : text.slice(colon + (text.charAt(colon + 1) === ` ` ? 2 : 1)));
80
+ };
81
+
82
+ const lines = (final: boolean): void => {
83
+ const terminators = /\r\n|\r|\n/gu;
84
+ terminators.lastIndex = scanned;
85
+ let start = 0;
86
+ for (let found = terminators.exec(buffer); found !== null; found = terminators.exec(buffer)) {
87
+ // A `\r` that ends the buffer may be the first half of a `\r\n` still in flight.
88
+ if (found[0] === `\r` && found.index === buffer.length - 1 && !final) {
89
+ break;
90
+ }
91
+ line(buffer.slice(start, found.index), found[0]);
92
+ start = found.index + found[0].length;
93
+ }
94
+ buffer = buffer.slice(start);
95
+ scanned = Math.max(0, buffer.length - 1);
96
+ };
97
+
98
+ return {
99
+ feed: (text) => {
100
+ let next = text;
101
+ // The grammar drops one byte-order mark at the very start; it stays in the first event's raw text.
102
+ if (!started && next !== ``) {
103
+ started = true;
104
+ if (next.startsWith(`\uFEFF`)) {
105
+ pending.raw += `\uFEFF`;
106
+ next = next.slice(1);
107
+ }
108
+ }
109
+ buffer += next;
110
+ lines(false);
111
+ },
112
+ end: () => {
113
+ lines(true);
114
+ if (buffer !== ``) {
115
+ line(buffer, ``);
116
+ buffer = ``;
117
+ }
118
+ if (pending.raw !== ``) {
119
+ emit();
120
+ }
121
+ },
122
+ };
123
+ };
124
+
125
+ // Every event in a body already read whole (a short MCP answer), the last one dispatched even without its blank line.
126
+ export const parseSseText = (text: string): SseEvent[] => {
127
+ const events: SseEvent[] = [];
128
+ const parser = createSseParser((event) => events.push(event));
129
+ parser.feed(text);
130
+ parser.end();
131
+ return events;
132
+ };
133
+
134
+ // Yields each event as it arrives. Ends, cancelling the reader, after `idleMs` of silence, so a reader of a daemon that
135
+ // went quiet without closing (a sleeping laptop's tunnel, a wedged proxy) finishes instead of hanging.
136
+ export async function* sseEvents(body: ReadableStream<Uint8Array>, idleMs: number = SSE_IDLE_MS): AsyncGenerator<SseEvent> {
13
137
  const reader = body.getReader();
14
138
  const decoder = new TextDecoder();
15
- let buffer = "";
139
+ let ready: SseEvent[] = [];
140
+ const parser = createSseParser((event) => ready.push(event));
16
141
  try {
17
142
  for (;;) {
18
143
  let timer: ReturnType<typeof setTimeout> | undefined;
19
- const idle = new Promise<"idle">((resolve) => {
20
- timer = setTimeout(() => resolve("idle"), idleMs);
144
+ const idle = new Promise<`idle`>((resolve) => {
145
+ timer = setTimeout(() => resolve(`idle`), idleMs);
21
146
  });
22
147
  // oxlint-disable-next-line eslint/no-await-in-loop -- streaming reader; each chunk must be awaited in order as it arrives
23
148
  const result = await Promise.race([reader.read(), idle]);
24
149
  clearTimeout(timer);
25
- if (result === "idle" || result.done) {
150
+ if (result === `idle`) {
26
151
  return;
27
152
  }
28
- buffer += decoder.decode(result.value, { stream: true });
29
- let separator = buffer.indexOf("\n\n");
30
- while (separator !== -1) {
31
- yield buffer.slice(0, separator);
32
- buffer = buffer.slice(separator + 2);
33
- separator = buffer.indexOf("\n\n");
153
+ if (result.done) {
154
+ parser.feed(decoder.decode());
155
+ parser.end();
156
+ } else {
157
+ parser.feed(decoder.decode(result.value, { stream: true }));
158
+ }
159
+ const events = ready;
160
+ ready = [];
161
+ yield* events;
162
+ if (result.done) {
163
+ return;
34
164
  }
35
165
  }
36
166
  } finally {
@@ -39,20 +169,36 @@ export async function* sseFrames(body: ReadableStream<Uint8Array>, idleMs: numbe
39
169
  }
40
170
  }
41
171
 
42
- // The parsed JSON payload of a frame's `data:` line, or undefined for a frame with no data line, an empty
43
- // payload, or malformed JSON (all cases the callers skip).
44
- export const sseData = (frame: string): unknown => {
45
- const dataLine = frame.split("\n").find((line) => line.startsWith("data:"));
46
- if (dataLine === undefined) {
47
- return undefined;
172
+ // Each event's text as it arrived, without the blank line that ended it: for a reader that keeps the frame and reads
173
+ // it with `sseData`. The same idle window as `sseEvents`.
174
+ export async function* sseFrames(body: ReadableStream<Uint8Array>, idleMs: number = SSE_IDLE_MS): AsyncGenerator<string> {
175
+ for await (const event of sseEvents(body, idleMs)) {
176
+ yield event.raw.replace(/(?:\r\n|\r|\n)+$/u, ``);
48
177
  }
49
- const payload = dataLine.slice(5).trim();
50
- if (payload.length === 0) {
178
+ }
179
+
180
+ // What a data payload parses to, before a caller's schema reads it.
181
+ export type SseJsonValue = string | number | boolean | null | readonly SseJsonValue[] | { readonly [key: string]: SseJsonValue };
182
+
183
+ // An event's data as JSON, or undefined for an event with no data, an empty payload, or malformed JSON (all cases
184
+ // the callers skip).
185
+ export const sseJson = (event: SseEvent): SseJsonValue | undefined => {
186
+ if (event.data === undefined || event.data.trim() === ``) {
51
187
  return undefined;
52
188
  }
53
189
  try {
54
- return JSON.parse(payload);
190
+ // SAFETY: JSON.parse returns only JSON values; each caller's schema reads the shape it expects.
191
+ return JSON.parse(event.data) as SseJsonValue;
55
192
  } catch {
56
193
  return undefined; // Skip a malformed frame.
57
194
  }
58
195
  };
196
+
197
+ // The same, for a frame's text as `sseFrames` yields it.
198
+ export const sseData = (frame: string): SseJsonValue | undefined => {
199
+ const event = parseSseText(frame).find((parsed) => parsed.data !== undefined);
200
+ return event === undefined ? undefined : sseJson(event);
201
+ };
202
+
203
+ // The `event:` name of a frame's text, for a reader holding the frame rather than the parsed event.
204
+ export const sseEventName = (frame: string): string | undefined => parseSseText(frame)[0]?.event;
@@ -128,6 +128,10 @@ export const ForkedFromSchema = z.object({
128
128
  });
129
129
  export type ForkedFrom = z.infer<typeof ForkedFromSchema>;
130
130
  // The turn's fields before the cross-field refinements below, so a subset (TurnProfileSchema) can be picked from them.
131
+ // How many files one turn may carry. A batch of a person's queued messages joins their files, so the admission splits a
132
+ // batch rather than build a turn past this (turn-admission.ts, together): a turn the daemon makes must parse back.
133
+ export const ATTACHMENT_LIMIT = 20;
134
+
131
135
  const AgentTurnFieldsSchema = z.object({
132
136
  prompt: z.string().describe("What to say to the agent. May be empty if you are only attaching files."),
133
137
  errand: TurnErrandSchema.optional().describe(
@@ -151,7 +155,7 @@ const AgentTurnFieldsSchema = z.object({
151
155
  // Already uploaded via /workspace/upload; Claude reads them via Read, Codex gets images as native input.
152
156
  attachments: z
153
157
  .array(z.string().min(1))
154
- .max(20)
158
+ .max(ATTACHMENT_LIMIT)
155
159
  .optional()
156
160
  .describe("Files to hand the agent along with the prompt, as workspace paths. Upload them first."),
157
161
  // Read out of the prompt's own `@path` tokens rather than chosen: a tokenizer over pasted text guesses, so a
@@ -450,7 +454,7 @@ export const runPickOf = (choice: {
450
454
  ...(choice.thinking === undefined ? {} : { thinking: choice.thinking }),
451
455
  ...(choice.fast === undefined ? {} : { fast: choice.fast }),
452
456
  });
453
- /* ONE ENTRY OF ONE ROLE'S MODEL LIST (settings.modelRoles): the standing version of the pick above, and not merely which model but HOW it is to be run. */
457
+ /* ONE ENTRY OF ONE SETUP'S MODEL LIST (settings.modelSetups): the standing version of the pick above, and not merely which model but HOW it is to be run. */
454
458
  export const ModelPinSchema = z.object({
455
459
  provider: AgentProviderSchema.describe("Which provider serves this work."),
456
460
  model: z.string().min(1).describe("Which of its models. Both halves, because a model name only means anything to the provider that serves it."),
@@ -538,12 +542,29 @@ export const ConversationQueueSchema = z.object({
538
542
  ),
539
543
  });
540
544
  export type ConversationQueue = z.infer<typeof ConversationQueueSchema>;
545
+ // What taking a waiting message back left: the queue, and whether the conversation went with the message. A conversation
546
+ // its scheduled message opened, in which nothing has happened since, exists only for that message.
547
+ export const QueueTakenBackSchema = ConversationQueueSchema.extend({
548
+ unopened: z
549
+ .boolean()
550
+ .optional()
551
+ .describe(
552
+ "True when the message was the only thing in a conversation it had opened by being scheduled: no turn ran and nothing else waits. The conversation went with it, so its card is off the board and its id is free to open again. Absent otherwise, and from sandboxes older than the field.",
553
+ ),
554
+ });
555
+ export type QueueTakenBack = z.infer<typeof QueueTakenBackSchema>;
541
556
  // What resuming a queue did: started a turn with what waited, when nothing else ran.
542
557
  export const QueueResumedSchema = z.object({
543
558
  run: z.string().optional().describe("The turn the waiting messages started. Absent when a turn was already running, and they go after it."),
544
559
  queue: ConversationQueueSchema.optional().describe(
545
560
  "The queue the release left: what still waits once the turn took what it could. Absent from sandboxes older than the field.",
546
561
  ),
562
+ reason: z
563
+ .string()
564
+ .optional()
565
+ .describe(
566
+ "Why nothing went out though messages still wait, in a sentence: a spent allowance whose window is shut, a recovery the sandbox runs first, a paused queue. Absent when something went out, when they simply go after a running turn, and from sandboxes older than the field.",
567
+ ),
547
568
  });
548
569
  export type QueueResumed = z.infer<typeof QueueResumedSchema>;
549
570
  // Attaches to a conversation's newest run (live, or finished within retention); no cursor to resume from, the head
@@ -180,11 +180,15 @@ export type AutomationTarget = z.infer<typeof AutomationTargetSchema>;
180
180
  export const FireOnSchema = z.enum(["pass", "change"]);
181
181
  export type FireOn = z.infer<typeof FireOnSchema>;
182
182
  // What the daemon remembers of a watch between checks (.intentic/records/automation-watch.json), and what the list
183
- // shows under it. Values are kept whole up to a bound, so the next check can say what changed.
183
+ // shows under it. Values are kept whole up to a bound, so the next check can say what changed; whether anything changed
184
+ // is decided on the digest of the whole value, so a page longer than the bound is not "changed" on every check.
184
185
  export const WatchStateSchema = z.object({
185
186
  armedAt: z.number().describe("When it was first checked, in milliseconds."),
186
187
  checkedAt: z.number().describe("When it was last checked, in milliseconds."),
187
- value: z.string().optional().describe("What the last passing check saw. Absent until one has passed."),
188
+ value: z.string().optional().describe("What the last passing check saw, cut to a bound. Absent until one has passed."),
189
+ digest: z.string().optional().describe("sha256 of the whole of what the last passing check saw, before any cut."),
190
+ // A watch edited to check something else starts over: what the old check saw says nothing about the new one.
191
+ condition: z.string().optional().describe("Which check this state belongs to: a digest of the guard or source it was recorded for."),
188
192
  changedAt: z.number().optional().describe("When what it saw last changed, in milliseconds."),
189
193
  firedAt: z.number().optional().describe("When it last set something off, in milliseconds."),
190
194
  // Why the last check did not pass, when it did not.
@@ -431,7 +435,7 @@ export const AutomationSchema = z.object({
431
435
  enabled: z.boolean().describe("Whether it fires at all."),
432
436
  });
433
437
  export type Automation = z.infer<typeof AutomationSchema>;
434
- // A wake held for owner approval (.intentic/records/approvals/<id>.json); snapshots the trigger payload so an approved
438
+ // A wake held for owner approval (automation-holds/<id>.json on the history volume); snapshots the trigger payload so an approved
435
439
  // run replays exactly what fired, even across a daemon restart.
436
440
  export const AutomationApprovalSchema = z.object({
437
441
  id: entryId.describe("This waiting item's own id, which approving and rejecting take."),
@@ -472,7 +476,7 @@ export const AutomationApprovalSchema = z.object({
472
476
  .number()
473
477
  .optional()
474
478
  .describe(
475
- "When it goes ahead on its own, in milliseconds, for a hold that is only a delay. Absent for one that genuinely waits on a person.",
479
+ "When it goes ahead on its own, in milliseconds, for a hold that is only a delay. Absent for one that genuinely waits on a person, and for one the sandbox did not itself queue, which only a person releases.",
476
480
  ),
477
481
  });
478
482
  export type AutomationApproval = z.infer<typeof AutomationApprovalSchema>;
@@ -480,7 +484,14 @@ export type AutomationApproval = z.infer<typeof AutomationApprovalSchema>;
480
484
  // Whether a held wake waits on a person. A countdown hold (`autoRunAt`) releases itself, so counting it would ask the
481
485
  // owner about something already about to happen on its own.
482
486
  export const awaitsOwner = (wake: Pick<AutomationApproval, "autoRunAt">): boolean => wake.autoRunAt === undefined;
483
- export const AutomationApprovalsListSchema = z.object({ approvals: z.array(AutomationApprovalSchema).describe("Everything waiting for a yes.") });
487
+ export const AutomationApprovalsListSchema = z.object({
488
+ approvals: z.array(AutomationApprovalSchema).describe("Everything waiting for a yes."),
489
+ // Reported, never dropped: a file in the queue the sandbox cannot read would otherwise be a hold that never runs and never says why.
490
+ unreadable: z
491
+ .array(z.string())
492
+ .default([])
493
+ .describe("Files in the queue this sandbox could not read, by name. Each is a held wake that cannot be released until it is fixed or removed."),
494
+ });
484
495
  export const AutomationApprovalIdParamSchema = z.object({ id: z.string().describe("Which waiting item.") });
485
496
  export const AutomationRunSchema = z.object({
486
497
  at: z.number(),
@@ -0,0 +1,28 @@
1
+ import { type DaemonHealth, DaemonHealthReadingSchema, DaemonHealthSchema } from "./system.js";
2
+
3
+ const answer: DaemonHealth = {
4
+ ok: true,
5
+ sandboxId: "0123456789ab",
6
+ profile: "workspace",
7
+ boot: { ready: true, startedAt: 1, steps: [] },
8
+ state: { journal: "none", engine: 84 },
9
+ announce: { state: "registered", at: 2 },
10
+ reach: { state: "reachable", at: 3 },
11
+ reachedBy: "tunnel",
12
+ build: { version: "1.2.3" },
13
+ };
14
+
15
+ test("the daemon's answer parses whole as the contract's health", () => {
16
+ expect(DaemonHealthSchema.parse(answer)).toEqual(answer);
17
+ });
18
+
19
+ test("a reader takes any daemon's answer, a field of the wrong shape reading as missing", () => {
20
+ expect(DaemonHealthReadingSchema.parse({ ok: true, ready: false, boot: "starting", state: { journal: 7 }, reachedBy: "tunnel" })).toEqual({
21
+ ok: true,
22
+ ready: false,
23
+ boot: undefined,
24
+ state: { journal: undefined },
25
+ reachedBy: "tunnel",
26
+ });
27
+ expect(DaemonHealthReadingSchema.parse(answer)).toMatchObject({ sandboxId: "0123456789ab", boot: { ready: true }, announce: { state: "registered" } });
28
+ });
@@ -0,0 +1,39 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { packageRoot } from "@intentic/constants/node";
4
+ import { DeviceSwapsSchema } from "./device-swaps.js";
5
+
6
+ // The rows `ic sandbox swaps --json` answers, as examples: golden/swap-verdicts.json holds each of ic's records and the
7
+ // row it gives (the Rust test, _sandbox/ic/src/sandbox/swaps.rs, builds every one; the machine agent's fallback is held
8
+ // to the same rows). This is the TS half: the rows ic gives are the rows the contract reads, to the field.
9
+
10
+ interface VerdictCase {
11
+ readonly name: string;
12
+ readonly record: string;
13
+ readonly now: number;
14
+ readonly swap: Record<string, unknown> | null;
15
+ }
16
+
17
+ // SAFETY: the golden is this package's own file; the assertions below fail on any field it lacks.
18
+ const GOLDEN = JSON.parse(readFileSync(join(packageRoot(import.meta.url), "golden", "swap-verdicts.json"), "utf8")) as {
19
+ readonly slug: string;
20
+ readonly cases: readonly VerdictCase[];
21
+ };
22
+ const rows: unknown[] = GOLDEN.cases.flatMap((one) => (one.swap === null ? [] : [one.swap]));
23
+
24
+ test("every row ic gives for the golden records is a row the contract reads, field for field", () => {
25
+ expect(rows.length).toBeGreaterThan(8);
26
+ const read: unknown = DeviceSwapsSchema.parse(rows);
27
+ expect(read).toEqual(rows);
28
+ });
29
+
30
+ test("a row from a newer ic keeps the swap going on: an unknown phase and an extra field still read", () => {
31
+ const [row] = DeviceSwapsSchema.parse([{ slug: "work", phase: "staging", at: 5, cutoverInProgress: false, next: "later" }]);
32
+ expect(row).toMatchObject({ slug: "work", phase: "staging", at: 5, cutoverInProgress: false });
33
+ });
34
+
35
+ test("a field of the wrong type is a field that is not there, but a row without its verdict is no row", () => {
36
+ const [row] = DeviceSwapsSchema.parse([{ slug: "work", phase: "cutover", at: 5, alive: "soon", cutoverInProgress: true }]);
37
+ expect(row?.alive).toBeUndefined();
38
+ expect(DeviceSwapsSchema.safeParse([{ slug: "work", phase: "cutover", at: 5 }]).success).toBe(false);
39
+ });
@@ -0,0 +1,26 @@
1
+ import { z } from "zod";
2
+
3
+ // WHAT `ic sandbox swaps --json` ANSWERS, one row per sandbox whose channel record names a swap in flight or on
4
+ // probation. `ic` is the one reader of its own swap records: `cutoverInProgress` is its verdict (a live ic run's
5
+ // heartbeat, `swap_alive`, is fresh), and every other process asks for it here instead of judging the record again. The
6
+ // command reads ic's home and not Docker, so it answers while Docker is down. golden/swap-verdicts.json holds each
7
+ // record's row, and ic's Rust test and the machine agent's tests both read it.
8
+
9
+ // Read loosely: a `phase` a newer ic adds is still a swap going on, and a field of an unexpected shape is a field that
10
+ // is not there. A row that lacks its identity or its verdict is no row at all (the caller falls back to the records).
11
+ export const DeviceSwapSchema = z.looseObject({
12
+ slug: z.string().min(1),
13
+ // ic's word: `cutover` (the old container is parked, the new one not yet proven) or `probation` (it is being watched).
14
+ phase: z.string().min(1),
15
+ // When the cutover began, epoch milliseconds.
16
+ at: z.number().int().nonnegative(),
17
+ // The last heartbeat of the ic run doing the cutover.
18
+ alive: z.number().int().nonnegative().optional().catch(undefined),
19
+ // When a probation ends and the parked container is let go.
20
+ probationUntil: z.number().int().nonnegative().optional().catch(undefined),
21
+ // ic's verdict at the moment it answered: this cutover is being done right now by a live ic run.
22
+ cutoverInProgress: z.boolean(),
23
+ });
24
+ export type DeviceSwap = z.infer<typeof DeviceSwapSchema>;
25
+
26
+ export const DeviceSwapsSchema = z.array(DeviceSwapSchema);
@@ -101,6 +101,9 @@ export const DeviceSandboxSchema = z.object({
101
101
  // so this side adopted it for its rounds: `keptElsewhere` is left out, and these say whose it was and since when.
102
102
  adoptedFrom: z.string().optional(),
103
103
  keeperSilentSince: z.number().optional(),
104
+ // (2026-10-10) Stopped by the machine's keeper after a stretch nobody needed it (`ic sandbox sleep`), and started
105
+ // again by itself when somebody opens it. `running` is false while this is true.
106
+ asleep: z.boolean().optional(),
104
107
  });
105
108
  export type DeviceSandbox = z.infer<typeof DeviceSandboxSchema>;
106
109
  // One operation on one sandbox, streamed as lines ending in a `result` or `error` frame. `prepare` builds the pending
@@ -318,12 +321,18 @@ export const DeviceSandboxIdSchema = z
318
321
  .regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/);
319
322
  // A folder on the DEVICE for `sync-install` to keep in step. The only caller-supplied string that reaches a command
320
323
  // line, so its shape is the guard: `~`, an absolute POSIX path or a drive letter, and none of the characters that would
321
- // end the argument it sits in. `~` is expanded by the daemon, which is why `$` is not allowed to arrive here.
324
+ // end the argument it sits in. `~` is expanded by the daemon, which is why `$` is not allowed to arrive here. The line
325
+ // may be PowerShell, which reads the typographic quotes “ ” „ as double quotes and ‘ ’ ‚ ‛ as single quotes beside the
326
+ // ASCII ones, so those are refused too: the daemon quotes the folder, and this keeps a second guard where one quoter
327
+ // misses a character.
322
328
  export const DeviceLocalDirSchema = z
323
329
  .string()
324
330
  .min(1)
325
331
  .max(4096)
326
- .regex(/^(?:~|\/|[A-Za-z]:[\\/])[^"'`$;|&\n\r]*$/);
332
+ // The curly quotes as escapes: bun's transpiler rewrites a literal one in a regex to its escape and tsgo keeps it,
333
+ // so spelled literally the pattern reads differently from source and from dist, and contract.lock.json can match
334
+ // only one of them.
335
+ .regex(/^(?:~|\/|[A-Za-z]:[\\/])[^"'`$;|&\n\r\u201C\u201D\u201E\u2018\u2019\u201A\u201B]*$/);
327
336
  // A TCP port number, the one value a caller supplies that reaches a command line as a number rather than a string.
328
337
  export const PortNumberSchema = z.number().int().min(1).max(65535);
329
338
  export const DeviceCommandInputSchema = z.object({
@@ -4,7 +4,7 @@ import { z } from "zod";
4
4
  // An engine is the installed program a runtime (agent-catalog.ts) spawns or imports — the `claude` binary, `codex`
5
5
  // wrapper, `@cursor/sdk`, `opencode`, `cli-proxy-api` — not the loop itself; one engine can back several runtimes.
6
6
 
7
- export const ENGINE_IDS = ["claude", "codex", "cursor", "opencode", "translator"] as const;
7
+ export const ENGINE_IDS = ["claude", "codex", "cursor", "devin", "opencode", "translator"] as const;
8
8
  export type EngineId = (typeof ENGINE_IDS)[number];
9
9
  export const EngineIdSchema = z.enum(ENGINE_IDS);
10
10
 
@@ -216,3 +216,7 @@ export const BundleExportSchema = z.object({
216
216
  });
217
217
  export type BundleExport = z.infer<typeof BundleExportSchema>;
218
218
  export const BundleExportsSchema = z.object({ exports: z.array(BundleExportSchema) });
219
+ // POST /bundles names the export it started, which fills in as it packs.
220
+ export const BundleStartedSchema = z.object({ name: z.string() });
221
+ // POST /bundles/ticket: the short-lived credential a download navigation carries, since it cannot send a bearer header.
222
+ export const BundleTicketSchema = z.object({ ticket: z.string(), expiresAt: z.number() });
@@ -0,0 +1,91 @@
1
+ import { type GitChange, UNATTRIBUTED_ORIGIN } from "./git.js";
2
+ import { changeIsFrom, DISCARDABLE_SIDES, discardOutcome, isWholeRepo, scopedPaths, STAGEABLE_SIDES, UNSTAGEABLE_SIDES } from "./git-scope.js";
3
+
4
+ const change = (path: string, extra: Partial<GitChange> = {}): GitChange => ({ path, status: "modified", ...extra });
5
+
6
+ // Shape a scan produces: small enough to read, with something on every side.
7
+ const sides = {
8
+ conflicted: [change("merge.ts", { status: "conflicted" })],
9
+ staged: [change("staged.ts"), change("moved.ts", { status: "renamed", from: "old.ts" })],
10
+ unstaged: [change("edited.ts"), change("new.ts", { status: "added" })],
11
+ };
12
+
13
+ test("a scope with no side reads every side the verb can move, and no others", () => {
14
+ // Staged is deliberately absent from stageable: re-adding it would sweep in worktree edits made after the freeze.
15
+ expect(scopedPaths(sides, STAGEABLE_SIDES, {})).toEqual(["edited.ts", "new.ts", "merge.ts"]);
16
+ expect(scopedPaths(sides, UNSTAGEABLE_SIDES, {})).toEqual(["staged.ts", "moved.ts", "old.ts"]);
17
+ expect(scopedPaths(sides, DISCARDABLE_SIDES, {})).toEqual(["merge.ts", "staged.ts", "moved.ts", "old.ts", "edited.ts", "new.ts"]);
18
+ });
19
+
20
+ test("a scope narrows the verb's sides and can never widen them", () => {
21
+ expect(scopedPaths(sides, STAGEABLE_SIDES, { side: "unstaged" })).toEqual(["edited.ts", "new.ts"]);
22
+ expect(scopedPaths(sides, STAGEABLE_SIDES, { side: "conflicted" })).toEqual(["merge.ts"]);
23
+ // Staging the one side it can't move answers empty, not everything: the honest answer to an impossible request.
24
+ expect(scopedPaths(sides, STAGEABLE_SIDES, { side: "staged" })).toEqual([]);
25
+ });
26
+
27
+ test("both legs of a rename resolve together, so no verb acts on half a move", () => {
28
+ expect(scopedPaths(sides, UNSTAGEABLE_SIDES, { side: "staged" })).toContain("old.ts");
29
+ });
30
+
31
+ test("an origin scope keeps only what that conversation landed, either leg of a rename counting", () => {
32
+ const origins = { "edited.ts": ["agent-a"], "new.ts": ["agent-b"], "old.ts": ["agent-a"] };
33
+ expect(scopedPaths(sides, STAGEABLE_SIDES, { origin: "agent-a" }, origins)).toEqual(["edited.ts"]);
34
+ // `moved.ts` attributes through the path it came from, the leg the land recorded.
35
+ expect(scopedPaths(sides, UNSTAGEABLE_SIDES, { origin: "agent-a" }, origins)).toEqual(["moved.ts", "old.ts"]);
36
+ // A side and an origin compose as an intersection, not either alone.
37
+ expect(scopedPaths(sides, DISCARDABLE_SIDES, { side: "unstaged", origin: "agent-b" }, origins)).toEqual(["new.ts"]);
38
+ // No matching attribution resolves to nothing, not a fallback to everyone's work.
39
+ expect(scopedPaths(sides, STAGEABLE_SIDES, { origin: "agent-a" })).toEqual([]);
40
+ });
41
+
42
+ // The Changes panel's "You" chip sends this; it used to resolve to nothing, so its Stage all, Discard and commit
43
+ // each moved no file at all.
44
+ test("the unattributed origin keeps exactly what no conversation landed, a rename counting only when neither leg was", () => {
45
+ const origins = { "edited.ts": ["agent-a"], "old.ts": ["agent-a"] };
46
+ expect(scopedPaths(sides, STAGEABLE_SIDES, { origin: UNATTRIBUTED_ORIGIN }, origins)).toEqual(["new.ts", "merge.ts"]);
47
+ // `moved.ts` came from a landed path, so it is the agent's work, not the owner's.
48
+ expect(scopedPaths(sides, UNSTAGEABLE_SIDES, { origin: UNATTRIBUTED_ORIGIN }, origins)).toEqual(["staged.ts"]);
49
+ // With nothing attributed, every row is the owner's.
50
+ expect(scopedPaths(sides, STAGEABLE_SIDES, { side: "unstaged", origin: UNATTRIBUTED_ORIGIN })).toEqual(["edited.ts", "new.ts"]);
51
+ });
52
+
53
+ test("a path listed on two sides at once resolves to one entry", () => {
54
+ const both = { conflicted: [], staged: [change("a.ts")], unstaged: [change("a.ts")] };
55
+ expect(scopedPaths(both, DISCARDABLE_SIDES, {})).toEqual(["a.ts"]);
56
+ });
57
+
58
+ test("a scope that narrows nothing is the whole repository", () => {
59
+ expect(isWholeRepo({})).toBe(true);
60
+ expect(isWholeRepo({ side: "unstaged" })).toBe(false);
61
+ expect(isWholeRepo({ origin: "agent-a" })).toBe(false);
62
+ });
63
+
64
+ // The panel's count, its discard question and the daemon's scope all read this one predicate, so a rename out of an
65
+ // agent's file cannot be "yours" in the question and the agent's in the action.
66
+ test("a row is an origin's when either leg was landed by it, and the owner's only when neither was", () => {
67
+ const origins = { "util.ts": ["agent-a"] };
68
+ const renamed = change("helpers.ts", { status: "renamed", from: "util.ts" });
69
+ expect(changeIsFrom(renamed, "agent-a", origins)).toBe(true);
70
+ expect(changeIsFrom(renamed, UNATTRIBUTED_ORIGIN, origins)).toBe(false);
71
+ expect(changeIsFrom(change("mine.ts"), UNATTRIBUTED_ORIGIN, origins)).toBe(true);
72
+ expect(changeIsFrom(change("mine.ts"), "agent-a", origins)).toBe(false);
73
+ });
74
+
75
+ test("a discard names what leaves the disk apart from what returns to its last commit", () => {
76
+ const changed = {
77
+ conflicted: [],
78
+ staged: [
79
+ change("helpers.ts", { status: "renamed", from: "util.ts" }),
80
+ change("fresh.ts", { status: "added" }),
81
+ change("gone.ts", { status: "deleted" }),
82
+ ],
83
+ // `gone.ts` deleted in the index and recreated on disk reads untracked, yet the last commit still holds it.
84
+ unstaged: [change("edited.ts"), change("scratch.ts", { status: "added" }), change("gone.ts", { status: "added" })],
85
+ };
86
+ const paths = scopedPaths(changed, DISCARDABLE_SIDES, {});
87
+ expect(discardOutcome(changed, paths)).toEqual({
88
+ deletes: ["helpers.ts", "fresh.ts", "scratch.ts"],
89
+ restores: ["util.ts", "gone.ts", "edited.ts"],
90
+ });
91
+ });