@intentic/sandbox-contract 1.316.0 → 1.318.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 (362) hide show
  1. package/README.md +12 -4
  2. package/dist/chores/chores.js +1 -1
  3. package/dist/chores/chores.js.map +1 -1
  4. package/dist/chores/probes.js +1 -1
  5. package/dist/chores/probes.js.map +1 -1
  6. package/dist/contracts/accounts.contract.d.ts +75 -0
  7. package/dist/contracts/accounts.contract.d.ts.map +1 -1
  8. package/dist/contracts/accounts.contract.js +10 -1
  9. package/dist/contracts/accounts.contract.js.map +1 -1
  10. package/dist/contracts/agent.contract.d.ts +616 -1
  11. package/dist/contracts/agent.contract.d.ts.map +1 -1
  12. package/dist/contracts/agents.contract.d.ts +1134 -29
  13. package/dist/contracts/agents.contract.d.ts.map +1 -1
  14. package/dist/contracts/agents.contract.js +12 -12
  15. package/dist/contracts/agents.contract.js.map +1 -1
  16. package/dist/contracts/capabilities.contract.d.ts +16 -0
  17. package/dist/contracts/capabilities.contract.d.ts.map +1 -1
  18. package/dist/contracts/capabilities.contract.js +10 -1
  19. package/dist/contracts/capabilities.contract.js.map +1 -1
  20. package/dist/contracts/ci.contract.d.ts +22 -4
  21. package/dist/contracts/ci.contract.d.ts.map +1 -1
  22. package/dist/contracts/device.contract.d.ts +39 -0
  23. package/dist/contracts/device.contract.d.ts.map +1 -1
  24. package/dist/contracts/device.contract.js +2 -0
  25. package/dist/contracts/device.contract.js.map +1 -1
  26. package/dist/contracts/endpoints.contract.d.ts +23 -0
  27. package/dist/contracts/endpoints.contract.d.ts.map +1 -1
  28. package/dist/contracts/endpoints.contract.js +6 -1
  29. package/dist/contracts/endpoints.contract.js.map +1 -1
  30. package/dist/contracts/extensions.contract.d.ts +12 -0
  31. package/dist/contracts/extensions.contract.d.ts.map +1 -1
  32. package/dist/contracts/needs.contract.d.ts +534 -0
  33. package/dist/contracts/needs.contract.d.ts.map +1 -0
  34. package/dist/contracts/needs.contract.js +80 -0
  35. package/dist/contracts/needs.contract.js.map +1 -0
  36. package/dist/contracts/providers.contract.d.ts +4 -0
  37. package/dist/contracts/providers.contract.d.ts.map +1 -1
  38. package/dist/contracts/runner.contract.d.ts +193 -2
  39. package/dist/contracts/runner.contract.d.ts.map +1 -1
  40. package/dist/contracts/secrets.contract.d.ts +53 -0
  41. package/dist/contracts/secrets.contract.d.ts.map +1 -1
  42. package/dist/contracts/secrets.contract.js +31 -2
  43. package/dist/contracts/secrets.contract.js.map +1 -1
  44. package/dist/contracts/sessions.contract.d.ts +83 -0
  45. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  46. package/dist/contracts/settings.contract.d.ts +33 -2
  47. package/dist/contracts/settings.contract.d.ts.map +1 -1
  48. package/dist/contracts/settings.contract.js +20 -1
  49. package/dist/contracts/settings.contract.js.map +1 -1
  50. package/dist/contracts/system.contract.d.ts +177 -356
  51. package/dist/contracts/system.contract.d.ts.map +1 -1
  52. package/dist/contracts/system.contract.js +13 -12
  53. package/dist/contracts/system.contract.js.map +1 -1
  54. package/dist/contracts/translator.contract.d.ts +7 -0
  55. package/dist/contracts/translator.contract.d.ts.map +1 -1
  56. package/dist/contracts/translator.contract.js.map +1 -1
  57. package/dist/contracts/webext.contract.d.ts +38 -0
  58. package/dist/contracts/webext.contract.d.ts.map +1 -1
  59. package/dist/contracts/webext.contract.js +3 -0
  60. package/dist/contracts/webext.contract.js.map +1 -1
  61. package/dist/contracts/workspace.contract.d.ts +1 -80
  62. package/dist/contracts/workspace.contract.d.ts.map +1 -1
  63. package/dist/contracts/workspace.contract.js +0 -27
  64. package/dist/contracts/workspace.contract.js.map +1 -1
  65. package/dist/events/agent-events.d.ts +330 -0
  66. package/dist/events/agent-events.d.ts.map +1 -1
  67. package/dist/events/agent-events.js +13 -0
  68. package/dist/events/agent-events.js.map +1 -1
  69. package/dist/events/agent-words.d.ts +1 -0
  70. package/dist/events/agent-words.d.ts.map +1 -1
  71. package/dist/events/agent-words.js +3 -2
  72. package/dist/events/agent-words.js.map +1 -1
  73. package/dist/events/land-breakage.d.ts.map +1 -1
  74. package/dist/events/land-breakage.js.map +1 -1
  75. package/dist/events/need-wake.d.ts +17 -0
  76. package/dist/events/need-wake.d.ts.map +1 -0
  77. package/dist/events/need-wake.js +34 -0
  78. package/dist/events/need-wake.js.map +1 -0
  79. package/dist/events/resume.d.ts +6 -1
  80. package/dist/events/resume.d.ts.map +1 -1
  81. package/dist/events/resume.js +17 -10
  82. package/dist/events/resume.js.map +1 -1
  83. package/dist/events/sandbox-notice.d.ts +183 -0
  84. package/dist/events/sandbox-notice.d.ts.map +1 -0
  85. package/dist/events/sandbox-notice.js +56 -0
  86. package/dist/events/sandbox-notice.js.map +1 -0
  87. package/dist/events/system-events.d.ts +84 -0
  88. package/dist/events/system-events.d.ts.map +1 -1
  89. package/dist/events/system-events.js +2 -0
  90. package/dist/events/system-events.js.map +1 -1
  91. package/dist/events/transcript.d.ts +508 -0
  92. package/dist/events/transcript.d.ts.map +1 -1
  93. package/dist/events/transcript.js +12 -1
  94. package/dist/events/transcript.js.map +1 -1
  95. package/dist/ids/conversation-ids.d.ts +1 -3
  96. package/dist/ids/conversation-ids.d.ts.map +1 -1
  97. package/dist/ids/conversation-ids.js +1 -3
  98. package/dist/ids/conversation-ids.js.map +1 -1
  99. package/dist/ids/project-dir.d.ts +6 -0
  100. package/dist/ids/project-dir.d.ts.map +1 -0
  101. package/dist/ids/project-dir.js +43 -0
  102. package/dist/ids/project-dir.js.map +1 -0
  103. package/dist/index.d.ts +3474 -1163
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +13 -4
  106. package/dist/index.js.map +1 -1
  107. package/dist/models/agent-runtimes.d.ts.map +1 -1
  108. package/dist/models/agent-runtimes.js +2 -2
  109. package/dist/models/agent-runtimes.js.map +1 -1
  110. package/dist/models/model-roles.d.ts +1 -9
  111. package/dist/models/model-roles.d.ts.map +1 -1
  112. package/dist/models/model-roles.js +1 -9
  113. package/dist/models/model-roles.js.map +1 -1
  114. package/dist/policy/command-classes.d.ts.map +1 -1
  115. package/dist/policy/command-classes.js +52 -3
  116. package/dist/policy/command-classes.js.map +1 -1
  117. package/dist/policy/safety-policy.d.ts +7 -1
  118. package/dist/policy/safety-policy.d.ts.map +1 -1
  119. package/dist/policy/safety-policy.js +2 -0
  120. package/dist/policy/safety-policy.js.map +1 -1
  121. package/dist/policy/secret-hosts.d.ts +13 -0
  122. package/dist/policy/secret-hosts.d.ts.map +1 -0
  123. package/dist/policy/secret-hosts.js +41 -0
  124. package/dist/policy/secret-hosts.js.map +1 -0
  125. package/dist/policy/turned-away.d.ts.map +1 -1
  126. package/dist/policy/turned-away.js +1 -0
  127. package/dist/policy/turned-away.js.map +1 -1
  128. package/dist/protocol/raw-routes.d.ts +7 -5
  129. package/dist/protocol/raw-routes.d.ts.map +1 -1
  130. package/dist/protocol/raw-routes.js +4 -1
  131. package/dist/protocol/raw-routes.js.map +1 -1
  132. package/dist/protocol/routes.d.ts +2 -6
  133. package/dist/protocol/routes.d.ts.map +1 -1
  134. package/dist/protocol/routes.js +2 -3
  135. package/dist/protocol/routes.js.map +1 -1
  136. package/dist/protocol/stream-of.d.ts +6 -0
  137. package/dist/protocol/stream-of.d.ts.map +1 -0
  138. package/dist/protocol/stream-of.js +4 -0
  139. package/dist/protocol/stream-of.js.map +1 -0
  140. package/dist/schemas/activity.js +1 -1
  141. package/dist/schemas/activity.js.map +1 -1
  142. package/dist/schemas/agent.d.ts +19 -2
  143. package/dist/schemas/agent.d.ts.map +1 -1
  144. package/dist/schemas/agent.js +2 -1
  145. package/dist/schemas/agent.js.map +1 -1
  146. package/dist/schemas/agents.d.ts +119 -0
  147. package/dist/schemas/agents.d.ts.map +1 -1
  148. package/dist/schemas/agents.js +45 -0
  149. package/dist/schemas/agents.js.map +1 -1
  150. package/dist/schemas/automations.d.ts +37 -0
  151. package/dist/schemas/automations.d.ts.map +1 -1
  152. package/dist/schemas/capabilities.d.ts +21 -0
  153. package/dist/schemas/capabilities.d.ts.map +1 -1
  154. package/dist/schemas/capabilities.js +50 -4
  155. package/dist/schemas/capabilities.js.map +1 -1
  156. package/dist/schemas/ci.d.ts +69 -9
  157. package/dist/schemas/ci.d.ts.map +1 -1
  158. package/dist/schemas/ci.js +29 -8
  159. package/dist/schemas/ci.js.map +1 -1
  160. package/dist/schemas/context-trim.d.ts +5 -1
  161. package/dist/schemas/context-trim.d.ts.map +1 -1
  162. package/dist/schemas/context-trim.js +8 -4
  163. package/dist/schemas/context-trim.js.map +1 -1
  164. package/dist/schemas/devices.d.ts +91 -0
  165. package/dist/schemas/devices.d.ts.map +1 -1
  166. package/dist/schemas/devices.js +23 -1
  167. package/dist/schemas/devices.js.map +1 -1
  168. package/dist/schemas/environment.d.ts +48 -0
  169. package/dist/schemas/environment.d.ts.map +1 -1
  170. package/dist/schemas/environment.js +15 -0
  171. package/dist/schemas/environment.js.map +1 -1
  172. package/dist/schemas/extension-updates.d.ts +24 -0
  173. package/dist/schemas/extension-updates.d.ts.map +1 -1
  174. package/dist/schemas/git/git.d.ts +13 -0
  175. package/dist/schemas/git/git.d.ts.map +1 -1
  176. package/dist/schemas/git/git.js +11 -1
  177. package/dist/schemas/git/git.js.map +1 -1
  178. package/dist/schemas/hosts.d.ts +4 -0
  179. package/dist/schemas/hosts.d.ts.map +1 -1
  180. package/dist/schemas/hosts.js +3 -1
  181. package/dist/schemas/hosts.js.map +1 -1
  182. package/dist/schemas/keep-warm.d.ts +4 -0
  183. package/dist/schemas/keep-warm.d.ts.map +1 -1
  184. package/dist/schemas/keep-warm.js +15 -5
  185. package/dist/schemas/keep-warm.js.map +1 -1
  186. package/dist/schemas/loopback-catch.d.ts +33 -0
  187. package/dist/schemas/loopback-catch.d.ts.map +1 -0
  188. package/dist/schemas/loopback-catch.js +20 -0
  189. package/dist/schemas/loopback-catch.js.map +1 -0
  190. package/dist/schemas/maintenance.js +1 -1
  191. package/dist/schemas/maintenance.js.map +1 -1
  192. package/dist/schemas/needs.d.ts +664 -0
  193. package/dist/schemas/needs.d.ts.map +1 -0
  194. package/dist/schemas/needs.js +203 -0
  195. package/dist/schemas/needs.js.map +1 -0
  196. package/dist/schemas/personas.d.ts.map +1 -1
  197. package/dist/schemas/personas.js +6 -3
  198. package/dist/schemas/personas.js.map +1 -1
  199. package/dist/schemas/providers/provider-oauth.d.ts +87 -0
  200. package/dist/schemas/providers/provider-oauth.d.ts.map +1 -1
  201. package/dist/schemas/providers/provider-oauth.js +20 -0
  202. package/dist/schemas/providers/provider-oauth.js.map +1 -1
  203. package/dist/schemas/secrets.d.ts +84 -0
  204. package/dist/schemas/secrets.d.ts.map +1 -1
  205. package/dist/schemas/secrets.js +70 -0
  206. package/dist/schemas/secrets.js.map +1 -1
  207. package/dist/schemas/settings-history.d.ts +1 -1
  208. package/dist/schemas/settings-history.d.ts.map +1 -1
  209. package/dist/schemas/settings-history.js +1 -0
  210. package/dist/schemas/settings-history.js.map +1 -1
  211. package/dist/schemas/settings.d.ts +44 -2
  212. package/dist/schemas/settings.d.ts.map +1 -1
  213. package/dist/schemas/settings.js +30 -4
  214. package/dist/schemas/settings.js.map +1 -1
  215. package/dist/schemas/state-plan.d.ts +1 -0
  216. package/dist/schemas/state-plan.d.ts.map +1 -1
  217. package/dist/schemas/state-plan.js +3 -1
  218. package/dist/schemas/state-plan.js.map +1 -1
  219. package/dist/schemas/system.d.ts +20 -0
  220. package/dist/schemas/system.d.ts.map +1 -1
  221. package/dist/schemas/system.js +7 -0
  222. package/dist/schemas/system.js.map +1 -1
  223. package/dist/schemas/terminal.d.ts +2 -3
  224. package/dist/schemas/terminal.d.ts.map +1 -1
  225. package/dist/schemas/terminal.js +12 -4
  226. package/dist/schemas/terminal.js.map +1 -1
  227. package/dist/schemas/updates.d.ts +39 -0
  228. package/dist/schemas/updates.d.ts.map +1 -0
  229. package/dist/schemas/updates.js +31 -0
  230. package/dist/schemas/updates.js.map +1 -0
  231. package/dist/schemas/webext.d.ts +3 -0
  232. package/dist/schemas/webext.d.ts.map +1 -1
  233. package/dist/schemas/webext.js +1 -0
  234. package/dist/schemas/webext.js.map +1 -1
  235. package/dist/schemas/workflows.js +3 -3
  236. package/dist/schemas/workflows.js.map +1 -1
  237. package/dist/schemas/workspace/workspace-tree.d.ts +2 -0
  238. package/dist/schemas/workspace/workspace-tree.d.ts.map +1 -1
  239. package/dist/schemas/workspace/workspace-tree.js +4 -0
  240. package/dist/schemas/workspace/workspace-tree.js.map +1 -1
  241. package/dist/state/definition.d.ts +28 -4
  242. package/dist/state/definition.d.ts.map +1 -1
  243. package/dist/state/history-state.d.ts.map +1 -1
  244. package/dist/state/history-state.js +10 -0
  245. package/dist/state/history-state.js.map +1 -1
  246. package/dist/state/runtime-state.d.ts +0 -3
  247. package/dist/state/runtime-state.d.ts.map +1 -1
  248. package/dist/state/runtime-state.js +0 -1
  249. package/dist/state/runtime-state.js.map +1 -1
  250. package/dist/state/workspace-state.d.ts +4 -5
  251. package/dist/state/workspace-state.d.ts.map +1 -1
  252. package/dist/state/workspace-state.js +1 -6
  253. package/dist/state/workspace-state.js.map +1 -1
  254. package/dist/text/documents.d.ts +3 -0
  255. package/dist/text/documents.d.ts.map +1 -1
  256. package/dist/text/documents.js +14 -0
  257. package/dist/text/documents.js.map +1 -1
  258. package/dist/text/transcript-fold.d.ts +4 -3
  259. package/dist/text/transcript-fold.d.ts.map +1 -1
  260. package/dist/text/transcript-fold.js +140 -31
  261. package/dist/text/transcript-fold.js.map +1 -1
  262. package/dist/webext/index.d.ts +1 -0
  263. package/dist/webext/index.d.ts.map +1 -1
  264. package/dist/webext/index.js +1 -0
  265. package/dist/webext/index.js.map +1 -1
  266. package/package.json +5 -5
  267. package/src/chores/chores.ts +2 -2
  268. package/src/chores/probes.ts +1 -1
  269. package/src/contracts/accounts.contract.ts +12 -0
  270. package/src/contracts/agents.contract.ts +13 -13
  271. package/src/contracts/capabilities.contract.ts +14 -1
  272. package/src/contracts/device.contract.ts +5 -0
  273. package/src/contracts/endpoints.contract.ts +20 -4
  274. package/src/contracts/needs.contract.ts +103 -0
  275. package/src/contracts/secrets.contract.ts +45 -3
  276. package/src/contracts/settings.contract.ts +29 -0
  277. package/src/contracts/system.contract.ts +19 -14
  278. package/src/contracts/translator.contract.ts +2 -1
  279. package/src/contracts/webext.contract.ts +7 -1
  280. package/src/contracts/workspace.contract.ts +0 -37
  281. package/src/events/agent-events.ts +22 -1
  282. package/src/events/agent-words.ts +6 -2
  283. package/src/events/land-breakage.ts +3 -4
  284. package/src/events/need-wake.test.ts +55 -0
  285. package/src/events/need-wake.ts +71 -0
  286. package/src/events/resume.test.ts +13 -2
  287. package/src/events/resume.ts +25 -12
  288. package/src/events/sandbox-notice.ts +92 -0
  289. package/src/events/system-events.ts +7 -0
  290. package/src/events/transcript.ts +23 -1
  291. package/src/events/verify-nudge.ts +3 -3
  292. package/src/ids/conversation-ids.test.ts +2 -26
  293. package/src/ids/conversation-ids.ts +6 -12
  294. package/src/ids/project-dir.fixture.json +65 -0
  295. package/src/ids/project-dir.test.ts +60 -0
  296. package/src/ids/project-dir.ts +70 -0
  297. package/src/index.ts +18 -4
  298. package/src/models/agent-catalog.test.ts +7 -1
  299. package/src/models/agent-runtimes.ts +7 -5
  300. package/src/models/model-roles.ts +1 -9
  301. package/src/policy/command-classes.test.ts +39 -0
  302. package/src/policy/command-classes.ts +70 -3
  303. package/src/policy/safety-policy.ts +10 -0
  304. package/src/policy/secret-hosts.test.ts +89 -0
  305. package/src/policy/secret-hosts.ts +73 -0
  306. package/src/policy/turned-away.ts +1 -0
  307. package/src/protocol/raw-routes.ts +6 -3
  308. package/src/protocol/routes.test.ts +6 -6
  309. package/src/protocol/routes.ts +2 -9
  310. package/src/protocol/stream-of.ts +13 -0
  311. package/src/schemas/activity.ts +1 -1
  312. package/src/schemas/agent.ts +4 -1
  313. package/src/schemas/agents.ts +72 -5
  314. package/src/schemas/automations.ts +2 -2
  315. package/src/schemas/capabilities.ts +72 -7
  316. package/src/schemas/ci.ts +56 -11
  317. package/src/schemas/context-trim.ts +11 -6
  318. package/src/schemas/devices.ts +55 -2
  319. package/src/schemas/environment.ts +39 -0
  320. package/src/schemas/git/git.ts +20 -2
  321. package/src/schemas/hosts.ts +7 -1
  322. package/src/schemas/keep-warm.ts +19 -6
  323. package/src/schemas/loopback-catch.ts +45 -0
  324. package/src/schemas/maintenance.ts +1 -1
  325. package/src/schemas/needs.ts +301 -0
  326. package/src/schemas/personas.ts +8 -3
  327. package/src/schemas/providers/provider-oauth.ts +35 -0
  328. package/src/schemas/secrets.ts +93 -0
  329. package/src/schemas/settings-history.ts +2 -0
  330. package/src/schemas/settings.test.ts +19 -1
  331. package/src/schemas/settings.ts +66 -9
  332. package/src/schemas/speaker.ts +5 -5
  333. package/src/schemas/state-plan.ts +8 -1
  334. package/src/schemas/system.ts +15 -0
  335. package/src/schemas/terminal.ts +19 -6
  336. package/src/schemas/updates.ts +56 -0
  337. package/src/schemas/webext.ts +3 -0
  338. package/src/schemas/workflows.ts +5 -5
  339. package/src/schemas/workspace/workspace-tree.ts +7 -0
  340. package/src/state/fix-attempt-plan.test.ts +1 -1
  341. package/src/state/fix-stance.ts +1 -1
  342. package/src/state/history-state.ts +19 -0
  343. package/src/state/runtime-state.ts +0 -3
  344. package/src/state/workspace-state.ts +4 -6
  345. package/src/text/documents.test.ts +25 -1
  346. package/src/text/documents.ts +24 -0
  347. package/src/text/title.ts +1 -1
  348. package/src/text/transcript-fold.test.ts +218 -25
  349. package/src/text/transcript-fold.ts +170 -45
  350. package/src/webext/index.ts +1 -0
  351. package/dist/schemas/workspace/push-checks.d.ts +0 -222
  352. package/dist/schemas/workspace/push-checks.d.ts.map +0 -1
  353. package/dist/schemas/workspace/push-checks.js +0 -97
  354. package/dist/schemas/workspace/push-checks.js.map +0 -1
  355. package/dist/state/red-streak.d.ts +0 -7
  356. package/dist/state/red-streak.d.ts.map +0 -1
  357. package/dist/state/red-streak.js +0 -11
  358. package/dist/state/red-streak.js.map +0 -1
  359. package/src/schemas/workspace/push-checks.test.ts +0 -28
  360. package/src/schemas/workspace/push-checks.ts +0 -155
  361. package/src/state/red-streak.test.ts +0 -28
  362. package/src/state/red-streak.ts +0 -28
@@ -0,0 +1,73 @@
1
+ // Which hosts a stored secret may be sent to: an owner's list per secret, matched against the hosts one use would reach.
2
+ // Shared by the daemon's check, its routes and the Secrets view, so the three cannot disagree on what a pattern means.
3
+ // A list names hosts, never ports or paths: the question it answers is who receives the value, not which door.
4
+
5
+ // One DNS label: letters, digits and inner hyphens, at most 63 characters.
6
+ const LABEL = String.raw`[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?`;
7
+
8
+ // An exact host (`api.github.com`, `localhost`, `10.0.0.2`) or every host under a domain (`*.github.com`). A wildcard
9
+ // names at least two labels under it, so `*.com` cannot stand in for most of the internet.
10
+ export const SECRET_HOST_PATTERN_RE = new RegExp(String.raw`^(?:\*\.${LABEL}(?:\.${LABEL})+|${LABEL}(?:\.${LABEL})*)$`);
11
+ export const SECRET_HOST_MAX = 253;
12
+ // Enough for a service's API, uploads and content hosts several times over; a list longer than this is no limit.
13
+ export const SECRET_HOSTS_MAX = 64;
14
+
15
+ const WILDCARD = "*.";
16
+
17
+ // What a person types or pastes, as the pattern it means: lowercased, trimmed, a trailing dot and a port dropped, and a
18
+ // whole URL reduced to its host. Undefined for anything that is not a host pattern, so a caller refuses it by name.
19
+ export const normalizeHostPattern = (raw: string): string | undefined => {
20
+ let text = raw.trim().toLowerCase();
21
+ if (/^[a-z][a-z0-9+.-]*:\/\//.test(text)) {
22
+ try {
23
+ text = new URL(text).hostname;
24
+ } catch {
25
+ return undefined;
26
+ }
27
+ }
28
+ text = text.replace(/:\d+$/, "").replace(/\.$/, "");
29
+ return text.length <= SECRET_HOST_MAX && SECRET_HOST_PATTERN_RE.test(text) ? text : undefined;
30
+ };
31
+
32
+ // A host a use would reach, in the form patterns are written in: lowercased, one trailing dot dropped. Undefined when it
33
+ // is not a plain host name or IPv4 address, which callers read as "cannot tell where this goes".
34
+ export const normalizeHost = (raw: string): string | undefined => {
35
+ const host = raw.toLowerCase().replace(/\.$/, "");
36
+ return host.length <= SECRET_HOST_MAX && !host.startsWith(WILDCARD) && SECRET_HOST_PATTERN_RE.test(host) ? host : undefined;
37
+ };
38
+
39
+ // `*.github.com` matches every host under github.com at any depth, and not github.com itself; anything else matches only
40
+ // itself. Both sides are expected normalized.
41
+ export const hostMatches = (pattern: string, host: string): boolean =>
42
+ pattern.startsWith(WILDCARD) ? host.endsWith(pattern.slice(1)) : host === pattern;
43
+
44
+ export const hostAllowed = (patterns: readonly string[], host: string): boolean => patterns.some((pattern) => hostMatches(pattern, host));
45
+
46
+ // Whether every host `pattern` matches is already matched by `patterns`: an exact host by any pattern that matches it, a
47
+ // wildcard only by itself or a wildcard over a domain above it.
48
+ const covered = (patterns: readonly string[], pattern: string): boolean => {
49
+ if (!pattern.startsWith(WILDCARD)) {
50
+ return hostAllowed(patterns, pattern);
51
+ }
52
+ const suffix = pattern.slice(1);
53
+ return patterns.some((held) => held.startsWith(WILDCARD) && suffix.endsWith(held.slice(1)));
54
+ };
55
+
56
+ // One secret's host guard as a change reads it: whether it asks off the list, and the list. Absent reads as off.
57
+ export interface HostGuardSetting {
58
+ readonly guard: boolean;
59
+ readonly hosts: readonly string[];
60
+ }
61
+
62
+ // Whether going from `current` to `next` only ever makes the agent ask more: turning the guard on, or taking hosts off a
63
+ // guarded list (down to none, where every use asks). The one test for who may make a change: anybody who may use
64
+ // secrets may tighten a guard, only the owner may loosen one, by adding a host or turning it off.
65
+ export const tightensHostGuard = (current: HostGuardSetting | undefined, next: HostGuardSetting): boolean => {
66
+ if (!next.guard) {
67
+ return current === undefined || !current.guard;
68
+ }
69
+ if (current === undefined || !current.guard) {
70
+ return true;
71
+ }
72
+ return next.hosts.every((pattern) => covered(current.hosts, pattern));
73
+ };
@@ -10,6 +10,7 @@ const TURNED_AWAY: ReadonlySet<string> = new Set([
10
10
  "claude-not-entitled",
11
11
  "unknown-command",
12
12
  "context-window-too-small",
13
+ "model-helper-only",
13
14
  "sandbox-memory-low",
14
15
  "trial-unavailable",
15
16
  "trial-model-unavailable",
@@ -61,6 +61,9 @@ export const RAW_ROUTES = {
61
61
  "POST /environment/approve": {},
62
62
  "POST /environment/reject": {},
63
63
  "POST /environment/runtime-install": {},
64
+ "POST /environment/remove": {},
65
+ "POST /environment/rebuild-when-idle": {},
66
+ "DELETE /environment/rebuild-when-idle": {},
64
67
  "GET /engines": {},
65
68
  "POST /engines/channel": {},
66
69
  "POST /engines/update": {},
@@ -83,9 +86,8 @@ export const RAW_ROUTES = {
83
86
  "GET /extensions/{id}/bundle": { lane: "bulk" },
84
87
  // An extension backend's own namespace, proxied verbatim.
85
88
  "ALL /x/*": {},
86
- // The `capabilities` CLI: discovery by name, and the ask that parks on an owner-decided card.
89
+ // The `capabilities` CLI's discovery by name; its ask is the needs door (contracts/needs.contract.ts).
87
90
  "GET /capabilities/connectable": { floor: "maintainer", agent: true, control: "never" },
88
- "POST /capabilities/ask": { floor: "maintainer", agent: true, control: "never" },
89
91
  // The `sandboxes` CLI; every create parks on a card in the owner's chat first.
90
92
  "GET /sandboxes": { agent: true },
91
93
  "POST /sandboxes": { agent: true },
@@ -93,13 +95,14 @@ export const RAW_ROUTES = {
93
95
  "GET /wallet/status": { agent: true, control: "never" },
94
96
  "POST /wallet/fetch": { agent: true, control: "never" },
95
97
  "GET /wallet/history": { agent: true, control: "never" },
96
- // The `agents` CLI's children: start, follow up, answer (never a consent card), cancel, list.
98
+ // The `agents` CLI's children: start, follow up, answer (never a consent card), cancel, merge held work, list.
97
99
  "POST /children/spawn": { agent: true, control: "never" },
98
100
  "GET /children/providers": { agent: true, control: "never" },
99
101
  "POST /children/wait": { agent: true, control: "never" },
100
102
  "POST /children/send": { agent: true, control: "never" },
101
103
  "POST /children/answer": { agent: true, control: "never" },
102
104
  "POST /children/cancel": { agent: true, control: "never" },
105
+ "POST /children/merge": { agent: true, control: "never" },
103
106
  "GET /children": { agent: true, control: "never" },
104
107
  // The fleet reads, and the one write: words in front of another conversation, which a person can do by typing.
105
108
  "GET /fleet": { agent: true },
@@ -1,6 +1,6 @@
1
1
  import { eventIterator, oc } from "@orpc/contract";
2
2
  import { z } from "zod";
3
- import { SANDBOX_ROUTE_NAMES, SANDBOX_ROUTE_SHAPES, SANDBOX_ROUTES, sandboxRouteName } from "../index.js";
3
+ import { SANDBOX_ROUTE_NAMES, SANDBOX_ROUTES, sandboxRouteName, sandboxRouteFingerprints } from "../index.js";
4
4
  import { procedure } from "./route-meta.js";
5
5
  import { contractRoutes, routeNameForRequest, routeShapes, servedRoute, streamOf } from "./routes.js";
6
6
 
@@ -231,7 +231,7 @@ describe(`the real sandbox contract`, () => {
231
231
  it(`fingerprints every route, streams included`, () => {
232
232
  // Nothing may opt out: a route with no fingerprint is a route the browser can never tell has drifted, and the
233
233
  // streaming ones (system.events, agent.attach, the exit dials) carry the frames a stale daemon breaks first.
234
- expect(SANDBOX_ROUTE_NAMES.filter((name) => !(name in SANDBOX_ROUTE_SHAPES))).toEqual([]);
234
+ expect(SANDBOX_ROUTE_NAMES.filter((name) => !(name in sandboxRouteFingerprints()))).toEqual([]);
235
235
  });
236
236
 
237
237
  it(`fingerprints the streaming routes eventIterator used to hide`, () => {
@@ -250,7 +250,7 @@ describe(`the real sandbox contract`, () => {
250
250
  `system.runDeviceAgentFlow`,
251
251
  `vpn.connect`,
252
252
  ]
253
- .filter((name) => name in SANDBOX_ROUTE_SHAPES)
253
+ .filter((name) => name in sandboxRouteFingerprints())
254
254
  .toSorted(),
255
255
  ).toEqual([
256
256
  `agent.attach`,
@@ -269,8 +269,8 @@ describe(`the real sandbox contract`, () => {
269
269
  });
270
270
 
271
271
  it(`fingerprints every route exactly once`, () => {
272
- expect(Object.keys(SANDBOX_ROUTE_SHAPES).every((name) => SANDBOX_ROUTE_NAMES.includes(name))).toBe(true);
273
- expect(Object.keys(SANDBOX_ROUTE_SHAPES).length).toBe(SANDBOX_ROUTE_NAMES.length);
272
+ expect(Object.keys(sandboxRouteFingerprints()).every((name) => SANDBOX_ROUTE_NAMES.includes(name))).toBe(true);
273
+ expect(Object.keys(sandboxRouteFingerprints()).length).toBe(SANDBOX_ROUTE_NAMES.length);
274
274
  });
275
275
 
276
276
  it(`derives a route table with no duplicate names`, () => {
@@ -287,7 +287,7 @@ describe(`the real sandbox contract`, () => {
287
287
  // differently has to read as drift rather than as a machine that runs nothing.
288
288
  it(`fingerprints the fleet view the device buttons are gated on`, () => {
289
289
  expect(sandboxRouteName(`GET`, `/system/devices`)).toBe(`system.devices`);
290
- expect(SANDBOX_ROUTE_SHAPES[`system.devices`]).toEqual(expect.any(String));
290
+ expect(sandboxRouteFingerprints()[`system.devices`]).toEqual(expect.any(String));
291
291
  });
292
292
 
293
293
  it(`resolves a known concrete request back to its contract name`, () => {
@@ -1,19 +1,12 @@
1
- import { eventIterator } from "@orpc/contract";
2
1
  import { z } from "zod";
3
2
  import type { RouteMeta } from "./route-meta.js";
3
+ import { FRAME, streamOf } from "./stream-of.js";
4
4
 
5
5
  // Named route surface of the daemon's contract (`<group>.<route>`), derived automatically so nothing here is
6
6
  // hand-maintained. The daemon advertises which routes it implements (the /events hello frame); the browser diffs that
7
7
  // against its own contract so an old daemon's gap is a named feature check, not a silent 404.
8
8
 
9
- // Every streamed route declares its frames through this, never oRPC's `eventIterator` directly: that returns an opaque
10
- // standard-schema validator holding no reachable inner schema, so a stream's payload would be unfingerprintable and its
11
- // drift invisible. The frame schema rides along under a symbol, which oRPC never reads and JSON never serializes.
12
- // Typed as a plain `symbol`, not the inferred unique one: a unique symbol would ride into every contract's inferred
13
- // type and break declaration emit on a local name nothing outside can refer to.
14
- const FRAME: symbol = Symbol.for("intentic.contract.frame");
15
-
16
- export const streamOf = <T extends z.ZodType>(frame: T) => Object.assign(eventIterator(frame), { [FRAME]: frame });
9
+ export { streamOf };
17
10
 
18
11
  // The frame schema behind a streamed route's validator, or undefined for an ordinary request/response schema.
19
12
  const frameOf = (schema: unknown): z.ZodType | undefined =>
@@ -0,0 +1,13 @@
1
+ import { eventIterator } from "@orpc/contract";
2
+ import type * as z from "zod";
3
+
4
+ // Every streamed route declares its frames through this, never oRPC's `eventIterator` directly: that returns an opaque
5
+ // standard-schema validator holding no reachable inner schema, so a stream's payload would be unfingerprintable and its
6
+ // drift invisible. The frame schema rides along under a symbol, which oRPC never reads and JSON never serializes.
7
+ // Typed as a plain `symbol`, not the inferred unique one: a unique symbol would ride into every contract's inferred
8
+ // type and break declaration emit on a local name nothing outside can refer to.
9
+ // Its own module, apart from routes.ts, because the browser extension's contract streams too and routes.ts reaches
10
+ // zod's namespace at runtime, which the extension's bundle budget cannot carry.
11
+ export const FRAME: symbol = Symbol.for("intentic.contract.frame");
12
+
13
+ export const streamOf = <T extends z.ZodType>(frame: T) => Object.assign(eventIterator(frame), { [FRAME]: frame });
@@ -26,7 +26,7 @@ export const ActivityEventSchema = z.object({
26
26
  type: z
27
27
  .string()
28
28
  .describe(
29
- "Exactly what happened: a message received or sent, a reaction, a turn starting or ending, a rule doing something. A rule that ran and passed says nothing here, because a feed of green ticks is one the eye learns to skip.",
29
+ "Exactly what happened: a message received or sent, a reaction, a turn starting or ending, a rule doing something. A rule that ran and passed says nothing here, because a feed of passes is one the eye learns to skip.",
30
30
  ),
31
31
  channelId: z.string().optional().describe("Which channel or thread it happened in."),
32
32
  // Inbound author display name.
@@ -131,7 +131,7 @@ export type ForkedFrom = z.infer<typeof ForkedFromSchema>;
131
131
  const AgentTurnFieldsSchema = z.object({
132
132
  prompt: z.string().describe("What to say to the agent. May be empty if you are only attaching files."),
133
133
  errand: TurnErrandSchema.optional().describe(
134
- "What the words are for, when the app or the sandbox composed them rather than a person typing them: a land conflict to resolve, a red CI run to fix. Shown as the sandbox's words, not yours. Leave it out for your own words.",
134
+ "What the words are for, when the app or the sandbox composed them rather than a person typing them: a land conflict to resolve, a failed CI run to fix. Shown as the sandbox's words, not yours. Leave it out for your own words.",
135
135
  ),
136
136
  // Minted by the sender so a send whose answer was lost can be sent again without being delivered twice.
137
137
  messageId: z
@@ -488,6 +488,9 @@ export type ConversationQueue = z.infer<typeof ConversationQueueSchema>;
488
488
  // What resuming a queue did: started a turn with what waited, when nothing else ran.
489
489
  export const QueueResumedSchema = z.object({
490
490
  run: z.string().optional().describe("The turn the waiting messages started. Absent when a turn was already running, and they go after it."),
491
+ queue: ConversationQueueSchema.optional().describe(
492
+ "The queue the release left: what still waits once the turn took what it could. Absent from sandboxes older than the field.",
493
+ ),
491
494
  });
492
495
  export type QueueResumed = z.infer<typeof QueueResumedSchema>;
493
496
  // Attaches to a conversation's newest run (live, or finished within retention); no cursor to resume from, the head
@@ -4,6 +4,7 @@ import { AgentHarnessSchema, AgentOriginSchema, AgentProviderSchema, Conversatio
4
4
  import { LoopStateSchema } from "./loops.js";
5
5
  import { LimitPolicySchema, RetryPolicySchema, TurnBreakPolicySchema, TurnBreakSchema } from "./turn-break.js";
6
6
  import { KeepWarmSchema } from "./keep-warm.js";
7
+ import { AgentNeedSchema } from "./needs.js";
7
8
  import { EMOJI_MAX_LENGTH, isSingleEmoji } from "../text/emoji.js";
8
9
  // A fleet agent is any conversation with a registry entry, keyed by conversationId. Isolated ones own a git worktree
9
10
  // (branch agent/<id>); workspace conversations have none, but both share one status/activity/cost lifecycle.
@@ -56,7 +57,7 @@ export const AgentAttentionSchema = z.object({
56
57
  plan: z.boolean().describe("It has proposed a plan and is waiting for a yes."),
57
58
  question: z.boolean().describe("It has asked you something."),
58
59
  permission: z.boolean().describe("It wants to use a tool it needs permission for."),
59
- // A missing capability (capabilities/offers/capability-offer.ts); lets the lane say "setup needed" rather than a generic
60
+ // A missing capability on a card from before needs (docs/architecture/needs.md); lets the lane say "setup needed" rather than a generic
60
61
  // pause.
61
62
  capability: z.boolean().describe("It needs something connected that is not connected yet."),
62
63
  // A gated credential parked on a named person's click; the one pause the board's reader may not be able to clear
@@ -65,6 +66,11 @@ export const AgentAttentionSchema = z.object({
65
66
  .boolean()
66
67
  .describe("It is waiting for a named person to release a credential. The one pause that may not be yours to clear, whatever your role."),
67
68
  conflict: z.boolean().describe("Its work cannot be merged without somebody resolving a clash."),
69
+ // A need outlives its turn (docs/architecture/needs.md), so this can stand on a card that is not running at all.
70
+ need: z
71
+ .boolean()
72
+ .optional()
73
+ .describe("It asked a person for something it still needs: a connection, a secret, wider reach, a tool. Absent from a daemon older than needs."),
68
74
  });
69
75
  export type AgentAttention = z.infer<typeof AgentAttentionSchema>;
70
76
  // What the last turn left open, measured at the moment it ended, unlike AgentAttentionSchema's live parked waits.
@@ -83,7 +89,7 @@ export const UnfinishedWorkSchema = z.object({
83
89
  })
84
90
  .optional()
85
91
  .describe("The agent's own checklist where that turn left it. Absent for a conversation that kept no list."),
86
- // Name of the `turn.ending` check still red when the turn ended. Retired with that check: nothing writes it now, and
92
+ // Name of the `turn.ending` check still failing when the turn ended. Retired with that check: nothing writes it now, and
87
93
  // it stays so entries written before still read.
88
94
  check: z
89
95
  .string()
@@ -92,7 +98,7 @@ export const UnfinishedWorkSchema = z.object({
92
98
  });
93
99
  export type UnfinishedWork = z.infer<typeof UnfinishedWorkSchema>;
94
100
  // What the last turn showed of its own work, read off its tool calls and never asked of the model: the record a card
95
- // badges instead of sending the turn back to prove anything. CI checks what the owner pushes (push-checks.ts).
101
+ // badges instead of sending the turn back to prove anything. CI checks what the owner pushes (schemas/ci.ts).
96
102
  export const TurnProofSchema = z.object({
97
103
  at: z.number().describe("When the turn that left this ended, in milliseconds."),
98
104
  verification: z
@@ -426,6 +432,32 @@ export const AgentSummarySchema = z.object({
426
432
  "Since when somebody's composer has held a message for it that they have not sent yet, in milliseconds. While set, the sandbox never archives it on its own for being idle.",
427
433
  ),
428
434
  attention: AgentAttentionSchema.describe("Which kinds of waiting-for-you it is doing."),
435
+ // Beside `attention.permission`, which says only that one waits: the id an answer names and what it asks, so a card
436
+ // can offer the answer itself instead of a trip to the chat. The oldest one, while its turn runs.
437
+ permissionAsk: z
438
+ .object({
439
+ requestId: z.string().describe("Which request this is: the id an answer to it names."),
440
+ ask: z.string().describe("What it asks to do, on one line: the runtime's own sentence, else the tool's short name."),
441
+ })
442
+ .optional()
443
+ .describe(
444
+ "The oldest permission its running turn is waiting on, so it can be answered where the conversation is listed. Absent when none waits, and from a sandbox older than it.",
445
+ ),
446
+ // Kept on the record until a land goes through, unlike `failure`, which the next turn clears: a land that broke
447
+ // leaves the work stuck on its branch however many turns run after it.
448
+ landFailure: z
449
+ .object({
450
+ reason: z.string().describe("Why it failed, in the words it failed with."),
451
+ code: z
452
+ .string()
453
+ .optional()
454
+ .describe("Which kind of failure it was, when the sandbox could tell: unlinked when its copy lost its link to the workspace. Absent otherwise."),
455
+ at: z.number().describe("When it failed, in milliseconds."),
456
+ })
457
+ .optional()
458
+ .describe(
459
+ "The last attempt to bring its work into the workspace failed, and why. Cleared by the next land that goes through. Absent when nothing failed, and from a sandbox older than it.",
460
+ ),
429
461
  // Only the causes that still hold, re-read live (conversations/land/standing.ts), never the stored report's own list: a
430
462
  // blocker the user has since cleared is not something to offer them an action about.
431
463
  conflictCauses: z
@@ -446,8 +478,8 @@ export const AgentSummarySchema = z.object({
446
478
  // Completed turns and lifetime tool calls, the card's msgs/tools counters.
447
479
  turns: z.number().optional().describe("Turns it has finished."),
448
480
  toolUses: z.number().optional().describe("Tools it has used, over its whole life."),
449
- // `running` reads the live subagent registry (swept after five minutes), `total` counts on the agent's own entry; a
450
- // child's spend is its own, never folded into the parent's cost.
481
+ // `running` reads the live subagent roster, `total` counts on the agent's own entry; a child's spend is its own,
482
+ // never folded into the parent's cost.
451
483
  subagents: z
452
484
  .object({
453
485
  running: z.number().describe("Subagents working right now."),
@@ -473,6 +505,25 @@ export const AgentSummarySchema = z.object({
473
505
  .object({
474
506
  landed: z.number().describe("Paths this conversation merged in."),
475
507
  present: z.number().describe("How many of them are still there, either pending or committed."),
508
+ // Read off who acted on the tree between the land and the moment it was found missing; only a sole candidate
509
+ // is named, so an unattributed removal says nothing rather than guessing.
510
+ removedBy: z
511
+ .discriminatedUnion("kind", [
512
+ z.object({
513
+ kind: z.literal("agent"),
514
+ id: z.string().describe("The conversation that took them out."),
515
+ title: z.string().optional().describe("What that conversation is called."),
516
+ }),
517
+ z.object({
518
+ kind: z.literal("person"),
519
+ email: z.string().optional().describe("Who it was, as the sandbox verified them. Absent when the request carried no identity."),
520
+ name: z.string().optional().describe("What to call them, when their sign-in carried a name."),
521
+ }),
522
+ ])
523
+ .optional()
524
+ .describe(
525
+ "Who took them out, when the sandbox could tell: an agent working in the workspace, or a person throwing the changes away. Absent when it could not tell, and from a sandbox older than it.",
526
+ ),
476
527
  })
477
528
  .optional()
478
529
  .describe(
@@ -527,6 +578,13 @@ export const AgentSummarySchema = z.object({
527
578
  .describe(
528
579
  "Outside conditions this conversation is parked on, each of which will wake it. Absent means none, which is nearly every conversation: an armed watch is why a finished-looking agent starts working by itself, and why a hosted machine will not go idle.",
529
580
  ),
581
+ // Kept in the needs store, so unlike `jobs` this survives a restart and stands while the conversation is idle.
582
+ needs: z
583
+ .array(AgentNeedSchema)
584
+ .optional()
585
+ .describe(
586
+ "What it is waiting on people for and has not got yet, oldest first. Absent means nothing: an open need is why an idle-looking agent still needs you.",
587
+ ),
530
588
  // In memory only: a daemon restart leaves transcript rows naming jobs this list no longer carries.
531
589
  jobs: z
532
590
  .array(
@@ -580,6 +638,12 @@ export const awaitsWake = (agent: { readonly awaitingWake?: boolean | undefined
580
638
  export type AgentWatch = NonNullable<AgentSummary["watches"]>[number];
581
639
  // One background job as a card carries it.
582
640
  export type AgentJob = NonNullable<AgentSummary["jobs"]>[number];
641
+ // Who took landed work out of the workspace, as the card names them.
642
+ export type LandedRemover = NonNullable<NonNullable<AgentSummary["landedPresence"]>["removedBy"]>;
643
+ // A land that broke, kept on the card until one goes through.
644
+ export type LandFailure = NonNullable<AgentSummary["landFailure"]>;
645
+ // The permission a running turn waits on, as a card can answer it.
646
+ export type WaitingPermission = NonNullable<AgentSummary["permissionAsk"]>;
583
647
  // AgentsListSchema is declared later, after AutomationApprovalSchema, since the fleet list carries held wakes and zod
584
648
  // needs that type declared first.
585
649
  export const AgentIdSchema = z.object({ id: z.string().min(1).describe("Which conversation.") });
@@ -610,6 +674,9 @@ export const AgentTranscriptQuerySchema = AgentIdSchema.extend({
610
674
  export const AgentToolChildrenQuerySchema = AgentIdSchema.extend({
611
675
  toolId: z.string().min(1).describe("Which tool call, by the id its card carries."),
612
676
  });
677
+ export const AgentSubagentQuerySchema = AgentIdSchema.extend({
678
+ subagentId: z.string().min(1).describe("Which subagent this conversation's runtime ran in-process, by the id of the call that started it."),
679
+ });
613
680
  // Absent `ids` archives every archivable finished agent (the lane's "Clear"); unarchive always names its own ids.
614
681
  export const AgentIdsSchema = z.object({ ids: z.array(z.string().min(1)).min(1).max(500).describe("Which conversations.") });
615
682
  // What actually moved, not the roster afterward: two archives finishing at once would otherwise let a slower snapshot
@@ -29,7 +29,7 @@ import { ZoneSchema } from "../time/zone.js";
29
29
  // turn.settled: fires once per isolated turn regardless of outcome, while the diff is still on screen
30
30
  // agent.landed: fires only once work reaches the main tree, including a later manual Land
31
31
  // deps.broken/deps.fixed: retired with the check that ran after every land (2026-09-27). Nothing emits them any more;
32
- // they stay so an automation written for them still reads, and it never fires. Main's red CI is the `ci` listener's.
32
+ // they stay so an automation written for them still reads, and it never fires. Main's failing CI is the `ci` listener's.
33
33
  export const WorkspaceEventKindSchema = z.enum(["turn.settled", "agent.landed", "deps.broken", "deps.fixed"]);
34
34
  export type WorkspaceEventKind = z.infer<typeof WorkspaceEventKindSchema>;
35
35
  // Payload of a workspace-triggered wake, delivered as JSON in $AUTOMATION_PAYLOAD and appended to the prompt.
@@ -45,7 +45,7 @@ export const WorkspaceEventSchema = z.object({
45
45
  outcome: z.enum(["landed", "conflict", "ready", "idle", "error"]),
46
46
  repos: z.array(z.object({ repo: z.string(), from: z.string(), dir: z.string() })),
47
47
  // Present only for `deps.*` events: which project broke, the command and exit code, a bounded log tail, and
48
- // `attempt` counting consecutive reds since the last green.
48
+ // `attempt` counting consecutive failures since the last pass.
49
49
  deps: z
50
50
  .object({
51
51
  project: z.string(),
@@ -104,6 +104,15 @@ export const SshConfigSchema = z.discriminatedUnion("auth", [
104
104
  user: z.string().min(1).describe("Which user to connect as."),
105
105
  privateKey: z.string().min(1).describe("The private key, whole. Stored with tight permissions and never echoed back."),
106
106
  }),
107
+ // The sandbox made this key itself (capabilities.sshKey), so its private half never crossed the wire; stored and
108
+ // written exactly like a pasted one.
109
+ z.object({
110
+ auth: z.literal("generated").describe("Sign in with a key the sandbox generated. Its private half never left the sandbox."),
111
+ host: z.string().min(1).describe("The machine's address."),
112
+ port: z.coerce.number().default(22).describe("Which port it listens on."),
113
+ user: z.string().min(1).describe("Which user to connect as."),
114
+ privateKey: z.string().min(1).describe("The private half of the generated key. Stored with tight permissions and never echoed back."),
115
+ }),
107
116
  z.object({
108
117
  auth: z.literal("password").describe("Sign in with a password."),
109
118
  host: z.string().min(1).describe("The machine's address."),
@@ -112,6 +121,14 @@ export const SshConfigSchema = z.discriminatedUnion("auth", [
112
121
  password: z.string().min(1).describe("The password. Stored, never echoed back."),
113
122
  }),
114
123
  ]);
124
+ // What a form sends where a generated key's private half goes: the token capabilities.sshKey answered with, which the
125
+ // add swaps for the key the daemon kept. VAULTED's sibling (policy/capability-secrets.ts) for a value not stored yet; a
126
+ // marker with nothing held behind it is refused, never written.
127
+ const STASHED_PREFIX = "__intentic_stashed__:";
128
+ export const stashedMarker = (token: string): string => `${STASHED_PREFIX}${token}`;
129
+ // The token a marker names; undefined for anything else (a real key, VAULTED, a blank).
130
+ export const stashedToken = (value: unknown): string | undefined =>
131
+ typeof value === "string" && value.startsWith(STASHED_PREFIX) && value.length > STASHED_PREFIX.length ? value.slice(STASHED_PREFIX.length) : undefined;
115
132
  // What is optional about the in-sandbox Docker Engine: `gpu` is an IMAGE option (rides the Dockerfile overlay, needs a
116
133
  // rebuild), everything else is an ENGINE option (rewrites daemon.json, restarts dockerd, no rebuild but stops running
117
134
  // containers). Flat strings, not nested booleans, to match the manifest's own two-state convention.
@@ -227,7 +244,8 @@ export const LOCAL_MODEL_WINDOW_MAX = 1_048_576;
227
244
  // against.
228
245
  export const LOCAL_MODEL_KV_BYTES_PER_TOKEN = 65_536;
229
246
  // What a rung honestly serves. `instant` is the one the connect view prefetches — it downloads in under a minute and
230
- // cannot drive a full agent turn, so no surface may sell it as one; `work` is what that view recommends for real use.
247
+ // cannot drive a full agent turn, so its catalog row is helper-only (Model.helperOnly): the chat picker does not offer
248
+ // it and the daemon refuses a turn on it. `work` is what that view recommends for real use.
231
249
  export type LocalModelTier = "instant" | "work";
232
250
  export interface LocalModelChoice {
233
251
  // Hugging Face owner/repo/file.gguf, exactly as LocalModelConfig.model carries it.
@@ -237,21 +255,57 @@ export interface LocalModelChoice {
237
255
  // estimate all read this, so a model cannot be priced two ways.
238
256
  readonly weightsBytes: number;
239
257
  readonly tier: LocalModelTier;
258
+ // The repository commit the file is fetched at, a sha and never a branch: `main` moves, and a re-upload under the
259
+ // same name would change what a card promised without the card changing.
260
+ readonly revision: string;
261
+ // The file's own sha256 at that commit (its LFS oid). A download that does not hash to it is deleted, not served.
262
+ readonly sha256: string;
240
263
  }
241
- // The curated list, smallest first. ponytail: these bytes are the Hugging Face tree API's answer for each file — the
264
+ // The curated list, smallest first. ponytail: sizes, revisions and digests are the Hugging Face API's answer for each
265
+ // file (`/api/models/<repo>/revision/main` for the sha, `/tree/<sha>` for the size and LFS oid), read 2026-09-29 — the
242
266
  // hand-written labels that predated them overstated gemma-4 12B by 2x and Qwen3.8 27B by a third, which is the class of
243
- // drift a machine-readable size exists to end.
267
+ // drift a machine-readable size exists to end. Bump a row's three together: each belongs to its revision.
244
268
  export const LOCAL_MODELS: readonly LocalModelChoice[] = [
245
- { id: "unsloth/Qwen3.5-2B-GGUF/Qwen3.5-2B-Q4_K_M.gguf", label: "Qwen3.5 2B", weightsBytes: 1_280_835_840, tier: "instant" },
269
+ {
270
+ id: "unsloth/Qwen3.5-2B-GGUF/Qwen3.5-2B-Q4_K_M.gguf",
271
+ label: "Qwen3.5 2B",
272
+ weightsBytes: 1_280_835_840,
273
+ tier: "instant",
274
+ revision: "f6d5376be1edb4d416d56da11e5397a961aca8ae",
275
+ sha256: "aaf42c8b7c3cab2bf3d69c355048d4a0ee9973d48f16c731c0520ee914699223",
276
+ },
246
277
  {
247
278
  id: "unsloth/Phi-4-mini-instruct-GGUF/Phi-4-mini-instruct-Q4_K_M.gguf",
248
279
  label: "Phi-4-mini 3.8B",
249
280
  weightsBytes: 2_491_874_272,
250
281
  tier: "work",
282
+ revision: "78eb92a46fc37e6b524df991ed9aca9bc6aa7b80",
283
+ sha256: "88c00229914083cd112853aab84ed51b87bdf6b9ce42f532d8c85c7c63b1730a",
284
+ },
285
+ {
286
+ id: "unsloth/Qwen3.5-9B-GGUF/Qwen3.5-9B-Q4_K_M.gguf",
287
+ label: "Qwen3.5 9B",
288
+ weightsBytes: 5_680_522_464,
289
+ tier: "work",
290
+ revision: "3885219b6810b007914f3a7950a8d1b469d598a5",
291
+ sha256: "03b74727a860a56338e042c4420bb3f04b2fec5734175f4cb9fa853daf52b7e8",
292
+ },
293
+ {
294
+ id: "unsloth/gemma-4-12b-it-GGUF/gemma-4-12b-it-Q4_K_M.gguf",
295
+ label: "Gemma 4 12B",
296
+ weightsBytes: 7_121_861_440,
297
+ tier: "work",
298
+ revision: "fc034cfff751157913579611efad8462ac1be606",
299
+ sha256: "0a270ec9fe6b34f4a0d33992b6135117b484ebc4766ab76b51d4ae8c457e4c42",
300
+ },
301
+ {
302
+ id: "unsloth/Qwen3.8-27B-GGUF/Qwen3.8-27B-UD-Q4_K_M.gguf",
303
+ label: "Qwen3.8 27B",
304
+ weightsBytes: 16_464_440_224,
305
+ tier: "work",
306
+ revision: "4ca720788d1e01f1bff70c033e0d0028fd02e502",
307
+ sha256: "322e194ff79741c7baa497c240f677f54b201b0efab44ca8e50f122b39123482",
251
308
  },
252
- { id: "unsloth/Qwen3.5-9B-GGUF/Qwen3.5-9B-Q4_K_M.gguf", label: "Qwen3.5 9B", weightsBytes: 5_680_522_464, tier: "work" },
253
- { id: "unsloth/gemma-4-12b-it-GGUF/gemma-4-12b-it-Q4_K_M.gguf", label: "Gemma 4 12B", weightsBytes: 7_121_861_440, tier: "work" },
254
- { id: "unsloth/Qwen3.8-27B-GGUF/Qwen3.8-27B-UD-Q4_K_M.gguf", label: "Qwen3.8 27B", weightsBytes: 16_464_440_224, tier: "work" },
255
309
  ];
256
310
  export const localModelChoice = (id: string): LocalModelChoice | undefined => LOCAL_MODELS.find((choice) => choice.id === id);
257
311
  // Exactly one row carries the instant tier; the fit module and the prefetch route take it from here rather than
@@ -460,3 +514,14 @@ export const CapabilityProbeSchema = z.object({
460
514
  who: z.string().optional().describe("Who the service said the credential belongs to, when it said."),
461
515
  });
462
516
  export type CapabilityProbe = z.infer<typeof CapabilityProbeSchema>;
517
+ // POST /capabilities/ssh-key: a key pair made inside the sandbox for a connection being set up. The public half is the
518
+ // whole answer; the private half stays in the daemon until an add sends `stashedMarker(token)` where it goes.
519
+ export const SshKeySchema = z.object({
520
+ publicKey: z.string().describe("The public half, as the one line a server's authorized_keys holds: `ssh-ed25519 AAAA… intentic-<sandbox>`."),
521
+ token: z
522
+ .string()
523
+ .describe(
524
+ "Stands for the private half, which never leaves the sandbox. Sent wrapped as a marker in the private key's place, it installs that key once, and it lapses after thirty minutes.",
525
+ ),
526
+ });
527
+ export type SshKey = z.infer<typeof SshKeySchema>;
package/src/schemas/ci.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  // ci: pipeline runs on the workspace repos' github/gitlab remotes
2
2
  import { z } from "zod";
3
3
  import { AgentRunPickSchema } from "./agent.js";
4
- import { RedDecisionSchema } from "./workspace/push-checks.js";
5
4
  // The daemon maps each workspace repo to its CI project via a connected github/gitlab capability, registers a webhook
6
5
  // to fire `ci` listener automations instantly, and serves the Pipelines rail from a webhook-freshened, poll-backfilled
7
6
  // cache. `host` names which provider API serves a repo; the listener provider is always `ci`, since a trigger narrows
@@ -112,31 +111,72 @@ export type CiRepo = z.infer<typeof CiRepoSchema>;
112
111
  // Fallback poll interval when a repo's webhook couldn't register; lives here since both the poller and the automation
113
112
  // editor need it to tell the owner what a `hookWarning` actually costs (minutes' delay, not the feature).
114
113
  export const CI_POLL_INTERVAL_MS = 2 * 60_000;
115
- // Main's CI while it is red, as the daemon's one fix agent on it sees it (ci/main-fixer.ts): from the first job that
116
- // failed until a later run of every workflow that failed on it passes, whatever else failed along the way.
117
- export const CiMainRedSchema = z.object({
114
+ // What was decided about a main-line branch while its CI fails, in the order it was decided (ci/main-fixer.ts).
115
+ export const MainFailureDecisionKindSchema = z.enum([
116
+ // Its one fix agent was put on it: by the sandbox at the first job that failed, or by a person's Fix press, which
117
+ // gives the agent its turns back.
118
+ "fix-up",
119
+ // Nobody was sent: repairs are switched off, or nothing could take it.
120
+ "reported",
121
+ // The fix agent had its turns, or stopped without a fix: it waits for a person.
122
+ "spent",
123
+ ]);
124
+ export type MainFailureDecisionKind = z.infer<typeof MainFailureDecisionKindSchema>;
125
+
126
+ // Why the fix agent handed the failure back (a `spent` decision), as a closed set the editor words for itself, so what a
127
+ // reader sees stays a few words in every language rather than whatever the turn's error happened to say.
128
+ export const MainFailureHandBackSchema = z.enum([
129
+ // It had every turn the sandbox gives one failure, and main still fails.
130
+ "turns",
131
+ // Its turn ended without changing anything, so the failure is likely not in the code.
132
+ "no-change",
133
+ // A person stopped it.
134
+ "stopped",
135
+ // Its turn was cut off.
136
+ "interrupted",
137
+ // Its turn failed.
138
+ "turn-failed",
139
+ // Its conversation is gone or archived.
140
+ "gone",
141
+ // It could not be told about the newest failure.
142
+ "refused",
143
+ ]);
144
+ export type MainFailureHandBack = z.infer<typeof MainFailureHandBackSchema>;
145
+
146
+ export const MainFailureDecisionSchema = z.object({
147
+ kind: MainFailureDecisionKindSchema.describe("What was decided."),
148
+ reason: MainFailureHandBackSchema.optional().describe("Why the fix agent handed it back, on a `spent` decision."),
149
+ conversationId: z.string().optional().describe("The conversation working on it, when one is."),
150
+ at: z.number().describe("When that was decided, in milliseconds."),
151
+ detail: z.string().optional().describe("One short sentence on why, in the sandbox's words."),
152
+ });
153
+ export type MainFailureDecision = z.infer<typeof MainFailureDecisionSchema>;
154
+
155
+ // A main-line branch while its CI fails, as the daemon's one fix agent on it sees it (ci/main-fixer.ts): from the first
156
+ // job that failed until a later run of every workflow that failed on it passes, whatever else failed along the way.
157
+ export const CiMainFailureSchema = z.object({
118
158
  repo: z.string().describe("Which workspace repository."),
119
159
  branch: z.string().describe("Which main-line branch."),
120
160
  since: z.number().describe("When its first job failed, in milliseconds."),
121
161
  runId: z.number().describe("The newest run that failed on it."),
122
- jobs: z.array(z.string()).describe("The jobs failing on it now, by name: the newest red run's failures, and any that failed since."),
162
+ jobs: z.array(z.string()).describe("The jobs failing on it now, by name: the newest failed run's failures, and any that failed since."),
123
163
  fixer: z
124
164
  .string()
125
165
  .optional()
126
- .describe("The one conversation working on it, which every failure goes to until the branch is green. Absent while nobody is on it."),
127
- decision: RedDecisionSchema.optional().describe(
166
+ .describe("The one conversation working on it, which every failure goes to until the branch passes. Absent while nobody is on it."),
167
+ decision: MainFailureDecisionSchema.optional().describe(
128
168
  "The latest thing decided about it: `fix-up` while the fix agent has it, `spent` once it waits for you (its turns are used up, or it stopped without a fix), `reported` when repairs are off.",
129
169
  ),
130
170
  });
131
- export type CiMainRed = z.infer<typeof CiMainRedSchema>;
171
+ export type CiMainFailure = z.infer<typeof CiMainFailureSchema>;
132
172
  export const CiRunsResponseSchema = z.object({
133
173
  repos: z.array(CiRepoSchema).describe("Which workspace repositories are wired to a forge, and how each one's notifications are set up."),
134
174
  // Newest first, across all mapped repos.
135
175
  runs: z.array(PipelineRunSchema).describe("Runs across all of them, newest first."),
136
- reds: z
137
- .array(CiMainRedSchema)
176
+ failures: z
177
+ .array(CiMainFailureSchema)
138
178
  .optional()
139
- .describe("Every main-line branch red right now, with the fix agent on it. Absent from a daemon that keeps none."),
179
+ .describe("Every main-line branch failing right now, with the fix agent on it. Absent from a daemon that keeps none."),
140
180
  });
141
181
  export type CiRunsResponse = z.infer<typeof CiRunsResponseSchema>;
142
182
  // Re-resolves repo → project + token on every call, so a stale card can't act on a mapping the workspace no longer has.
@@ -155,6 +195,11 @@ export const CiFixParamSchema = CiRunParamSchema.extend({
155
195
  pick: AgentRunPickSchema.describe(
156
196
  "Which model to open the conversation on, when somebody chose one. Leave it out for the sandbox's own choice, which is the ordinary path.",
157
197
  ),
198
+ // The browser's own chat pick, which the daemon never holds: without it an unpinned fix opened on a provider the
199
+ // owner never chose. Absent (an older app, or nobody pressed) keeps the sandbox's own fallback.
200
+ fallback: AgentRunPickSchema.describe(
201
+ "The model a new chat opens on for whoever pressed, for when nobody chose one and no model set for fixing pipelines can run. Used while this sandbox can serve its provider; left out, or not servable, the sandbox takes its default provider when connected, else the first one connected.",
202
+ ),
158
203
  // The verb the picker's bar was ended with, when an attempt already exists (state/fix-attempt-plan.ts). Left out,
159
204
  // an ended attempt is continued and a fresh press opens attempt 1; one still in play answers CONFLICT.
160
205
  mode: z