@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
@@ -0,0 +1,86 @@
1
+ import { redeemWhileWarming, type RedeemRetry } from "./pairing-redeem.js";
2
+
3
+ /* The redeem loop both pairing peers run, over a sandbox the test plays one answer at a time. */
4
+
5
+ const scripted = (answers: readonly (number | "offline" | "hang")[]) => {
6
+ const tries: number[] = [];
7
+ const send = async (attempt: number, signal: AbortSignal | undefined): Promise<Response> => {
8
+ tries.push(attempt);
9
+ const next = answers[attempt - 1] ?? answers.at(-1) ?? "offline";
10
+ if (next === "offline") {
11
+ throw new TypeError("fetch failed");
12
+ }
13
+ if (next === "hang") {
14
+ return new Promise<Response>((_resolve, reject) => signal?.addEventListener("abort", () => reject(signal.reason)));
15
+ }
16
+ return new Response(`status ${next}`, { status: next });
17
+ };
18
+ return { tries, send };
19
+ };
20
+
21
+ const statusOf = (outcome: Awaited<ReturnType<typeof redeemWhileWarming>>): number | undefined =>
22
+ outcome.kind === "answered" ? outcome.response.status : undefined;
23
+
24
+ describe("redeemWhileWarming", () => {
25
+ it("answers the first reply below 500 without asking again", async () => {
26
+ const { tries, send } = scripted([200]);
27
+ expect(statusOf(await redeemWhileWarming(send, { attempts: 3, delayMs: 0 }))).toBe(200);
28
+ expect(tries).toEqual([1]);
29
+ });
30
+
31
+ it("asks again through a warming sandbox and an unreachable one, saying why each time", async () => {
32
+ const { tries, send } = scripted(["offline", 502, 200]);
33
+ const heard: { attempt: number; why: RedeemRetry["kind"] }[] = [];
34
+ const outcome = await redeemWhileWarming(send, { attempts: 5, delayMs: 0, onRetry: (attempt, why) => heard.push({ attempt, why: why.kind }) });
35
+ expect(statusOf(outcome)).toBe(200);
36
+ expect(tries).toEqual([1, 2, 3]);
37
+ expect(heard).toEqual([
38
+ { attempt: 1, why: "unreachable" },
39
+ { attempt: 2, why: "warming" },
40
+ ]);
41
+ });
42
+
43
+ it("hands back a 401 at once: an expired code is an answer, not a blip", async () => {
44
+ const { tries, send } = scripted([401, 200]);
45
+ expect(statusOf(await redeemWhileWarming(send, { attempts: 5, delayMs: 0 }))).toBe(401);
46
+ expect(tries).toEqual([1]);
47
+ });
48
+
49
+ it("answers the last 5xx when the sandbox is still warming after every try", async () => {
50
+ const { tries, send } = scripted([503]);
51
+ const heard: number[] = [];
52
+ const outcome = await redeemWhileWarming(send, { attempts: 3, delayMs: 0, onRetry: (attempt) => heard.push(attempt) });
53
+ expect(outcome.kind).toBe("answered");
54
+ expect(outcome.kind === "answered" ? await outcome.response.text() : undefined).toBe("status 503");
55
+ expect(tries).toEqual([1, 2, 3]);
56
+ // The last try is not retried, so it is not narrated as one.
57
+ expect(heard).toEqual([1, 2]);
58
+ });
59
+
60
+ it("is unreachable, with the last try's failure, when nothing ever answered", async () => {
61
+ const { tries, send } = scripted(["offline"]);
62
+ const outcome = await redeemWhileWarming(send, { attempts: 2, delayMs: 0 });
63
+ expect(outcome.kind).toBe("unreachable");
64
+ expect(outcome.kind === "unreachable" ? outcome.error : undefined).toBeInstanceOf(TypeError);
65
+ expect(tries).toEqual([1, 2]);
66
+ });
67
+
68
+ it("abandons a try that outlives its timeout and asks again", async () => {
69
+ const { tries, send } = scripted(["hang", 200]);
70
+ expect(statusOf(await redeemWhileWarming(send, { attempts: 2, delayMs: 0, timeoutMs: 10 }))).toBe(200);
71
+ expect(tries).toEqual([1, 2]);
72
+ });
73
+
74
+ it("rejects with the stop's reason rather than answering when its signal aborts", async () => {
75
+ const stop = new AbortController();
76
+ const { tries, send } = scripted([502]);
77
+ const running = redeemWhileWarming(send, {
78
+ attempts: 5,
79
+ delayMs: 60_000,
80
+ signal: stop.signal,
81
+ onRetry: () => stop.abort(new Error("stopped")),
82
+ });
83
+ await expect(running).rejects.toThrow("stopped");
84
+ expect(tries).toEqual([1]);
85
+ });
86
+ });
@@ -0,0 +1,79 @@
1
+ import { retry, withDeadline } from "@intentic/base/async";
2
+
3
+ // Redeeming a pairing code's one-time token, as every peer that takes one does it (the machine agent's
4
+ // `postWhileWarming`, the webext's `redeemPairing`): a sandbox still warming up answers 5xx through its tunnel, or
5
+ // nothing at all, and is asked again. Any other answer, a 401 above all (the code has expired), is the caller's to read
6
+ // at once: asking again would only delay the fresh code the person has to fetch.
7
+
8
+ // Why a try is being asked again: no answer at all, or a 5xx from a sandbox not ready yet.
9
+ export type RedeemRetry = { readonly kind: "unreachable"; readonly error: unknown } | { readonly kind: "warming"; readonly status: number };
10
+
11
+ export type RedeemOutcome =
12
+ // The sandbox's answer: below 500 at once, or still 5xx when the tries ran out.
13
+ | { readonly kind: "answered"; readonly response: Response }
14
+ // No answer at all on the last try; `error` is that try's failure.
15
+ | { readonly kind: "unreachable"; readonly error: unknown };
16
+
17
+ export interface RedeemOptions {
18
+ // Tries in all, the first included.
19
+ readonly attempts: number;
20
+ // The wait between tries.
21
+ readonly delayMs: number;
22
+ // How long one try may take before it is abandoned and counts as unreachable; none when absent.
23
+ readonly timeoutMs?: number | undefined;
24
+ // Stops the tries: a stop rejects with the signal's reason rather than answering.
25
+ readonly signal?: AbortSignal | undefined;
26
+ // Hears each try that is about to be asked again, before the wait.
27
+ readonly onRetry?: ((attempt: number, why: RedeemRetry) => void) | undefined;
28
+ }
29
+
30
+ // A 5xx carried out of a try so `retry` asks again, and handed back as the answer when the tries run out.
31
+ class Warming extends Error {
32
+ constructor(readonly response: Response) {
33
+ super(`the sandbox is warming up (HTTP ${response.status})`);
34
+ }
35
+ }
36
+
37
+ // Runs `send` (one POST, handed the try's number and a signal to pass to its fetch, undefined when there is neither a
38
+ // deadline nor a stop) until the sandbox answers below 500 or the tries run out.
39
+ export const redeemWhileWarming = async (
40
+ send: (attempt: number, signal: AbortSignal | undefined) => Promise<Response>,
41
+ options: RedeemOptions,
42
+ ): Promise<RedeemOutcome> => {
43
+ const { attempts, delayMs, timeoutMs, signal, onRetry } = options;
44
+ const once = (attempt: number): Promise<Response> =>
45
+ timeoutMs === undefined ? send(attempt, signal) : withDeadline((bounded) => send(attempt, bounded), timeoutMs, { signal });
46
+ try {
47
+ const response = await retry(
48
+ async (attempt) => {
49
+ let answer: Response;
50
+ try {
51
+ answer = await once(attempt);
52
+ } catch (error) {
53
+ if (attempt < attempts && signal?.aborted !== true) {
54
+ onRetry?.(attempt, { kind: "unreachable", error });
55
+ }
56
+ throw error;
57
+ }
58
+ if (answer.status < 500) {
59
+ return answer;
60
+ }
61
+ if (attempt < attempts) {
62
+ onRetry?.(attempt, { kind: "warming", status: answer.status });
63
+ }
64
+ throw new Warming(answer);
65
+ },
66
+ { attempts, delayMs: () => delayMs, signal },
67
+ );
68
+ return { kind: "answered", response };
69
+ } catch (error) {
70
+ if (signal?.aborted === true) {
71
+ // oxlint-disable-next-line typescript/only-throw-error -- the stop's own reason, as an aborted fetch rejects with it
72
+ throw signal.reason;
73
+ }
74
+ if (error instanceof Warming) {
75
+ return { kind: "answered", response: error.response };
76
+ }
77
+ return { kind: "unreachable", error };
78
+ }
79
+ };
@@ -2,6 +2,7 @@
2
2
  // namespace keeps zod's 60 locales (~250 kB) that esbuild can otherwise drop. _devices/webext/scripts/size-budget.mjs holds the ceiling.
3
3
  import { prettifyError, toJSONSchema } from "zod";
4
4
  import type * as z from "zod";
5
+ import { isRecord } from "@intentic/base/escape";
5
6
 
6
7
  // The MCP version every peer announces, here rather than in host-protocol.ts because a browser extension bundles this
7
8
  // module and would otherwise carry the machine handshake's schemas to a store. Also read by the daemon's peer bridge,
@@ -99,8 +100,6 @@ export interface McpServerSpec<Ctx> {
99
100
  readonly audit: (entry: McpAuditEntry) => Promise<void> | void;
100
101
  }
101
102
 
102
- const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
103
-
104
103
  // Handles one JSON-RPC message; returns undefined for a notification. `ctx` is read per call, so a grant pushed
105
104
  // mid-session takes effect on the next call.
106
105
  export const createMcpServer = <Ctx>(spec: McpServerSpec<Ctx>): ((message: unknown, ctx: Ctx) => Promise<Record<string, unknown> | undefined>) => {
@@ -0,0 +1,87 @@
1
+ import { RAW_JSON_ROUTES, type RawJsonRoute } from "./raw-json-routes.js";
2
+ import { RAW_ROUTES, rawRouteUrl } from "./raw-routes.js";
3
+
4
+ // The table the editor calls the daemon's plain-JSON routes by: each entry is a route the daemon registers, says what it
5
+ // takes and answers, and parses what the editor reads off the wire.
6
+
7
+ const entries = Object.entries<RawJsonRoute>(RAW_JSON_ROUTES);
8
+ const keys = entries.map(([key]) => key);
9
+ const bodies = (route: RawJsonRoute): number => [route.input !== undefined, route.text === true, route.bytes === true].filter(Boolean).length;
10
+
11
+ describe(`RAW_JSON_ROUTES`, () => {
12
+ it(`names only routes the daemon registers, so a typed call can never aim at a path nothing serves`, () => {
13
+ expect(keys.filter((key) => !(key in RAW_ROUTES))).toEqual([]);
14
+ });
15
+
16
+ it(`gives a route one kind of body: JSON, a document, or bytes`, () => {
17
+ expect(entries.filter(([, route]) => bodies(route) > 1).map(([key]) => key)).toEqual([]);
18
+ });
19
+
20
+ it(`takes a body only on a route that writes: a read has none to send`, () => {
21
+ expect(entries.filter(([key, route]) => key.startsWith(`GET `) && bodies(route) > 0).map(([key]) => key)).toEqual([]);
22
+ });
23
+
24
+ it(`holds the routes the editor's rosters, access tab, uploads and dictation reach by key`, () => {
25
+ expect(keys).toEqual(
26
+ expect.arrayContaining([
27
+ `GET /system/hosts`,
28
+ `GET /system/webext`,
29
+ `GET /system/phones`,
30
+ `GET /system/runners`,
31
+ `GET /system/sync`,
32
+ `POST /system/sync/pair`,
33
+ `GET /system/passkeys`,
34
+ `POST /system/passkeys/register`,
35
+ `GET /system/control/tokens`,
36
+ `GET /bundles`,
37
+ `POST /arrivals/plan`,
38
+ `GET /speech/status`,
39
+ `POST /workspace/upload`,
40
+ `POST /workspace/upload-diff`,
41
+ ]),
42
+ );
43
+ });
44
+
45
+ it(`has a path every {param} of which is filled and encoded as one segment`, () => {
46
+ expect(rawRouteUrl(`DELETE /system/hosts/{id}`, { id: `my laptop/1` })).toBe(`/system/hosts/my%20laptop%2F1`);
47
+ expect(rawRouteUrl(`POST /system/runners/{id}/definition/sync`, { id: `r 1` })).toBe(`/system/runners/r%201/definition/sync`);
48
+ });
49
+ });
50
+
51
+ describe(`what the rosters and pairings answer`, () => {
52
+ it(`reads a peer's pairing from an older daemon, which sent a token and no code`, () => {
53
+ expect(RAW_JSON_ROUTES[`POST /system/webext/pair`].output.parse({ token: `iht_x` })).toEqual({ token: `iht_x` });
54
+ expect(RAW_JSON_ROUTES[`POST /system/hosts/pair`].output.parse({ token: `iht_x`, expiresIn: 600, code: `ABCD1234` })).toEqual({
55
+ token: `iht_x`,
56
+ expiresIn: 600,
57
+ code: `ABCD1234`,
58
+ });
59
+ });
60
+
61
+ it(`reads the sync pairing with the mode the daemon granted`, () => {
62
+ expect(RAW_JSON_ROUTES[`POST /system/sync/pair`].output.parse({ token: `t`, expiresIn: 600, code: `ABCD1234`, mode: `mirror` })).toMatchObject({ mode: `mirror` });
63
+ expect(() => RAW_JSON_ROUTES[`POST /system/sync/pair`].output.parse({ token: `t`, mode: `both` })).toThrow();
64
+ });
65
+
66
+ it(`reads a runner list, which two screens used to each describe themselves`, () => {
67
+ expect(RAW_JSON_ROUTES[`GET /system/runners`].output.parse({ runners: [{ id: `r1`, online: false, parity: `unknown` }] })).toEqual({
68
+ runners: [{ id: `r1`, online: false, parity: `unknown` }],
69
+ });
70
+ expect(() => RAW_JSON_ROUTES[`GET /system/runners`].output.parse({ runners: [{ id: `r1` }] })).toThrow();
71
+ });
72
+
73
+ it(`accepts the WebAuthn options only with the challenge the browser's own parser needs`, () => {
74
+ expect(RAW_JSON_ROUTES[`POST /system/passkeys/register/options`].output.safeParse({ challenge: `AwQ`, rp: { id: `app.test`, name: `work` } }).success).toBe(true);
75
+ expect(RAW_JSON_ROUTES[`POST /system/passkeys/register/options`].output.safeParse({ rp: { id: `app.test` } }).success).toBe(false);
76
+ expect(RAW_JSON_ROUTES[`POST /system/passkeys/assert/options`].output.safeParse({ challenge: `AQID`, available: true }).success).toBe(true);
77
+ });
78
+
79
+ it(`reads a session recovery as the session and the codes still unspent`, () => {
80
+ expect(RAW_JSON_ROUTES[`POST /system/session/recover`].output.parse({ token: `s`, expiresAt: 5, email: `a@example.com`, remaining: 3 })).toEqual({
81
+ token: `s`,
82
+ expiresAt: 5,
83
+ email: `a@example.com`,
84
+ remaining: 3,
85
+ });
86
+ });
87
+ });
@@ -1,6 +1,10 @@
1
1
  import type { z } from "zod";
2
+ import { ControlTokenMintedSchema, ControlTokenMintSchema, ControlTokensListSchema } from "../../policy/control-scopes.js";
2
3
  import { EngineChannelInputSchema, EngineRevertInputSchema, EnginesViewSchema, EngineUpdateInputSchema, EngineWriteResultSchema } from "../../schemas/engines.js";
3
4
  import {
5
+ BundleExportsSchema,
6
+ BundleStartedSchema,
7
+ BundleTicketSchema,
4
8
  EnvironmentApproveSchema,
5
9
  EnvironmentContentsSchema,
6
10
  EnvironmentRebuildWhenIdleSchema,
@@ -8,22 +12,64 @@ import {
8
12
  EnvironmentRuntimeDecisionSchema,
9
13
  EnvironmentSchema,
10
14
  } from "../../schemas/environment.js";
15
+ import { SyncStatusSchema } from "../../schemas/devices.js";
16
+ import { HostsListSchema } from "../../schemas/hosts.js";
17
+ import { VoicesViewSchema } from "../../schemas/voice.js";
11
18
  import { MemberEmailSchema, MemberGrantSchema, MembersListSchema, MembersRosterSchema } from "../../schemas/members.js";
19
+ import { PhonesListSchema, PhoneWakeRegistrationSchema } from "../../schemas/phone.js";
12
20
  import { OkSchema } from "../../schemas/shared.js";
21
+ import { EnrollCodeAnswerSchema, EnrollCodeRequestSchema, PeerPairingAnswerSchema, SyncPairAnswerSchema, SyncPairRequestSchema } from "../../schemas/short-codes.js";
22
+ import { SpeechStatusSchema, SpeechTranscriptSchema } from "../../schemas/speech.js";
23
+ import {
24
+ AuthenticationOptionsSchema,
25
+ DaemonSessionSchema,
26
+ PasskeyAssertRequestSchema,
27
+ PasskeyPolicyAnswerSchema,
28
+ PasskeyPolicySchema,
29
+ PasskeyRecoverRequestSchema,
30
+ PasskeyRegisteredSchema,
31
+ PasskeyRegisterRequestSchema,
32
+ PasskeysListSchema,
33
+ RecoveryCodesSchema,
34
+ RegistrationOptionsSchema,
35
+ SessionRecoveredSchema,
36
+ } from "../../schemas/system.js";
37
+ import { WebExtsListSchema } from "../../schemas/webext.js";
38
+ import { UploadManifestSchema, UploadSkipSchema } from "../../schemas/workspace/workspace-upload.js";
39
+ import {
40
+ ArrivalAbandonedSchema,
41
+ ArrivalApplySchema,
42
+ ArrivalHostsSchema,
43
+ ArrivalPlanSchema,
44
+ ArrivalReportSchema,
45
+ ArrivalScanSchema,
46
+ ProviderKeysAppliedSchema,
47
+ ProviderKeysApplySchema,
48
+ ProviderKeysSchema,
49
+ } from "../../state/arrival.js";
13
50
  import { DefinitionDiffSchema, DefinitionExportSchema, WorkspacePublishResultSchema, WorkspacePublishSchema, WorkspaceRemoteSchema } from "../../state/definition.js";
51
+ import { RunnerDefinitionAppliedSchema, RunnersListSchema } from "../doors/runner-protocol.js";
14
52
  import type { RawRouteKey } from "./raw-routes.js";
15
53
 
16
54
  // What a raw route that answers plain JSON takes and answers, under its RAW_ROUTES key. `input` is the JSON body a write
17
- // sends; `text` marks one whose body is a document rather than JSON (a sandbox.toml to diff). A caller reaches these by
18
- // key through a typed helper that parses the answer with `output`, so a hand-written route is as typed as a procedure.
19
- // A refusal is the daemon's `{ "error": … }` with a 4xx status on every one of them, outside these schemas.
55
+ // sends; `text` marks one whose body is a document rather than JSON (a sandbox.toml to diff), and `bytes` one whose body
56
+ // is the thing itself (a file's bytes, a WAV, a tar), which the caller hands over as a Blob, an ArrayBuffer or a string.
57
+ // A caller reaches these by key through a typed helper that parses the answer with `output`, so a hand-written route is
58
+ // as typed as a procedure. What a route takes in its query string (`?id=`, `?path=`) and in a header is the helper's
59
+ // `query` and `headers`, since the daemon reads them by name and there is nothing to parse. A refusal is the daemon's
60
+ // `{ "error": … }` with a 4xx status on every one of them, outside these schemas.
20
61
  export interface RawJsonRoute {
21
62
  readonly input?: z.ZodType;
22
63
  readonly text?: true;
64
+ readonly bytes?: true;
23
65
  readonly output: z.ZodType;
24
66
  }
25
67
 
26
68
  export const RAW_JSON_ROUTES = {
69
+ // The voices a sandbox speaks with, for the Voice settings (schemas/voice.ts).
70
+ "GET /speech/voices": { output: VoicesViewSchema },
71
+ // A server-connect code for the owner's command (short-codes.ts).
72
+ "POST /system/enroll/code": { input: EnrollCodeRequestSchema, output: EnrollCodeAnswerSchema },
27
73
  // The roster is ownership's; every write answers the roster after it.
28
74
  "GET /members": { output: MembersRosterSchema },
29
75
  "POST /members": { input: MemberGrantSchema, output: MembersListSchema },
@@ -46,6 +92,70 @@ export const RAW_JSON_ROUTES = {
46
92
  "POST /definition/diff": { text: true, output: DefinitionDiffSchema },
47
93
  "GET /definition/workspace": { output: WorkspaceRemoteSchema },
48
94
  "POST /definition/workspace/publish": { input: WorkspacePublishSchema, output: WorkspacePublishResultSchema },
95
+ // Sandboxes moving in: a plan read off an archive (`bytes`) or a connected device, applied by its token, and the
96
+ // model keys the owner's devices hold.
97
+ "GET /arrivals/hosts": { output: ArrivalHostsSchema },
98
+ "POST /arrivals/scan": { input: ArrivalScanSchema, output: ArrivalPlanSchema },
99
+ "POST /arrivals/plan": { bytes: true, output: ArrivalPlanSchema },
100
+ "POST /arrivals/apply": { input: ArrivalApplySchema, output: ArrivalReportSchema },
101
+ "GET /arrivals/keys": { output: ProviderKeysSchema },
102
+ "POST /arrivals/keys/apply": { input: ProviderKeysApplySchema, output: ProviderKeysAppliedSchema },
103
+ "DELETE /arrivals": { output: ArrivalAbandonedSchema },
104
+ // Exports: `?secrets=1` on the start, `?name=` on the delete and the ticket.
105
+ "GET /bundles": { output: BundleExportsSchema },
106
+ "POST /bundles": { output: BundleStartedSchema },
107
+ "DELETE /bundles": { output: OkSchema },
108
+ "POST /bundles/ticket": { output: BundleTicketSchema },
109
+ // Paired machines: each door's roster, the pairing minted for one id (`?id=`), and dropping one's enrollment. The
110
+ // devices' answer is `hosts`, the browsers' `browsers`, the phones' `phones`, the runners' `runners`.
111
+ "GET /system/hosts": { output: HostsListSchema },
112
+ "POST /system/hosts/pair": { output: PeerPairingAnswerSchema },
113
+ "DELETE /system/hosts/{id}": { output: OkSchema },
114
+ "GET /system/webext": { output: WebExtsListSchema },
115
+ "POST /system/webext/pair": { output: PeerPairingAnswerSchema },
116
+ "DELETE /system/webext/{id}": { output: OkSchema },
117
+ "GET /system/phones": { output: PhonesListSchema },
118
+ "POST /system/phones/pair": { output: PeerPairingAnswerSchema },
119
+ "DELETE /system/phones/{id}": { output: OkSchema },
120
+ "POST /system/phones/{id}/wake": { input: PhoneWakeRegistrationSchema, output: OkSchema },
121
+ "GET /system/runners": { output: RunnersListSchema },
122
+ "DELETE /system/runners/{id}": { output: OkSchema },
123
+ "POST /system/runners/{id}/definition/sync": { output: RunnerDefinitionAppliedSchema },
124
+ // Desktop sync: the sandbox-level state, the pairing (`?mode=mirror` asks for the narrower one), one machine's key.
125
+ "GET /system/sync": { output: SyncStatusSchema },
126
+ "POST /system/sync/pair": { input: SyncPairRequestSchema, output: SyncPairAnswerSchema },
127
+ "DELETE /system/authorized-key/{machine}": { output: OkSchema },
128
+ // Every browser session revoked, the owner's own included.
129
+ "POST /system/sessions/revoke": { output: OkSchema },
130
+ "GET /system/control/tokens": { output: ControlTokensListSchema },
131
+ "POST /system/control/tokens": { input: ControlTokenMintSchema, output: ControlTokenMintedSchema },
132
+ "DELETE /system/control/tokens/{id}": { output: OkSchema },
133
+ // One's own passkeys, the require rule and its recovery codes. The two registration calls carry the WebAuthn ceremony's
134
+ // options out and its response back, the shapes the browser's own parser reads and writes.
135
+ "GET /system/passkeys": { output: PasskeysListSchema },
136
+ "POST /system/passkeys/register/options": { output: RegistrationOptionsSchema },
137
+ "POST /system/passkeys/register": { input: PasskeyRegisterRequestSchema, output: PasskeyRegisteredSchema },
138
+ "POST /system/passkeys/policy": { input: PasskeyPolicySchema, output: PasskeyPolicyAnswerSchema },
139
+ "POST /system/passkeys/recovery": { output: RecoveryCodesSchema },
140
+ "DELETE /system/passkeys/{id}": { output: OkSchema },
141
+ // The sign-in's own calls, before any session exists: the assertion is how one is minted, and recovery is the owner's
142
+ // way back in with the code a policy handed out. Their credential is the door's (a challenge, a recovery code under the
143
+ // caller's own proof), so the editor sends them outside its session client (client/session/passkeySignIn.ts) and reads
144
+ // their answers with these schemas.
145
+ "POST /system/passkeys/assert/options": { output: AuthenticationOptionsSchema },
146
+ "POST /system/passkeys/assert": { input: PasskeyAssertRequestSchema, output: DaemonSessionSchema },
147
+ "POST /system/session/recover": { input: PasskeyRecoverRequestSchema, output: SessionRecoveredSchema },
148
+ // Dictation over HTTP, for a daemon without the phrase stream (`?lang=`): the model's readiness, arming it, and one
149
+ // phrase as WAV.
150
+ "GET /speech/status": { output: SpeechStatusSchema },
151
+ "POST /speech/prepare": { output: SpeechStatusSchema },
152
+ "POST /speech/transcribe": { bytes: true, output: SpeechTranscriptSchema },
153
+ // A file written into the tree (`?path=`, the body its bytes, `x-intentic-base-hash` refusing a stale save), and the
154
+ // pre-drop check of which dropped files are already there.
155
+ "POST /workspace/upload": { bytes: true, output: OkSchema },
156
+ // A dropped tree as one tar, extracted entry by entry.
157
+ "POST /workspace/upload-archive": { bytes: true, output: OkSchema },
158
+ "POST /workspace/upload-diff": { input: UploadManifestSchema, output: UploadSkipSchema },
49
159
  } as const satisfies Partial<Record<RawRouteKey, RawJsonRoute>>;
50
160
 
51
161
  export type RawJsonRouteKey = keyof typeof RAW_JSON_ROUTES;
@@ -53,9 +163,21 @@ export type RawJsonRouteKey = keyof typeof RAW_JSON_ROUTES;
53
163
  // What a JSON raw route answers, parsed.
54
164
  export type RawJsonOutput<Key extends RawJsonRouteKey> = z.output<(typeof RAW_JSON_ROUTES)[Key]["output"]>;
55
165
 
56
- // What a JSON raw route's body is: its input's shape, a string for a document body, undefined for one that takes none.
166
+ // The routes that take their body as bytes: a file, a WAV, a tar. Some of these also go up in parts with progress.
167
+ export type RawBytesRouteKey = {
168
+ [Key in RawJsonRouteKey]: (typeof RAW_JSON_ROUTES)[Key] extends { readonly bytes: true } ? Key : never;
169
+ }[RawJsonRouteKey];
170
+
171
+ // The body a `bytes` route takes: a Blob or typed array (a file, a tar), an ArrayBuffer (an encoded WAV) or a string
172
+ // (a saved file's text).
173
+ export type RawBytes = Blob | ArrayBuffer | ArrayBufferView | string;
174
+
175
+ // What a JSON raw route's body is: its input's shape, a string for a document body, the bytes for a `bytes` route,
176
+ // undefined for one that takes none.
57
177
  export type RawJsonInput<Key extends RawJsonRouteKey> = (typeof RAW_JSON_ROUTES)[Key] extends { readonly input: infer Input extends z.ZodType }
58
178
  ? z.input<Input>
59
179
  : (typeof RAW_JSON_ROUTES)[Key] extends { readonly text: true }
60
180
  ? string
61
- : undefined;
181
+ : (typeof RAW_JSON_ROUTES)[Key] extends { readonly bytes: true }
182
+ ? RawBytes
183
+ : undefined;
@@ -21,6 +21,12 @@ export const RAW_ROUTES = {
21
21
  // The live phrase stream (schemas/speech.ts): a WebSocket, so the query ticket, floored at the collaborator in the
22
22
  // handler like the transcribe it streams.
23
23
  "GET /speech/stream": { auth: "door" },
24
+ // Speaking (schemas/voice.ts): the voices to choose from is a read any tier makes; speech costs this box's CPU or
25
+ // the owner's provider credits, so saying anything is the collaborator's, like dictating. The WebSocket floors its
26
+ // query ticket in the handler; the HTTP door is Discord's calls and anything without a socket.
27
+ "GET /speech/voices": { guest: true },
28
+ "POST /speech/say": { floor: "collaborator" },
29
+ "GET /speech/say": { auth: "door" },
24
30
  // The bytes behind the workspace reads; each applies the caller's fence itself.
25
31
  "GET /workspace/raw": { guest: true, lane: "bulk" },
26
32
  // No door, unlike media: the page fetches a thumbnail itself, so the request carries the header.
@@ -49,6 +55,8 @@ export const RAW_ROUTES = {
49
55
  "GET /system/desktop-view": { auth: "door", beforeBoot: true, control: "never" },
50
56
  // Deploy-target enrollment, gated by the connect token.
51
57
  "POST /enroll": { auth: "door", control: "never" },
58
+ // A server-connect code for the owner's command: its single-use enroll token stands in for the connect token.
59
+ "POST /system/enroll/code": { floor: "owner", control: "never" },
52
60
  // An event automation's webhook, gated by the automation's own token.
53
61
  "POST /automations/{id}/fire": { auth: "door" },
54
62
  // The release gate a pipeline runner waits on, gated by the workflow's minted gate token.
@@ -154,6 +162,9 @@ export const RAW_ROUTES = {
154
162
  "POST /ci/webhook/{host}": { auth: "door" },
155
163
  // Below maintainer a pairing is capped to port-mirror, so a collaborator may mint a preview tunnel.
156
164
  "POST /system/sync/pair": { floor: "collaborator", control: "never" },
165
+ // A copy-paste command's short code, redeemed by the install script it was handed to: a device's first contact,
166
+ // gated by the code itself and a limit on failed attempts (peers/redeem-codes.ts).
167
+ "POST /system/redeem/{code}": { auth: "door", control: "never" },
157
168
  // The peer doors: a device, browser or runner redeems its pairing at `enroll` and dials `connect` with the token.
158
169
  "POST /system/hosts/pair": { control: "never" },
159
170
  "POST /system/hosts/enroll": { auth: "door", control: "never" },
@@ -1,4 +1,4 @@
1
- import { sseData, sseFrames } from "./sse.js";
1
+ import { parseSseText, type SseEvent, sseData, sseEvents, sseFrames, sseJson } from "./sse.js";
2
2
 
3
3
  const encoder = new TextEncoder();
4
4
 
@@ -47,3 +47,52 @@ it("reads a frame's data line as JSON, and nothing from a frame without one or w
47
47
  expect(sseData(`: keep-alive`)).toBeUndefined();
48
48
  expect(sseData(`data: {not json`)).toBeUndefined();
49
49
  });
50
+
51
+ const collectEvents = async (events: AsyncGenerator<SseEvent>): Promise<SseEvent[]> => {
52
+ const seen: SseEvent[] = [];
53
+ for await (const event of events) {
54
+ seen.push(event);
55
+ }
56
+ return seen;
57
+ };
58
+
59
+ it("ends events at CRLF, CR and LF blank lines alike, a CRLF split across chunks included", async () => {
60
+ const { stream } = body([`data: 1\r\n\r`, `\ndata: 2\r\rdata: 3\n\n`], true);
61
+ expect((await collectEvents(sseEvents(stream))).map((event) => event.data)).toEqual([`1`, `2`, `3`]);
62
+ });
63
+
64
+ it("joins every data line of an event with a newline", () => {
65
+ expect(parseSseText(`data: {"a":\ndata: 1}\n\n`).map((event) => event.data)).toEqual([`{"a":\n1}`]);
66
+ expect(sseData(`data: {"a":\ndata: 1}`)).toEqual({ a: 1 });
67
+ });
68
+
69
+ it("reads event, id and retry, keeps the last id across events, and skips comments", () => {
70
+ const events = parseSseText(`: hello\nevent: turn\nid: 7\nretry: 3000\ndata: x\n\ndata: y\n\n`);
71
+ expect(events.map(({ event, data, id, retry }) => ({ event, data, id, retry }))).toEqual([
72
+ { event: `turn`, data: `x`, id: `7`, retry: 3000 },
73
+ { event: undefined, data: `y`, id: `7`, retry: undefined },
74
+ ]);
75
+ });
76
+
77
+ it("drops one space after the colon and no more, and a leading byte-order mark", () => {
78
+ expect(parseSseText(`\uFEFFdata: two spaces\n\n`).map((event) => event.data)).toEqual([` two spaces`]);
79
+ });
80
+
81
+ it("keeps each event's raw text, its blank line included, for a reader forwarding it unchanged", () => {
82
+ expect(parseSseText(`event: a\r\ndata: 1\r\n\r\n: ping\n\n`).map((event) => event.raw)).toEqual([`event: a\r\ndata: 1\r\n\r\n`, `: ping\n\n`]);
83
+ });
84
+
85
+ it("dispatches an event the stream ended inside", async () => {
86
+ const { stream } = body([`data: last`], true);
87
+ expect((await collectEvents(sseEvents(stream))).map((event) => event.data)).toEqual([`last`]);
88
+ });
89
+
90
+ it("ends an event stream that goes silent past the idle window", async () => {
91
+ const { stream, cancelled } = body([`data: 1\n\n`], false);
92
+ expect((await collectEvents(sseEvents(stream, 20))).map((event) => event.data)).toEqual([`1`]);
93
+ expect(cancelled()).toBe(true);
94
+ });
95
+
96
+ it("sseJson reads a parsed event's data", () => {
97
+ expect([sseJson(parseSseText(`data: [1]\n\n`)[0]!), sseJson(parseSseText(`: c\n\n`)[0]!)]).toEqual([[1], undefined]);
98
+ });