@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,301 @@
1
+ import { z } from "zod";
2
+
3
+ // A need: something an agent cannot finish its task without that only a person can provide (a connection, a secret's
4
+ // value, wider reach, a tool in the image). Unlike a parked card it outlives the turn that raised it: the sandbox keeps
5
+ // it until someone answers, draws it wherever it is waiting, and wakes the conversation when it is met
6
+ // (docs/architecture/needs.md). Every word on a need is the daemon's except `why`, the agent's own case.
7
+
8
+ export const NEED_KINDS = ["capability", "secret", "grant", "release", "environment"] as const;
9
+ export const NeedKindSchema = z.enum(NEED_KINDS).describe("What is being asked for: a connection, a secret's value, wider reach, a gated credential, or a tool in the image.");
10
+ export type NeedKind = z.infer<typeof NeedKindSchema>;
11
+
12
+ // `working` is a yes still being carried out: a connection whose form is open, an overlay approved but not yet built.
13
+ export const NeedStatusSchema = z
14
+ .enum(["open", "working", "met", "declined", "cancelled"])
15
+ .describe(
16
+ "Where it stands: open (waiting on a person), working (a person said yes and it is being set up), met, declined, or cancelled (the agent withdrew it, or its conversation went away).",
17
+ );
18
+ export type NeedStatus = z.infer<typeof NeedStatusSchema>;
19
+
20
+ export const OPEN_NEED_STATUSES: readonly NeedStatus[] = ["open", "working"];
21
+ export const isOpenNeed = (need: { readonly status: NeedStatus }): boolean => OPEN_NEED_STATUSES.includes(need.status);
22
+
23
+ // A setting value as a form holds it: every field of a catalog entry is a string on the way in.
24
+ const FieldValuesSchema = z.record(z.string(), z.string());
25
+
26
+ export const CapabilityNeedModeSchema = z
27
+ .enum(["connect", "reconnect", "change"])
28
+ .describe("Connect something new, give a connected one a credential that works again, or change a setting on a connected one.");
29
+ export type CapabilityNeedMode = z.infer<typeof CapabilityNeedModeSchema>;
30
+
31
+ export const CapabilityNeedSchema = z.object({
32
+ kind: z.literal("capability"),
33
+ entry: z.string().min(1).describe("The catalog entry, as the catalog names it."),
34
+ name: z.string().describe("What the catalog calls it, never the agent's spelling."),
35
+ mode: CapabilityNeedModeSchema,
36
+ instance: z.string().optional().describe("The connection a reconnect or a change is about."),
37
+ target: z.string().optional().describe("The site, host or address the connection is for, when the entry can hold several."),
38
+ prefill: FieldValuesSchema.optional().describe(
39
+ "Settings the agent could fill in for a new connection, never a credential: the daemon keeps only the entry's own non-secret fields.",
40
+ ),
41
+ changes: FieldValuesSchema.optional().describe("For a change: each setting and the value it would take. Never a credential."),
42
+ reason: z.string().optional().describe("The daemon's own sentence on why this is the ask, such as the refusal a connected credential keeps getting."),
43
+ reported: z
44
+ .boolean()
45
+ .optional()
46
+ .describe(
47
+ "The agent reported the credential refused while the connection still probes as working, so only a person's word that it is fixed meets it: the probe could not see the refusal in the first place.",
48
+ ),
49
+ });
50
+ export type CapabilityNeed = z.infer<typeof CapabilityNeedSchema>;
51
+
52
+ // The alphabet a `{{secret:NAME}}` reference resolves; a pasted value is stored under exactly this name.
53
+ export const SECRET_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
54
+
55
+ export const SecretNeedSchema = z.object({
56
+ kind: z.literal("secret"),
57
+ name: z.string().regex(SECRET_NAME).describe("The name it is stored under, and what `{{secret:NAME}}` will resolve."),
58
+ where: z.string().max(200).optional().describe("How it will be used: the header, the command, the site it goes to."),
59
+ link: z
60
+ .string()
61
+ .url()
62
+ .refine((url) => url.startsWith("https://"), "only an https link")
63
+ .optional()
64
+ .describe("Where a person gets one, shown as a link on the card."),
65
+ hint: z.string().max(120).optional().describe("What a valid one looks like, so a wrong paste is caught by eye."),
66
+ replace: z.boolean().optional().describe("One is stored under this name and is being refused: the ask is for a new value in its place."),
67
+ });
68
+ export type SecretNeed = z.infer<typeof SecretNeedSchema>;
69
+
70
+ export const GrantSubjectSchema = z
71
+ .enum(["capability", "folder", "shelf", "site"])
72
+ .describe(
73
+ "A connected capability the persona leaves out, a folder outside the conversation's reach, a whole shelf of tools, or a site in the person's own browser, which only their browser extension can allow.",
74
+ );
75
+ export type GrantSubject = z.infer<typeof GrantSubjectSchema>;
76
+
77
+ // A site as a person's browser grants it: the host alone, whatever scheme, path or pattern it was written with
78
+ // ("https://github.com/*" and "github.com" are one site). What a site grant need is keyed and met by.
79
+ export const grantSite = (origin: string): string =>
80
+ origin
81
+ .trim()
82
+ .replace(/^[a-z][a-z0-9+.-]*:\/\//i, "")
83
+ .replace(/[/?#].*$/, "")
84
+ .toLowerCase();
85
+
86
+ export const GRANT_SHELVES = ["files", "shell", "code", "web", "browser", "delegate", "sandbox"] as const;
87
+ export const GrantShelfSchema = z.enum(GRANT_SHELVES);
88
+ export type GrantShelf = z.infer<typeof GrantShelfSchema>;
89
+
90
+ export const GrantScopeSchema = z
91
+ .enum(["conversation", "persona"])
92
+ .describe("How far a yes goes: this conversation only, or the persona itself, for every conversation that wears it.");
93
+ export type GrantScope = z.infer<typeof GrantScopeSchema>;
94
+
95
+ export const GrantNeedSchema = z.object({
96
+ kind: z.literal("grant"),
97
+ subject: GrantSubjectSchema,
98
+ what: z.string().min(1).describe("The capability's id, the folder, or the shelf."),
99
+ label: z.string().describe("What it is, in the daemon's words: the account and its kind, the folder, the shelf's name."),
100
+ persona: z.string().optional().describe("The persona that withholds it, when one does."),
101
+ scope: GrantScopeSchema.optional().describe("How far the yes went, once there was one."),
102
+ });
103
+ export type GrantNeed = z.infer<typeof GrantNeedSchema>;
104
+
105
+ export const ReleaseNeedSchema = z.object({
106
+ kind: z.literal("release"),
107
+ subject: z.string().min(1).describe("The gated account or connector."),
108
+ approvers: z.array(z.string()).describe("Who may release it. Anyone else's answer is refused and leaves it waiting."),
109
+ });
110
+ export type ReleaseNeed = z.infer<typeof ReleaseNeedSchema>;
111
+
112
+ export const EnvironmentNeedSchema = z.object({
113
+ kind: z.literal("environment"),
114
+ tool: z.string().min(1).describe("What the steps install, the name the proposal is filed under."),
115
+ steps: z.string().min(1).describe("The Dockerfile steps proposed for the image's custom section: RUN and ENV lines only."),
116
+ approvedHash: z.string().optional().describe("The overlay these steps were approved into; met once the running container was built from it."),
117
+ });
118
+ export type EnvironmentNeed = z.infer<typeof EnvironmentNeedSchema>;
119
+
120
+ export const NeedSubjectSchema = z
121
+ .discriminatedUnion("kind", [CapabilityNeedSchema, SecretNeedSchema, GrantNeedSchema, ReleaseNeedSchema, EnvironmentNeedSchema])
122
+ .describe("What exactly is asked for.");
123
+ export type NeedSubject = z.infer<typeof NeedSubjectSchema>;
124
+
125
+ // How the agent learned the outcome: the call it raised the need with, words into its live turn, a turn of its own, or
126
+ // queued behind what the conversation was doing.
127
+ export const NeedToldSchema = z.enum(["call", "turn", "queued"]);
128
+ export type NeedTold = z.infer<typeof NeedToldSchema>;
129
+
130
+ export const NeedSchema = z.object({
131
+ id: z.string().describe("The need's handle, the one `needs cancel` takes."),
132
+ conversationId: z.string().describe("The conversation that asked, and the one its answer wakes."),
133
+ subject: NeedSubjectSchema,
134
+ title: z.string().describe("The one line it leads with, in the daemon's words."),
135
+ why: z.string().max(280).optional().describe("The agent's case for it, and the only words on a need that are the agent's."),
136
+ status: NeedStatusSchema,
137
+ createdAt: z.number().describe("When it was raised, in milliseconds."),
138
+ updatedAt: z.number().describe("When it last moved, in milliseconds."),
139
+ answeredBy: z.string().optional().describe("Who answered it, as the sandbox verified them."),
140
+ outcome: z.string().optional().describe("How it ended, in the daemon's words, once it has."),
141
+ told: NeedToldSchema.optional().describe("How the agent heard the outcome, once it has."),
142
+ unattended: z.boolean().optional().describe("Raised by a turn nobody was watching, so the card waited for whoever came next."),
143
+ });
144
+ export type Need = z.infer<typeof NeedSchema>;
145
+
146
+ // What an agent may send: the kind and what it can know about it. Names, titles and every other field the card shows are
147
+ // the daemon's to fill in, from the catalog, the persona, the gate policy.
148
+ export const NeedAskSchema = z.discriminatedUnion("kind", [
149
+ z.object({
150
+ kind: z.literal("capability"),
151
+ entry: z.string().min(1).describe("The catalog entry or a connected instance's id."),
152
+ target: z.string().max(200).optional().describe("The site, host or address it is for."),
153
+ set: FieldValuesSchema.optional().describe("Settings to fill in, or to change on a connected one. Credentials are refused."),
154
+ reconnect: z
155
+ .boolean()
156
+ .optional()
157
+ .describe("It is connected, but its credential is being refused: ask for a new one rather than being told to use it."),
158
+ }),
159
+ SecretNeedSchema,
160
+ z.object({
161
+ kind: z.literal("grant"),
162
+ subject: GrantSubjectSchema,
163
+ what: z.string().min(1).max(400),
164
+ }),
165
+ z.object({ kind: z.literal("release"), subject: z.string().min(1) }),
166
+ z.object({ kind: z.literal("environment"), tool: z.string().min(1).max(64), steps: z.string().min(1).max(20_000) }),
167
+ ]);
168
+ export type NeedAsk = z.infer<typeof NeedAskSchema>;
169
+
170
+ // The longest a raising call holds: under the agent shell's 110-second cutoff, so the answer returns on the call.
171
+ export const NEED_WAIT_MAX_S = 100;
172
+ export const NEED_WAIT_DEFAULT_S = 90;
173
+
174
+ export const NeedRaiseSchema = z.object({
175
+ ask: NeedAskSchema,
176
+ why: z.string().max(280).optional(),
177
+ wait: z
178
+ .number()
179
+ .int()
180
+ .min(0)
181
+ .max(NEED_WAIT_MAX_S)
182
+ .optional()
183
+ .describe("Seconds to hold the call for an answer. Absent is 90 for a watched turn and 0 for an unattended one."),
184
+ });
185
+ export type NeedRaise = z.infer<typeof NeedRaiseSchema>;
186
+
187
+ // How a raising call ended. `met`: usable now, exit 0. `open`: asked and still waiting, exit 3; the answer arrives in the
188
+ // conversation by itself. `refused`: nothing was raised or it was declined, exit 1.
189
+ export const NeedRaisedSchema = z.object({
190
+ state: z.enum(["met", "open", "refused"]),
191
+ message: z.string().describe("The sentence the CLI prints, written for the agent to act on."),
192
+ need: NeedSchema.optional(),
193
+ code: z.string().optional().describe("A refusal's type, for a script to branch on."),
194
+ });
195
+ export type NeedRaised = z.infer<typeof NeedRaisedSchema>;
196
+
197
+ // A person's answer, by what the card offered. A secret's value travels on its own route (needs.contract.ts), never here.
198
+ export const NeedAnswerSchema = z.discriminatedUnion("kind", [
199
+ z.object({ kind: z.literal("decline"), note: z.string().max(500).optional().describe("Why not, passed to the agent.") }),
200
+ // A connection's yes before its form is saved: moves the need to working, so the board keeps saying so.
201
+ z.object({ kind: z.literal("accept") }),
202
+ // A change to a connected capability's settings, applied as the card showed it.
203
+ z.object({ kind: z.literal("apply") }),
204
+ z.object({ kind: z.literal("grant"), scope: GrantScopeSchema }),
205
+ z.object({ kind: z.literal("release") }),
206
+ z.object({ kind: z.literal("approve") }),
207
+ ]);
208
+ export type NeedAnswer = z.infer<typeof NeedAnswerSchema>;
209
+
210
+ export const NeedIdParamSchema = z.object({ id: z.string().min(1).describe("Which need.") });
211
+ export const NeedAnswerInputSchema = NeedIdParamSchema.extend({ answer: NeedAnswerSchema });
212
+ export const NeedSecretInputSchema = NeedIdParamSchema.extend({
213
+ value: z.string().min(1).max(64_000).describe("The secret's value. Stored, never echoed, never written into a transcript."),
214
+ });
215
+ export const NeedsQuerySchema = z.object({
216
+ conversationId: z.string().optional().describe("One conversation's needs. Absent is every conversation's."),
217
+ open: z.boolean().optional().describe("Only the ones still waiting."),
218
+ });
219
+ export const NeedsListSchema = z.object({ needs: z.array(NeedSchema).describe("Newest first.") });
220
+
221
+ // What `capabilities list` reads (GET /capabilities/connectable, a raw route): shared by the daemon's handler and the
222
+ // CLI's suite, so the two cannot drift apart again the way a renamed field once left the command printing nothing.
223
+ export const CapabilityConnectableSchema = z.object({
224
+ entries: z.array(
225
+ z.object({
226
+ entry: z.string().describe("The catalog entry's id, what `capabilities request` takes."),
227
+ name: z.string(),
228
+ description: z.string(),
229
+ connected: z.boolean().describe("Whether an instance of it is live, not merely added."),
230
+ }),
231
+ ),
232
+ suggested: z
233
+ .array(
234
+ z.object({
235
+ entry: z.string(),
236
+ claim: z.string().describe("What the workspace seems to want, in words."),
237
+ evidence: z.string().describe("What was read to say so, verbatim: a file, a remote."),
238
+ }),
239
+ )
240
+ .default([]),
241
+ });
242
+ export type CapabilityConnectable = z.infer<typeof CapabilityConnectableSchema>;
243
+
244
+ // One open need as the conversation's card lists it: enough for the board to say what the agent waits on.
245
+ export const AgentNeedSchema = z.object({
246
+ id: z.string(),
247
+ kind: NeedKindSchema,
248
+ title: z.string(),
249
+ status: NeedStatusSchema,
250
+ });
251
+ export type AgentNeed = z.infer<typeof AgentNeedSchema>;
252
+
253
+ // What a person allowed one conversation beyond its persona and area, from a grant need's card
254
+ // (personas/conversation-grants.ts). Honoured from that conversation's next turn on, and revocable on the Grants page.
255
+ export const ConversationGrantSchema = z.object({
256
+ capabilities: z.array(z.string()).default([]).describe("Connected capabilities it may use although its persona leaves them out."),
257
+ folders: z.array(z.string()).default([]).describe("Workspace folders its file tools may touch beyond its fence."),
258
+ shelves: z.array(GrantShelfSchema).default([]).describe("Shelves of tools opened for it."),
259
+ // Answered on an install card's "Allow installs for this conversation", read live by the command gate, so it holds
260
+ // from the install that asked onward, this turn included, and across restarts.
261
+ installs: z
262
+ .boolean()
263
+ .default(false)
264
+ .describe("Whether its own dependency installs run without asking, where the owner's setting would otherwise ask first."),
265
+ updatedAt: z.number().describe("When it last changed, in milliseconds."),
266
+ by: z.string().optional().describe("Who last allowed something here."),
267
+ });
268
+ export type ConversationGrant = z.infer<typeof ConversationGrantSchema>;
269
+
270
+ // Every yes still standing, by conversation: what its grants widened past its persona or area, and which gated
271
+ // credentials a named person released to it. Reviewed and taken back from Needs you.
272
+ export const StandingGrantsSchema = z.object({
273
+ conversations: z.array(
274
+ z.object({
275
+ conversationId: z.string(),
276
+ capabilities: z.array(z.string()).describe("Connected capabilities allowed although its persona leaves them out."),
277
+ folders: z.array(z.string()).describe("Workspace folders its file tools may touch beyond its fence."),
278
+ shelves: z.array(GrantShelfSchema).describe("Shelves of tools opened for it."),
279
+ installs: z.boolean().default(false).describe("Whether its own dependency installs run without asking."),
280
+ by: z.string().optional().describe("Who last allowed one of those."),
281
+ updatedAt: z.number().optional().describe("When one of those last changed, in milliseconds."),
282
+ releases: z
283
+ .array(z.object({ subject: z.string(), approvedBy: z.string(), at: z.number() }))
284
+ .describe("Gated credentials released to it, until somebody takes one back."),
285
+ }),
286
+ ),
287
+ });
288
+ export type StandingGrants = z.infer<typeof StandingGrantsSchema>;
289
+
290
+ export const GrantRevokeSchema = z.object({
291
+ conversationId: z.string(),
292
+ kind: z
293
+ .enum(["capability", "folder", "shelf", "release", "install"])
294
+ .describe("Which kind of yes: a grant past the persona or area, a credential's release, or letting its installs run unasked."),
295
+ what: z.string().describe("The capability id, folder, shelf or released credential it named; empty for installs."),
296
+ });
297
+ export type GrantRevoke = z.infer<typeof GrantRevokeSchema>;
298
+
299
+ // The file the needs store keeps: every open need and the recent closed ones, by id.
300
+ export const NeedsFileSchema = z.record(z.string(), NeedSchema);
301
+ export type NeedsFile = z.infer<typeof NeedsFileSchema>;
@@ -4,7 +4,7 @@ import { z } from "zod";
4
4
  import { type ModelSource, readyChain } from "../models/model-pins.js";
5
5
  import { type ModelPin, ModelPinSchema } from "./agent.js";
6
6
  import { entryId } from "./internal.js";
7
- import { SkillDraftSchema, SkillNameSchema, SystemPromptModeSchema } from "./settings.js";
7
+ import { SkillDraftSchema, SkillNameSchema, SYSTEM_PROMPT_MAX, SystemPromptModeSchema } from "./settings.js";
8
8
  // A named face the sandbox shows outward: who it speaks as, what it may do, where it works, what it's told, what it
9
9
  // runs on. No credential lives on the card; accounts stay private, per-sandbox. Not a security boundary for a watched
10
10
  // chat — a real fence only for an unattended turn, whose resolver defaults to nothing.
@@ -176,7 +176,12 @@ export interface TurnBriefingFixture {
176
176
  export const TURN_BRIEFING_FIXTURES: readonly TurnBriefingFixture[] = [
177
177
  { label: "Where this turn's files live", why: "Without it a runtime that is only cwd'd into its branch writes into the shared checkout." },
178
178
  { label: "Who this turn is acting as", why: "The card's own identity, on the runtimes that have nowhere else to put it." },
179
- { label: "Standing instructions for this workspace", why: "Your own AGENTS.md rules. Replace them by giving this card its own system prompt." },
179
+ // Kept under a card's own system prompt too (system-prompt.ts `ownPrompt`): writing a prompt withdraws this product's
180
+ // guidance, never the owner's own rules.
181
+ {
182
+ label: "Standing instructions for this workspace",
183
+ why: "Your own AGENTS.md rules, sent even when this card has its own system prompt. Change them in AGENTS.md.",
184
+ },
180
185
  {
181
186
  label: "Some connected accounts need a person's approval",
182
187
  why: "Names who to ask; without it a withheld account reads as simply not connected.",
@@ -307,7 +312,7 @@ export type PersonaKit = z.infer<typeof PersonaKitSchema>;
307
312
  export const PersonaPromptSchema = PersonaIdParamSchema.extend({
308
313
  prompt: z
309
314
  .string()
310
- .max(20000)
315
+ .max(SYSTEM_PROMPT_MAX)
311
316
  .describe(
312
317
  "What to tell this persona. Sending an empty one removes it entirely rather than storing a blank, so the persona falls back to the sandbox's own instructions.",
313
318
  ),
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { AccountStateSchema, AccountUsageSchema } from "./plan-limits.js";
3
+ import { SignInCatcherSchema } from "../loopback-catch.js";
3
4
  import { KeyedProviderSchema } from "./provider-subscriptions.js";
4
5
  // Claude uses PKCE paste-back (start → exchange); Codex uses OpenAI's device-code flow (start → poll). A sandbox can
5
6
  // hold several accounts per provider; `id` is the store key, `label` the display name. Tokens never ride this shape —
@@ -91,6 +92,12 @@ export const LoginStartSchema = z.object({
91
92
  "This attempt's id, for finishing or abandoning it. Not a credential and not redeemable: the proof that completes the sign-in never leaves the sandbox.",
92
93
  ),
93
94
  expiresAt: z.number().describe("When this attempt stops being answerable, in milliseconds, so a card can stop waiting instead of spinning."),
95
+ catchers: z
96
+ .array(SignInCatcherSchema)
97
+ .optional()
98
+ .describe(
99
+ "Who is watching for where the browser lands, on the machine it is on: a device or a browser of yours. Listed means the sign-in finishes by itself once the page is approved there; the paste stays open for a browser anywhere else. Empty or absent means nothing is watching.",
100
+ ),
94
101
  });
95
102
  export type LoginStart = z.infer<typeof LoginStartSchema>;
96
103
  // e.g. Z.ai's international vs. mainland plans.
@@ -111,6 +118,18 @@ export const LoginCompletedSchema = z.object({
111
118
  "The account it connected, where the sign-in ends here. Absent means keep watching the account list.",
112
119
  ),
113
120
  });
121
+ // One attempt's standing, for a card to watch the attempt itself rather than the account list: a list that already
122
+ // held an account says nothing about whether this sign-in, adding another, has landed.
123
+ export const LoginStatusSchema = z.discriminatedUnion("status", [
124
+ z.object({ status: z.literal("wait") }),
125
+ z.object({
126
+ status: z.literal("ok"),
127
+ account: OauthAccountSchema.optional().describe("The account it connected. Absent where the row lands in the account list a little later."),
128
+ }),
129
+ z.object({ status: z.literal("error"), error: z.string().min(1).describe("Why it failed, to show as it is.") }),
130
+ ]);
131
+ export type LoginStatus = z.infer<typeof LoginStatusSchema>;
132
+ export const LoginStatusQuerySchema = z.object({ handshake: z.string().min(1).describe("Which attempt.") });
114
133
  // Tidiness, not a security boundary: an unanswered attempt also times out on its own (`expiresAt`).
115
134
  export const LoginCancelSchema = z.object({ handshake: z.string().min(1).describe("Which attempt to stop waiting on.") });
116
135
  // codex/grok/kimi/gemini via CLIProxyAPI; `flow` is explicit even when a provider's URL already embeds an optional
@@ -124,6 +143,12 @@ export const TranslatorStartSchema = z.object({
124
143
  .describe(
125
144
  "Which shape this is. A device sign-in finishes by itself and you poll the attempt; a redirect needs the address it landed on handed back. Said outright rather than guessed at from whether a code happens to exist.",
126
145
  ),
146
+ catchers: z
147
+ .array(SignInCatcherSchema)
148
+ .optional()
149
+ .describe(
150
+ "Who is watching for where the browser lands, on the machine it is on: a device or a browser of yours. Listed means the sign-in finishes by itself once the page is approved there; the paste stays open for a browser anywhere else. Empty or absent means nothing is watching.",
151
+ ),
127
152
  });
128
153
  export const TranslatorStatusSchema = z.discriminatedUnion("status", [
129
154
  z.object({ status: z.literal("wait") }),
@@ -142,6 +167,13 @@ export const TranslatorCompleteSchema = z.object({
142
167
  // Order is meaningful: the provider's own preference order, never re-ranked.
143
168
  export const ModelBadgeSchema = z.enum(["reasoning", "fast"]);
144
169
  export type ModelBadge = z.infer<typeof ModelBadgeSchema>;
170
+ // Why a model may run one-shot helper jobs (a commit message, a title) and never an agent turn. Only a positive answer
171
+ // sets it, so a server that says nothing leaves a model usable:
172
+ // no-tool-calls: its server reports a chat template that cannot call tools (llama.cpp's /props `chat_template_caps`),
173
+ // and llama.cpp drops a turn's tools without a word, so the turn would run and do nothing useful
174
+ // instant-tier: the curated local model sold as too small to drive a turn (LOCAL_MODELS' `instant` rung)
175
+ export const HelperOnlySchema = z.enum(["no-tool-calls", "instant-tier"]);
176
+ export type HelperOnly = z.infer<typeof HelperOnlySchema>;
145
177
  export const ModelSchema = z.object({
146
178
  id: z.string().describe("What to name when asking for this model."),
147
179
  label: z.string().describe("What to call it on screen."),
@@ -156,6 +188,9 @@ export const ModelSchema = z.object({
156
188
  // The served window, not the training length — memory can clamp it lower. Turns are refused against this. Absent
157
189
  // means unknown, never unlimited.
158
190
  contextWindow: z.number().optional().describe("How many tokens this model will accept in one request, where the server publishes it."),
191
+ helperOnly: HelperOnlySchema.optional().describe(
192
+ "Set where this model may write commit messages, titles and other one-shot jobs but never run a chat turn, and why: its server says it cannot call tools, or it is the small local model kept for quick jobs. Absent means nothing has said it cannot.",
193
+ ),
159
194
  // Epoch seconds. A fact about the MODEL, not about any one account: a routed provider balances across credentials,
160
195
  // so a model every one of them is benched on is unrunnable however much headroom their rings show. Absent is the
161
196
  // ordinary case and means runnable, never "unknown".
@@ -1,5 +1,6 @@
1
1
  // secrets: user-supplied env-var secrets the daemon writes to desired-state/.env
2
2
  import { z } from "zod";
3
+ import { SECRET_HOST_MAX, SECRET_HOST_PATTERN_RE, SECRET_HOSTS_MAX } from "../policy/secret-hosts.js";
3
4
  // Straight to the sandbox daemon, never through the platform; `apply` reloads .env with no restart. `list` returns keys
4
5
  // only; `reveal` is the one owner-only exception that returns a value.
5
6
  // An env var name, as a shell exports it. Exported so the browser validates against this rather than restating it:
@@ -19,6 +20,29 @@ export const SecretKeysSchema = z.object({
19
20
  export const SecretKeyParamSchema = z.object({ key: secretKey.describe("Which secret, by name.") });
20
21
  export const SecretRevealSchema = z.object({ value: z.string().describe("The value itself. The only place in this API one is ever returned.") });
21
22
 
23
+ // A secret nobody has to find or paste: a session key, a webhook signing secret, a database password the task sets up
24
+ // itself. The sandbox makes it and keeps it; the caller gets its reference and length, never the value.
25
+ export const SECRET_FORMATS = ["hex", "base64url", "alnum"] as const;
26
+ export const SecretGenerateSchema = z.object({
27
+ key: secretKey.describe("The name to store it under: a name nothing here holds yet, since a new value would break whatever uses the old one."),
28
+ bytes: z
29
+ .number()
30
+ .int()
31
+ .min(16)
32
+ .max(128)
33
+ .default(32)
34
+ .describe("How much randomness, in bytes. 32 unless whatever reads it demands a particular length."),
35
+ format: z
36
+ .enum(SECRET_FORMATS)
37
+ .default("hex")
38
+ .describe("How it is spelled: `hex` (0-9, a-f), `base64url` (letters, digits, - and _), or `alnum` (letters and digits only, for readers that refuse symbols)."),
39
+ });
40
+ export const SecretGeneratedSchema = z.object({
41
+ key: z.string().describe("The name it is stored under."),
42
+ length: z.number().int().describe("How many characters it is, which a reader's validation may care about."),
43
+ stored: z.enum(["env", "sandbox"]).describe("Where it was kept: desired-state/.env once DevOps is active, the sandbox's own store before that."),
44
+ });
45
+
22
46
  // A wall against the agent's own judgment, not a compromised container: a shell here can read the policy same as the
23
47
  // vault. Approvers are an exact list, never a role floor; the owner isn't on it unless added.
24
48
  export const CredentialGateScopeSchema = z
@@ -81,6 +105,66 @@ export const CredentialGrantSchema = z.object({
81
105
  message: z.string().describe("What the grant means in practice, and what to do next."),
82
106
  });
83
107
 
108
+ // The host guard: the second way a secret can need a person, beside a named approver. On, a use goes unasked only when
109
+ // every host its text names is on the secret's list; one aimed anywhere else, or anywhere its text does not show, waits
110
+ // for a person's click, whatever the safety judge says and whether or not it could be asked. Off, the guard never asks.
111
+ // Keyed like a gate: a secret by its name, a connected capability by its id, covering every field of it.
112
+ export const SecretHostSchema = z
113
+ .string()
114
+ .max(SECRET_HOST_MAX)
115
+ .regex(SECRET_HOST_PATTERN_RE)
116
+ .describe(
117
+ "One host, `api.github.com`, or every host under a domain, `*.github.com` (which does not include github.com itself). Lowercase, no scheme or port.",
118
+ );
119
+
120
+ export const SecretHostSourceSchema = z
121
+ .enum(["owner", "connector"])
122
+ .describe(
123
+ "Who set it: the owner, or the connector the credential belongs to, whose guard is on with its own service's hosts until the owner changes it.",
124
+ );
125
+ export type SecretHostSource = z.infer<typeof SecretHostSourceSchema>;
126
+
127
+ export const SecretHostGuardSchema = z.object({
128
+ subject: z.string().min(1).describe("Which secret, by the name its reference carries, or which connected capability, by its id."),
129
+ kind: CredentialGateKindSchema,
130
+ guard: z.boolean().describe("On: a use off the list, or whose destination cannot be read, asks a person first. Off: it never asks."),
131
+ hosts: z
132
+ .array(SecretHostSchema)
133
+ .max(SECRET_HOSTS_MAX)
134
+ .describe("Where it goes without asking while the guard is on. Empty with the guard on: every use asks. Kept while it is off, for turning it back on."),
135
+ source: SecretHostSourceSchema,
136
+ });
137
+ export type SecretHostGuard = z.infer<typeof SecretHostGuardSchema>;
138
+
139
+ export const SecretHostGuardsSchema = z.object({
140
+ guards: z
141
+ .array(SecretHostGuardSchema)
142
+ .describe("Every secret whose host guard has been set, or that a connector guards by default. One not listed has its guard off. Names and hosts only, never a value."),
143
+ });
144
+
145
+ export const SecretHostGuardSetSchema = z.object({
146
+ subject: z.string().min(1).describe("Which secret, by name, or which connected capability, by id."),
147
+ kind: CredentialGateKindSchema.optional().describe("Whether the subject is a secret or a capability. Worked out from the name when absent."),
148
+ guard: z.boolean().describe("Whether a use off the list, or whose destination cannot be read, must ask a person first."),
149
+ hosts: z
150
+ .array(SecretHostSchema)
151
+ .max(SECRET_HOSTS_MAX)
152
+ .describe(
153
+ "The whole new list. Turning the guard on or taking hosts away is open to anybody who may use secrets; turning it off or adding a host is the owner's to approve.",
154
+ ),
155
+ // Filled by the CLI itself, so the model cannot aim the owner's card at somebody else's conversation.
156
+ conversationId: z
157
+ .string()
158
+ .optional()
159
+ .describe("Which conversation to ask the owner in, when the change needs them. The CLI fills this from the running turn."),
160
+ });
161
+
162
+ export const SecretHostGuardSetResultSchema = z.object({
163
+ guard: z.boolean().describe("Whether the guard is on now."),
164
+ hosts: z.array(z.string()).describe("Where it goes without asking while the guard is on."),
165
+ approvedBy: z.string().optional().describe("Who approved the change, when it needed the owner's click. Absent when nobody had to."),
166
+ });
167
+
84
168
  // Across every store: env/generated secrets, capability credentials, AI-provider accounts. Values never ride this
85
169
  // shape; `revealable` says whether `reveal` can return one (everything but provider accounts).
86
170
  export const SecretInventoryEntrySchema = z.object({
@@ -128,6 +212,15 @@ export const SecretInventoryEntrySchema = z.object({
128
212
  })
129
213
  .optional()
130
214
  .describe("Who has to release this before the agent can use it, and for how long one release lasts. Absent when it is not gated."),
215
+ // Joined from the host guards, the owner's and a connector's own, so the row can say where it may go.
216
+ hosts: z
217
+ .object({
218
+ guard: z.boolean().describe("Whether a use off the list, or whose destination cannot be read, asks a person first."),
219
+ list: z.array(z.string()).describe("The hosts it goes to unasked while the guard is on, each exact or `*.domain`."),
220
+ source: SecretHostSourceSchema,
221
+ })
222
+ .optional()
223
+ .describe("Its host guard. Absent when none was ever set and no connector sets one: the guard is off."),
131
224
  });
132
225
  export type SecretInventoryEntry = z.infer<typeof SecretInventoryEntrySchema>;
133
226
  export const SecretInventorySchema = z.object({
@@ -107,4 +107,6 @@ export const SETTINGS_HISTORY = [
107
107
  keepWarmObject,
108
108
  // Where the check after landing ran (2026-09-27): nothing checks work after it lands any more, CI does.
109
109
  at("offload", drop("landCheck")),
110
+ // The model list for the fix a refused push offered (2026-09-28): a refused push now says why and offers nothing.
111
+ at("modelRoles", drop("pre-push-fix")),
110
112
  ] as const;
@@ -23,7 +23,7 @@ describe(`which built-in stands at which moment`, () => {
23
23
  expect(rule(`agent.landed`, { kind: `command`, command: `pnpm test` }).success).toBe(false);
24
24
  });
25
25
 
26
- test(`there is no push moment: the repository's own pre-push hook gates a push`, () => {
26
+ test(`there is no push moment: nothing runs on the way to a push, CI checks what was pushed`, () => {
27
27
  expect(rule(`push.starting`, { kind: `command`, command: `pnpm test` }).success).toBe(false);
28
28
  });
29
29
  });
@@ -111,3 +111,21 @@ describe(`the folded keep-warm settings`, () => {
111
111
  expect(convertDocument(SETTINGS_HISTORY, `object`, current, true).changes).toEqual([]);
112
112
  });
113
113
  });
114
+
115
+ // The pre-push fix's model list went with the fix a refused push used to offer (2026-09-28). A settings file that still
116
+ // names it must read, since an unknown role refuses the whole file, and every other job's list stays as its owner set it.
117
+ describe(`the retired pre-push fix role`, () => {
118
+ const pin = { provider: `claude`, model: `claude-opus-5-5` };
119
+
120
+ test(`is dropped from a stored file, leaving every other job's list, and the file reads`, () => {
121
+ const stored = { modelRoles: { "pipeline-fix": [pin], "pre-push-fix": [pin] } };
122
+ expect(SandboxSettingsSchema.safeParse(stored).success).toBe(false);
123
+ const converted = convertDocument(SETTINGS_HISTORY, `object`, stored, true);
124
+ expect(converted.value).toEqual({ modelRoles: { "pipeline-fix": [pin] } });
125
+ expect(SandboxSettingsSchema.parse(converted.value).modelRoles).toEqual({ "pipeline-fix": [pin] });
126
+ });
127
+
128
+ test(`leaves a file without it alone`, () => {
129
+ expect(convertDocument(SETTINGS_HISTORY, `object`, { modelRoles: { "pipeline-fix": [pin] } }, true).changes).toEqual([]);
130
+ });
131
+ });