@intentic/sandbox-contract 1.317.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 (333) 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 +55 -1
  11. package/dist/contracts/agent.contract.d.ts.map +1 -1
  12. package/dist/contracts/agents.contract.d.ts +796 -93
  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/ci.contract.d.ts +22 -4
  17. package/dist/contracts/ci.contract.d.ts.map +1 -1
  18. package/dist/contracts/device.contract.d.ts +38 -0
  19. package/dist/contracts/device.contract.d.ts.map +1 -1
  20. package/dist/contracts/device.contract.js +2 -0
  21. package/dist/contracts/device.contract.js.map +1 -1
  22. package/dist/contracts/endpoints.contract.d.ts +23 -0
  23. package/dist/contracts/endpoints.contract.d.ts.map +1 -1
  24. package/dist/contracts/endpoints.contract.js +6 -1
  25. package/dist/contracts/endpoints.contract.js.map +1 -1
  26. package/dist/contracts/extensions.contract.d.ts +12 -0
  27. package/dist/contracts/extensions.contract.d.ts.map +1 -1
  28. package/dist/contracts/needs.contract.d.ts +2 -0
  29. package/dist/contracts/needs.contract.d.ts.map +1 -1
  30. package/dist/contracts/providers.contract.d.ts +4 -0
  31. package/dist/contracts/providers.contract.d.ts.map +1 -1
  32. package/dist/contracts/runner.contract.d.ts +143 -124
  33. package/dist/contracts/runner.contract.d.ts.map +1 -1
  34. package/dist/contracts/secrets.contract.d.ts +37 -0
  35. package/dist/contracts/secrets.contract.d.ts.map +1 -1
  36. package/dist/contracts/secrets.contract.js +20 -1
  37. package/dist/contracts/secrets.contract.js.map +1 -1
  38. package/dist/contracts/sessions.contract.d.ts +4 -0
  39. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  40. package/dist/contracts/settings.contract.d.ts +31 -2
  41. package/dist/contracts/settings.contract.d.ts.map +1 -1
  42. package/dist/contracts/settings.contract.js +20 -1
  43. package/dist/contracts/settings.contract.js.map +1 -1
  44. package/dist/contracts/system.contract.d.ts +96 -435
  45. package/dist/contracts/system.contract.d.ts.map +1 -1
  46. package/dist/contracts/system.contract.js +2 -12
  47. package/dist/contracts/system.contract.js.map +1 -1
  48. package/dist/contracts/translator.contract.d.ts +7 -0
  49. package/dist/contracts/translator.contract.d.ts.map +1 -1
  50. package/dist/contracts/translator.contract.js.map +1 -1
  51. package/dist/contracts/webext.contract.d.ts +38 -0
  52. package/dist/contracts/webext.contract.d.ts.map +1 -1
  53. package/dist/contracts/webext.contract.js +3 -0
  54. package/dist/contracts/webext.contract.js.map +1 -1
  55. package/dist/contracts/workspace.contract.d.ts +1 -80
  56. package/dist/contracts/workspace.contract.d.ts.map +1 -1
  57. package/dist/contracts/workspace.contract.js +0 -27
  58. package/dist/contracts/workspace.contract.js.map +1 -1
  59. package/dist/events/agent-events.d.ts +21 -0
  60. package/dist/events/agent-events.d.ts.map +1 -1
  61. package/dist/events/agent-events.js +11 -0
  62. package/dist/events/agent-events.js.map +1 -1
  63. package/dist/events/agent-words.d.ts +1 -0
  64. package/dist/events/agent-words.d.ts.map +1 -1
  65. package/dist/events/agent-words.js +1 -1
  66. package/dist/events/agent-words.js.map +1 -1
  67. package/dist/events/land-breakage.d.ts.map +1 -1
  68. package/dist/events/land-breakage.js.map +1 -1
  69. package/dist/events/resume.d.ts +6 -1
  70. package/dist/events/resume.d.ts.map +1 -1
  71. package/dist/events/resume.js +17 -10
  72. package/dist/events/resume.js.map +1 -1
  73. package/dist/events/sandbox-notice.d.ts +183 -0
  74. package/dist/events/sandbox-notice.d.ts.map +1 -0
  75. package/dist/events/sandbox-notice.js +56 -0
  76. package/dist/events/sandbox-notice.js.map +1 -0
  77. package/dist/events/system-events.d.ts +46 -0
  78. package/dist/events/system-events.d.ts.map +1 -1
  79. package/dist/events/system-events.js +2 -0
  80. package/dist/events/system-events.js.map +1 -1
  81. package/dist/events/transcript.d.ts +24 -0
  82. package/dist/events/transcript.d.ts.map +1 -1
  83. package/dist/events/transcript.js +3 -1
  84. package/dist/events/transcript.js.map +1 -1
  85. package/dist/ids/conversation-ids.d.ts +1 -3
  86. package/dist/ids/conversation-ids.d.ts.map +1 -1
  87. package/dist/ids/conversation-ids.js +1 -3
  88. package/dist/ids/conversation-ids.js.map +1 -1
  89. package/dist/ids/project-dir.d.ts +6 -0
  90. package/dist/ids/project-dir.d.ts.map +1 -0
  91. package/dist/ids/project-dir.js +43 -0
  92. package/dist/ids/project-dir.js.map +1 -0
  93. package/dist/index.d.ts +11570 -11030
  94. package/dist/index.d.ts.map +1 -1
  95. package/dist/index.js +7 -4
  96. package/dist/index.js.map +1 -1
  97. package/dist/models/agent-runtimes.d.ts.map +1 -1
  98. package/dist/models/agent-runtimes.js +2 -2
  99. package/dist/models/agent-runtimes.js.map +1 -1
  100. package/dist/models/model-roles.d.ts +1 -9
  101. package/dist/models/model-roles.d.ts.map +1 -1
  102. package/dist/models/model-roles.js +1 -9
  103. package/dist/models/model-roles.js.map +1 -1
  104. package/dist/policy/command-classes.d.ts.map +1 -1
  105. package/dist/policy/command-classes.js +52 -3
  106. package/dist/policy/command-classes.js.map +1 -1
  107. package/dist/policy/safety-policy.d.ts +7 -1
  108. package/dist/policy/safety-policy.d.ts.map +1 -1
  109. package/dist/policy/safety-policy.js +2 -0
  110. package/dist/policy/safety-policy.js.map +1 -1
  111. package/dist/policy/secret-hosts.d.ts +13 -0
  112. package/dist/policy/secret-hosts.d.ts.map +1 -0
  113. package/dist/policy/secret-hosts.js +41 -0
  114. package/dist/policy/secret-hosts.js.map +1 -0
  115. package/dist/policy/turned-away.d.ts.map +1 -1
  116. package/dist/policy/turned-away.js +1 -0
  117. package/dist/policy/turned-away.js.map +1 -1
  118. package/dist/protocol/raw-routes.d.ts +7 -0
  119. package/dist/protocol/raw-routes.d.ts.map +1 -1
  120. package/dist/protocol/raw-routes.js +4 -0
  121. package/dist/protocol/raw-routes.js.map +1 -1
  122. package/dist/protocol/routes.d.ts +2 -6
  123. package/dist/protocol/routes.d.ts.map +1 -1
  124. package/dist/protocol/routes.js +2 -3
  125. package/dist/protocol/routes.js.map +1 -1
  126. package/dist/protocol/stream-of.d.ts +6 -0
  127. package/dist/protocol/stream-of.d.ts.map +1 -0
  128. package/dist/protocol/stream-of.js +4 -0
  129. package/dist/protocol/stream-of.js.map +1 -0
  130. package/dist/schemas/activity.js +1 -1
  131. package/dist/schemas/activity.js.map +1 -1
  132. package/dist/schemas/agent.d.ts +19 -2
  133. package/dist/schemas/agent.d.ts.map +1 -1
  134. package/dist/schemas/agent.js +2 -1
  135. package/dist/schemas/agent.js.map +1 -1
  136. package/dist/schemas/agents.d.ts +61 -0
  137. package/dist/schemas/agents.d.ts.map +1 -1
  138. package/dist/schemas/agents.js +36 -0
  139. package/dist/schemas/agents.js.map +1 -1
  140. package/dist/schemas/automations.d.ts +18 -0
  141. package/dist/schemas/automations.d.ts.map +1 -1
  142. package/dist/schemas/capabilities.d.ts +2 -0
  143. package/dist/schemas/capabilities.d.ts.map +1 -1
  144. package/dist/schemas/capabilities.js +34 -4
  145. package/dist/schemas/capabilities.js.map +1 -1
  146. package/dist/schemas/ci.d.ts +69 -9
  147. package/dist/schemas/ci.d.ts.map +1 -1
  148. package/dist/schemas/ci.js +29 -8
  149. package/dist/schemas/ci.js.map +1 -1
  150. package/dist/schemas/context-trim.d.ts +5 -1
  151. package/dist/schemas/context-trim.d.ts.map +1 -1
  152. package/dist/schemas/context-trim.js +8 -4
  153. package/dist/schemas/context-trim.js.map +1 -1
  154. package/dist/schemas/devices.d.ts +23 -0
  155. package/dist/schemas/devices.d.ts.map +1 -1
  156. package/dist/schemas/devices.js +16 -1
  157. package/dist/schemas/devices.js.map +1 -1
  158. package/dist/schemas/environment.d.ts +48 -0
  159. package/dist/schemas/environment.d.ts.map +1 -1
  160. package/dist/schemas/environment.js +15 -0
  161. package/dist/schemas/environment.js.map +1 -1
  162. package/dist/schemas/extension-updates.d.ts +24 -0
  163. package/dist/schemas/extension-updates.d.ts.map +1 -1
  164. package/dist/schemas/git/git.d.ts +13 -0
  165. package/dist/schemas/git/git.d.ts.map +1 -1
  166. package/dist/schemas/git/git.js +11 -1
  167. package/dist/schemas/git/git.js.map +1 -1
  168. package/dist/schemas/hosts.d.ts +2 -0
  169. package/dist/schemas/hosts.d.ts.map +1 -1
  170. package/dist/schemas/hosts.js +2 -1
  171. package/dist/schemas/hosts.js.map +1 -1
  172. package/dist/schemas/keep-warm.d.ts +4 -0
  173. package/dist/schemas/keep-warm.d.ts.map +1 -1
  174. package/dist/schemas/keep-warm.js +15 -5
  175. package/dist/schemas/keep-warm.js.map +1 -1
  176. package/dist/schemas/loopback-catch.d.ts +33 -0
  177. package/dist/schemas/loopback-catch.d.ts.map +1 -0
  178. package/dist/schemas/loopback-catch.js +20 -0
  179. package/dist/schemas/loopback-catch.js.map +1 -0
  180. package/dist/schemas/maintenance.js +1 -1
  181. package/dist/schemas/maintenance.js.map +1 -1
  182. package/dist/schemas/needs.d.ts +3 -0
  183. package/dist/schemas/needs.d.ts.map +1 -1
  184. package/dist/schemas/needs.js +10 -3
  185. package/dist/schemas/needs.js.map +1 -1
  186. package/dist/schemas/personas.d.ts.map +1 -1
  187. package/dist/schemas/personas.js +6 -3
  188. package/dist/schemas/personas.js.map +1 -1
  189. package/dist/schemas/providers/provider-oauth.d.ts +87 -0
  190. package/dist/schemas/providers/provider-oauth.d.ts.map +1 -1
  191. package/dist/schemas/providers/provider-oauth.js +20 -0
  192. package/dist/schemas/providers/provider-oauth.js.map +1 -1
  193. package/dist/schemas/secrets.d.ts +66 -0
  194. package/dist/schemas/secrets.d.ts.map +1 -1
  195. package/dist/schemas/secrets.js +50 -0
  196. package/dist/schemas/secrets.js.map +1 -1
  197. package/dist/schemas/settings-history.d.ts +1 -1
  198. package/dist/schemas/settings-history.d.ts.map +1 -1
  199. package/dist/schemas/settings-history.js +1 -0
  200. package/dist/schemas/settings-history.js.map +1 -1
  201. package/dist/schemas/settings.d.ts +42 -2
  202. package/dist/schemas/settings.d.ts.map +1 -1
  203. package/dist/schemas/settings.js +26 -4
  204. package/dist/schemas/settings.js.map +1 -1
  205. package/dist/schemas/terminal.d.ts +2 -3
  206. package/dist/schemas/terminal.d.ts.map +1 -1
  207. package/dist/schemas/terminal.js +12 -4
  208. package/dist/schemas/terminal.js.map +1 -1
  209. package/dist/schemas/webext.d.ts +3 -0
  210. package/dist/schemas/webext.d.ts.map +1 -1
  211. package/dist/schemas/webext.js +1 -0
  212. package/dist/schemas/webext.js.map +1 -1
  213. package/dist/schemas/workflows.js +3 -3
  214. package/dist/schemas/workflows.js.map +1 -1
  215. package/dist/schemas/workspace/workspace-tree.d.ts +2 -0
  216. package/dist/schemas/workspace/workspace-tree.d.ts.map +1 -1
  217. package/dist/schemas/workspace/workspace-tree.js +4 -0
  218. package/dist/schemas/workspace/workspace-tree.js.map +1 -1
  219. package/dist/state/definition.d.ts +12 -4
  220. package/dist/state/definition.d.ts.map +1 -1
  221. package/dist/state/history-state.d.ts.map +1 -1
  222. package/dist/state/history-state.js +7 -0
  223. package/dist/state/history-state.js.map +1 -1
  224. package/dist/state/runtime-state.d.ts +0 -3
  225. package/dist/state/runtime-state.d.ts.map +1 -1
  226. package/dist/state/runtime-state.js +0 -1
  227. package/dist/state/runtime-state.js.map +1 -1
  228. package/dist/state/workspace-state.d.ts +0 -5
  229. package/dist/state/workspace-state.d.ts.map +1 -1
  230. package/dist/state/workspace-state.js +0 -6
  231. package/dist/state/workspace-state.js.map +1 -1
  232. package/dist/text/documents.d.ts +3 -0
  233. package/dist/text/documents.d.ts.map +1 -1
  234. package/dist/text/documents.js +14 -0
  235. package/dist/text/documents.js.map +1 -1
  236. package/dist/text/transcript-fold.d.ts +4 -3
  237. package/dist/text/transcript-fold.d.ts.map +1 -1
  238. package/dist/text/transcript-fold.js +137 -31
  239. package/dist/text/transcript-fold.js.map +1 -1
  240. package/dist/webext/index.d.ts +1 -0
  241. package/dist/webext/index.d.ts.map +1 -1
  242. package/dist/webext/index.js +1 -0
  243. package/dist/webext/index.js.map +1 -1
  244. package/package.json +5 -5
  245. package/src/chores/chores.ts +2 -2
  246. package/src/chores/probes.ts +1 -1
  247. package/src/contracts/accounts.contract.ts +12 -0
  248. package/src/contracts/agents.contract.ts +13 -13
  249. package/src/contracts/device.contract.ts +5 -0
  250. package/src/contracts/endpoints.contract.ts +20 -4
  251. package/src/contracts/secrets.contract.ts +28 -0
  252. package/src/contracts/settings.contract.ts +29 -0
  253. package/src/contracts/system.contract.ts +5 -14
  254. package/src/contracts/translator.contract.ts +2 -1
  255. package/src/contracts/webext.contract.ts +7 -1
  256. package/src/contracts/workspace.contract.ts +0 -37
  257. package/src/events/agent-events.ts +18 -1
  258. package/src/events/agent-words.ts +4 -1
  259. package/src/events/land-breakage.ts +3 -4
  260. package/src/events/resume.test.ts +13 -2
  261. package/src/events/resume.ts +25 -12
  262. package/src/events/sandbox-notice.ts +92 -0
  263. package/src/events/system-events.ts +7 -0
  264. package/src/events/transcript.ts +7 -1
  265. package/src/events/verify-nudge.ts +3 -3
  266. package/src/ids/conversation-ids.test.ts +2 -26
  267. package/src/ids/conversation-ids.ts +6 -12
  268. package/src/ids/project-dir.fixture.json +65 -0
  269. package/src/ids/project-dir.test.ts +60 -0
  270. package/src/ids/project-dir.ts +70 -0
  271. package/src/index.ts +12 -4
  272. package/src/models/agent-catalog.test.ts +7 -1
  273. package/src/models/agent-runtimes.ts +7 -5
  274. package/src/models/model-roles.ts +1 -9
  275. package/src/policy/command-classes.test.ts +39 -0
  276. package/src/policy/command-classes.ts +70 -3
  277. package/src/policy/safety-policy.ts +10 -0
  278. package/src/policy/secret-hosts.test.ts +89 -0
  279. package/src/policy/secret-hosts.ts +73 -0
  280. package/src/policy/turned-away.ts +1 -0
  281. package/src/protocol/raw-routes.ts +5 -1
  282. package/src/protocol/routes.test.ts +6 -6
  283. package/src/protocol/routes.ts +2 -9
  284. package/src/protocol/stream-of.ts +13 -0
  285. package/src/schemas/activity.ts +1 -1
  286. package/src/schemas/agent.ts +4 -1
  287. package/src/schemas/agents.ts +58 -4
  288. package/src/schemas/automations.ts +2 -2
  289. package/src/schemas/capabilities.ts +44 -7
  290. package/src/schemas/ci.ts +56 -11
  291. package/src/schemas/context-trim.ts +11 -6
  292. package/src/schemas/devices.ts +38 -2
  293. package/src/schemas/environment.ts +39 -0
  294. package/src/schemas/git/git.ts +20 -2
  295. package/src/schemas/hosts.ts +4 -1
  296. package/src/schemas/keep-warm.ts +19 -6
  297. package/src/schemas/loopback-catch.ts +45 -0
  298. package/src/schemas/maintenance.ts +1 -1
  299. package/src/schemas/needs.ts +12 -3
  300. package/src/schemas/personas.ts +8 -3
  301. package/src/schemas/providers/provider-oauth.ts +35 -0
  302. package/src/schemas/secrets.ts +70 -0
  303. package/src/schemas/settings-history.ts +2 -0
  304. package/src/schemas/settings.test.ts +19 -1
  305. package/src/schemas/settings.ts +58 -9
  306. package/src/schemas/speaker.ts +5 -5
  307. package/src/schemas/terminal.ts +19 -6
  308. package/src/schemas/webext.ts +3 -0
  309. package/src/schemas/workflows.ts +5 -5
  310. package/src/schemas/workspace/workspace-tree.ts +7 -0
  311. package/src/state/fix-attempt-plan.test.ts +1 -1
  312. package/src/state/fix-stance.ts +1 -1
  313. package/src/state/history-state.ts +14 -0
  314. package/src/state/runtime-state.ts +0 -3
  315. package/src/state/workspace-state.ts +0 -6
  316. package/src/text/documents.test.ts +25 -1
  317. package/src/text/documents.ts +24 -0
  318. package/src/text/title.ts +1 -1
  319. package/src/text/transcript-fold.test.ts +218 -25
  320. package/src/text/transcript-fold.ts +166 -45
  321. package/src/webext/index.ts +1 -0
  322. package/dist/schemas/workspace/push-checks.d.ts +0 -222
  323. package/dist/schemas/workspace/push-checks.d.ts.map +0 -1
  324. package/dist/schemas/workspace/push-checks.js +0 -97
  325. package/dist/schemas/workspace/push-checks.js.map +0 -1
  326. package/dist/state/red-streak.d.ts +0 -7
  327. package/dist/state/red-streak.d.ts.map +0 -1
  328. package/dist/state/red-streak.js +0 -11
  329. package/dist/state/red-streak.js.map +0 -1
  330. package/src/schemas/workspace/push-checks.test.ts +0 -28
  331. package/src/schemas/workspace/push-checks.ts +0 -155
  332. package/src/state/red-streak.test.ts +0 -28
  333. package/src/state/red-streak.ts +0 -28
@@ -1,5 +1,6 @@
1
1
  // context-trim: what a window too small for a full turn made the sandbox leave out
2
2
  import { z } from "zod";
3
+ import type { SandboxNotice } from "../events/sandbox-notice.js";
3
4
 
4
5
  // The only disclosure of a note that never rode; without it a turn runs thinner and nothing on screen says why.
5
6
  export const ContextTrimSchema = z.object({
@@ -18,12 +19,16 @@ export type ContextTrim = z.infer<typeof ContextTrimSchema>;
18
19
  // 16k, 32k; a server's own odd number spelled out in full.
19
20
  const windowLabel = (tokens: number): string => (tokens % 1024 === 0 ? `${tokens / 1024}k` : tokens.toLocaleString("en-US"));
20
21
 
22
+ /** The trim's notice by code and facts, for a reader that words it in its own language. */
23
+ export const contextTrimNotice = ({ window, omitted, base }: ContextTrim): Extract<SandboxNotice, { code: "contextTrim" }> => ({
24
+ code: "contextTrim",
25
+ params: { window: windowLabel(window), ...(omitted.length === 0 ? {} : { omitted: omitted.join(`, `) }), base },
26
+ });
27
+
21
28
  // The closing clause is load-bearing: without it the row reads as "my AGENTS.md may not have arrived".
22
- export const contextTrimLine = ({ window, omitted, base }: ContextTrim): string => {
29
+ export const contextTrimLine = (trim: ContextTrim): string => {
30
+ const { window, omitted, base } = contextTrimNotice(trim).params;
23
31
  const swapped = base ? ` The agent's own base instructions were swapped for a short paragraph, too.` : ``;
24
- const left = omitted.length === 0 ? `` : ` — left out: ${omitted.join(`, `)}`;
25
- return (
26
- `Sent thin for this model's ${windowLabel(window)} window${left}.${swapped}` +
27
- ` Your workspace rules, this turn's persona and where its files live still rode.`
28
- );
32
+ const left = omitted === undefined ? `` : ` — left out: ${omitted}`;
33
+ return `Sent thin for this model's ${window} window${left}.${swapped}` + ` Your workspace rules, this turn's persona and where its files live still rode.`;
29
34
  };
@@ -178,8 +178,10 @@ export const icShapeArgs = (slug: string, fields: SandboxShapeFields, when: Sand
178
178
  export const icForgetShapeArgs = (slug: string): string[] => ["sandbox", "shape", slug, "--forget"];
179
179
  // Power goes through ic too, so a Start or Restart from any door applies the shape saved for the next restart.
180
180
  export const icPowerArgs = (op: "start" | "stop" | "restart", slug: string): string[] => ["sandbox", op, slug];
181
- // Adds the device id to the flow input; the daemon looks up machines by id.
182
- export const DeviceSandboxFlowInputSchema = DeviceSandboxFlowSchema.extend({ id: z.string().min(1) });
181
+ // Adds the device id to the flow input; the daemon looks up machines by id. `resumeTurns` is the owner saying, as they
182
+ // restart the sandbox relaying this, that the turns it cuts should run again once it is back; the daemon keeps it for
183
+ // its next boot and never forwards it to the machine. A sandbox older than it ignores it, and the cut turns wait.
184
+ export const DeviceSandboxFlowInputSchema = DeviceSandboxFlowSchema.extend({ id: z.string().min(1), resumeTurns: z.boolean().optional() });
183
185
  export type DeviceSandboxFlowInput = z.infer<typeof DeviceSandboxFlowInputSchema>;
184
186
  // Streamed operation output, matching `IntenticLineSchema`: `line` frames as printed, then one terminal frame, `result`
185
187
  // or `error`, carrying the machine's own message rather than a code.
@@ -374,6 +376,9 @@ export const DevicePairingSchema = z.object({
374
376
  mode: z.enum(["sync", "mirror"]),
375
377
  // Set only for mode "sync", and only for the sandbox being reported to.
376
378
  localDir: z.string().optional(),
379
+ // Which sandbox folder `localDir` holds when it is not /work itself: a project pairing's `/work/<name>`, the
380
+ // owner's own folder, which carries no state backup and no git bridge. Absent means /work.
381
+ remoteDir: z.string().optional(),
377
382
  // Machine-owned switch (agent's own `sync mirror off`); absent reads as "on", not merely unknown.
378
383
  mirroring: z.enum(["on", "off"]).optional(),
379
384
  // Mutagen's own status word, kept verbatim rather than reduced to a traffic light.
@@ -512,6 +517,37 @@ export const DeviceSyncSchema = z.object({
512
517
  environment: z.string().optional(),
513
518
  });
514
519
  export type DeviceSync = z.infer<typeof DeviceSyncSchema>;
520
+
521
+ // A public host key as a machine pins it in known_hosts: its type and base64 body, no comment and nothing after.
522
+ export const SshHostKeySchema = z
523
+ .string()
524
+ .regex(/^(?:ssh-ed25519|ssh-rsa|ecdsa-sha2-nistp(?:256|384|521)|sk-ssh-ed25519@openssh\.com|sk-ecdsa-sha2-nistp256@openssh\.com) [A-Za-z0-9+/]+={0,2}$/);
525
+
526
+ // POST /system/authorized-key: a machine's agent enrolling its SSH key for desktop sync, redeeming a pairing token.
527
+ // Every field past `key` is optional: an older agent sends none of them and is still enrolled.
528
+ export const SyncEnrollmentRequestSchema = z.object({
529
+ // One authorized_keys line; its comment is the name the enrollment goes by (`DeviceSync.machine`).
530
+ key: z.string(),
531
+ // The computer and the environment on it that are enrolling, so the enrollment joins that machine's card.
532
+ machineId: MachineIdSchema.optional(),
533
+ environment: z.string().min(1).optional(),
534
+ });
535
+ export type SyncEnrollmentRequest = z.infer<typeof SyncEnrollmentRequestSchema>;
536
+
537
+ // The daemon's answer to an enrollment. Every field is optional for the agent reading it, since each arrived in a later
538
+ // daemon than the route: without `syncToken` the agent refuses the enrollment, without `mode` it reads "sync", and
539
+ // without `hostKey` it falls back to recording whatever key the sandbox presents first.
540
+ export const SyncEnrollmentAnswerSchema = z.object({
541
+ ok: z.literal(true).optional(),
542
+ // Authorizes the ports poll, the machine report and the SSH transport, sent as `x-intentic-sync`.
543
+ syncToken: z.string().optional(),
544
+ // What the pairing granted: a member's pairing only ever enrolls "mirror".
545
+ mode: z.enum(["sync", "mirror"]).optional(),
546
+ // The public key the sandbox's sshd presents (an SshHostKeySchema line), for the agent to pin under the pairing's
547
+ // alias. Checked by the agent on its own, so a key it cannot read costs the pin, never the enrollment.
548
+ hostKey: z.string().optional(),
549
+ });
550
+ export type SyncEnrollmentAnswer = z.infer<typeof SyncEnrollmentAnswerSchema>;
515
551
  export const DeviceSchema = z.object({
516
552
  // Stable row key: the reported hostname when either door produced one, else the name that door knows it by.
517
553
  key: z.string(),
@@ -40,10 +40,17 @@ export const RuntimeInstallSchema = z.object({
40
40
  declinedAt: z.number().optional(),
41
41
  });
42
42
  export type RuntimeInstall = z.infer<typeof RuntimeInstallSchema>;
43
+ // A draft the owner already answered (approved, removed or declined), by its tool and the hash of its steps: the same
44
+ // file coming back with an agent's land, which still carries it on its branch, proposes nothing. An agent asking again
45
+ // (`environment propose`) clears its tool's entries.
46
+ export const SettledDraftSchema = z.object({ tool: z.string(), hash: z.string(), at: z.number() });
47
+ export type SettledDraft = z.infer<typeof SettledDraftSchema>;
43
48
  export const RuntimeInstallsFileSchema = z.object({
44
49
  installs: z.array(RuntimeInstallSchema),
45
50
  // The last drift snapshot, persisted so a daemon restart does not blank the card until the next sweep.
46
51
  drift: EnvironmentDriftSchema.optional(),
52
+ // Drafts already answered, newest last and capped; absent on a ledger written before removal existed.
53
+ settled: z.array(SettledDraftSchema).optional(),
47
54
  });
48
55
  export type RuntimeInstallsFile = z.infer<typeof RuntimeInstallsFileSchema>;
49
56
  // Ledger entry as the Environment card shows it: recurrence joined with whether it's present in the live container
@@ -74,6 +81,26 @@ export type EnvironmentRuntimeDecision = z.infer<typeof EnvironmentRuntimeDecisi
74
81
  // there (SANDBOX_DEV_ROOT), absent on a sandbox that was handed a local image without one.
75
82
  export const EnvironmentLocalImageSchema = z.object({ base: z.string(), root: z.string().optional() });
76
83
  export type EnvironmentLocalImage = z.infer<typeof EnvironmentLocalImageSchema>;
84
+ // A rebuild the owner asked to start by itself once no agent is mid-turn, kept by the sandbox rather than the page that
85
+ // asked, so a page closed or a phone locked meanwhile does not lose it. Held in the sandbox's memory: a restart of the
86
+ // sandbox for any other reason drops it, and the card offers the rebuild again.
87
+ export const EnvironmentRebuildWaitSchema = z.object({
88
+ // The approved overlay it builds, and the connected device that runs it.
89
+ hash: z.string(),
90
+ host: z.string(),
91
+ requestedAt: z.number(),
92
+ // waiting: until no agent is mid-turn. rebuilding: handed to the device, which restarts this sandbox at the end.
93
+ // failed: the device refused or could not be reached; said until the owner dismisses it or asks again.
94
+ phase: z.enum(["waiting", "rebuilding", "failed"]),
95
+ // Who it waits on, by the title the board shows them under; empty once it is no longer waiting.
96
+ waitingOn: z.array(z.string()),
97
+ // The device's own words for a failed one.
98
+ message: z.string().optional(),
99
+ });
100
+ export type EnvironmentRebuildWait = z.infer<typeof EnvironmentRebuildWaitSchema>;
101
+ // Asks for that rebuild: the approved overlay's hash and the device that rebuilds this sandbox.
102
+ export const EnvironmentRebuildWhenIdleSchema = z.object({ host: z.string().min(1), hash: z.string().min(1) });
103
+ export type EnvironmentRebuildWhenIdle = z.infer<typeof EnvironmentRebuildWhenIdleSchema>;
77
104
  export const EnvironmentSchema = z.object({
78
105
  proposal: environmentFileSchema.optional(),
79
106
  // The owner-approved agent-written custom section (.intentic/config/environment.custom.Dockerfile).
@@ -89,6 +116,12 @@ export const EnvironmentSchema = z.object({
89
116
  recurring: z.array(EnvironmentRecurringSchema).optional(),
90
117
  // Absent on a sandbox running a published image, which is every sandbox but a dogfooding one.
91
118
  localImage: EnvironmentLocalImageSchema.optional(),
119
+ // A rebuild waiting for the agents to be idle, or started or refused once they were; absent when none is asked.
120
+ rebuildWhenIdle: EnvironmentRebuildWaitSchema.optional(),
121
+ // This sandbox can hold a rebuild until no agent is mid-turn (POST /environment/rebuild-when-idle), and resume the
122
+ // turns a restart the owner starts cuts when asked (a device swap's `resumeTurns`). Absent from one too old to,
123
+ // where the rebuild dialog offers only "Rebuild now" and the cut turns wait to be continued, as before.
124
+ waitsForAgents: z.literal(true).optional(),
92
125
  });
93
126
  export type Environment = z.infer<typeof EnvironmentSchema>;
94
127
  export const EnvironmentApproveSchema = z.object({ hash: z.string().min(1) });
@@ -127,8 +160,14 @@ export const EnvironmentItemSchema = z.object({
127
160
  detail: z.string().optional(),
128
161
  // The block's own instruction lines, for the reader who wants to see exactly what runs.
129
162
  commands: z.string().optional(),
163
+ // The custom-section block this row is, which the owner may take out (POST /environment/remove names it). Absent
164
+ // on capability and base rows, and from a sandbox that cannot remove one, so no Remove is offered there.
165
+ block: z.string().optional(),
130
166
  });
131
167
  export type EnvironmentItem = z.infer<typeof EnvironmentItemSchema>;
168
+ // Takes one tool out of the environment: its approved block, its pending draft, or both.
169
+ export const EnvironmentRemoveSchema = z.object({ block: z.string() });
170
+ export type EnvironmentRemove = z.infer<typeof EnvironmentRemoveSchema>;
132
171
  export const EnvironmentContentsSchema = z.object({ items: z.array(EnvironmentItemSchema) });
133
172
  export type EnvironmentContents = z.infer<typeof EnvironmentContentsSchema>;
134
173
  // A sandbox is four stores: /work, /history, the container, and the AI-provider credential root. A bundle carries only
@@ -86,7 +86,7 @@ export const PushSchema = RepoParamSchema.extend({
86
86
  });
87
87
  // Who said no to a push, read off git's own words (`pushRefusal`); the three answers ask three different things of the
88
88
  // owner.
89
- // hook: this repo's own pre-push hook refused it; the code is known wrong, worth a fix.
89
+ // hook: this repo's own pre-push hook refused it; what it printed is about the code, and the terminal holds all of it.
90
90
  // remote: the server rejected the refs (non-fast-forward, protected branch); pull first or push elsewhere.
91
91
  // transport: it never got there (credentials, unreachable host); a retry is the only useful button.
92
92
  export const PushRefusalSchema = z.enum(["hook", "remote", "transport"]);
@@ -100,7 +100,7 @@ export const PushRunSchema = CommandRunSchema.extend({
100
100
  .optional()
101
101
  .describe("Why not, in git's own words: the last verdict line, for a row that has room for one line. The whole tail is `output`."),
102
102
  refusedBy: PushRefusalSchema.optional().describe(
103
- "Who refused a failed push: this repository's pre-push hook (the code is wrong, a fix is worth proposing), the remote (pull first), or the transport (credentials, network: retry). Absent while it runs and for a push that went.",
103
+ "Who refused a failed push: this repository's own pre-push hook (what it printed is about the code), the remote (pull first), or the transport (credentials, network: retry). Absent while it runs and for a push that went.",
104
104
  ),
105
105
  });
106
106
  export type PushRun = z.infer<typeof PushRunSchema>;
@@ -389,6 +389,17 @@ export const AgentChangeSchema = GitChangeSchema.extend({
389
389
  ),
390
390
  });
391
391
  export type AgentChange = z.infer<typeof AgentChangeSchema>;
392
+ // One manifest's new dependency names: the file at the delta's anchor against the file as it stands now, so a review
393
+ // can say what the project takes on before it lands.
394
+ export const AddedDependenciesSchema = z.object({
395
+ path: z.string().describe("The manifest, relative to the repository root, such as video/package.json."),
396
+ added: z
397
+ .array(z.string())
398
+ .describe(
399
+ "The names it declares now and did not declare before, sorted. Only new names: a version bump of a dependency already there is not listed.",
400
+ ),
401
+ });
402
+ export type AddedDependencies = z.infer<typeof AddedDependenciesSchema>;
392
403
  // An agent worktree's delta vs its base, deliberately not RepoChanges: no index here, so sharing that shape would force
393
404
  // an empty `staged` and a staging affordance that cannot work on a worktree nobody checks out.
394
405
  export const AgentRepoChangesSchema = z.object({
@@ -401,6 +412,13 @@ export const AgentRepoChangesSchema = z.object({
401
412
  .describe(
402
413
  "The packages of the tree these changes came from, so a review can group by package. Carried with the changes rather than looked up separately, because a package the conversation has just created exists only in its own copy and the shared tree has never heard of it.",
403
414
  ),
415
+ // Absent rather than empty when nothing was added, the common case.
416
+ addedDependencies: z
417
+ .array(AddedDependenciesSchema)
418
+ .optional()
419
+ .describe(
420
+ "Dependencies the changed manifests here declare that they did not before (package.json, pyproject.toml, requirements.txt), one entry per manifest that gained any. The review is where a new dependency is approved: a conversation installs freely in its own copy, and this is what the project takes on if the work lands. Absent when none was added.",
421
+ ),
404
422
  });
405
423
  export type AgentRepoChanges = z.infer<typeof AgentRepoChangesSchema>;
406
424
  // The review, plus why the last land refused: a conflict is found when the turn ends but resolved later on this
@@ -70,11 +70,14 @@ export type DeviceFacts = z.infer<typeof DeviceFactsSchema>;
70
70
  // Both features ride that verb, so an agent advertises both or neither.
71
71
  // - `rollback-to`: the `rollback` op's `to`, going back to an older version kept on the machine rather than the
72
72
  // previous one; advertised only when the agent's `ic sandbox rollback` takes `--to`.
73
- export const DeviceFeatureSchema = z.enum(["reshape-later", "set-shape", "rollback-to"]);
73
+ // - `loopback-catch`: the `catchLoopback` procedure, a sign-in's loopback redirect caught on this machine
74
+ // (loopback-catch.ts); an agent without it rejects the call as an unknown procedure.
75
+ export const DeviceFeatureSchema = z.enum(["reshape-later", "set-shape", "rollback-to", "loopback-catch"]);
74
76
  export type DeviceFeature = z.infer<typeof DeviceFeatureSchema>;
75
77
  export const DEVICE_FEATURE_RESHAPE_LATER: DeviceFeature = "reshape-later";
76
78
  export const DEVICE_FEATURE_SET_SHAPE: DeviceFeature = "set-shape";
77
79
  export const DEVICE_FEATURE_ROLLBACK_TO: DeviceFeature = "rollback-to";
80
+ export const DEVICE_FEATURE_LOOPBACK_CATCH: DeviceFeature = "loopback-catch";
78
81
  // The features an agent advertised that this build knows; an unknown one is a newer agent's and means nothing here.
79
82
  export const deviceFeatures = (facts: Pick<DeviceFacts, "features"> | undefined): DeviceFeature[] =>
80
83
  (facts?.features ?? []).flatMap((feature) => {
@@ -2,6 +2,7 @@
2
2
  // clock lives here; how many refreshes a hold may spend is priced by the provider that serves it (runtimes/claude).
3
3
 
4
4
  import { z } from "zod";
5
+ import type { SandboxNotice } from "../events/sandbox-notice.js";
5
6
 
6
7
  // A cache clock as the daemon publishes it: the last request that touched the entry, and the entry's lifetime.
7
8
  export interface CacheClock {
@@ -95,16 +96,28 @@ const spanLabel = (ms: number): string => {
95
96
  return hours === 0 ? `${minutes}m` : `${hours}h ${minutes % 60}m`;
96
97
  };
97
98
 
98
- /** The transcript's line for a turn that picked up a conversation kept warm for it; undefined for any other turn. */
99
- export const keptWarmLine = (opening: PromptCacheOpening): string | undefined => {
99
+ /** The notice for a turn that picked up a conversation kept warm for it, by code and facts; undefined for any other turn. */
100
+ export const keptWarmNotice = (opening: PromptCacheOpening): Extract<SandboxNotice, { code: "keptWarm" | "keptCold" }> | undefined => {
100
101
  const { kept } = opening;
101
102
  if (kept === undefined) {
102
103
  return undefined;
103
104
  }
104
- const held = `kept warm for ${spanLabel(kept.forMs)} with ${kept.refreshes} ${kept.refreshes === 1 ? "refresh" : "refreshes"}`;
105
- return opening.readTokens >= opening.writtenTokens
106
- ? `Picked up warm: ${tokensLabel(opening.readTokens)} tokens read from the cache, ${held}.`
107
- : `Picked up cold despite being ${held}: ${tokensLabel(opening.writtenTokens)} tokens sent again, because this turn's prompt no longer matched the cache.`;
105
+ const warm = opening.readTokens >= opening.writtenTokens;
106
+ const params = { tokens: tokensLabel(warm ? opening.readTokens : opening.writtenTokens), span: spanLabel(kept.forMs), refreshes: kept.refreshes };
107
+ return warm ? { code: "keptWarm", params } : { code: "keptCold", params };
108
+ };
109
+
110
+ /** The transcript's line for a turn that picked up a conversation kept warm for it; undefined for any other turn. */
111
+ export const keptWarmLine = (opening: PromptCacheOpening): string | undefined => {
112
+ const notice = keptWarmNotice(opening);
113
+ if (notice === undefined) {
114
+ return undefined;
115
+ }
116
+ const { tokens, span, refreshes } = notice.params;
117
+ const held = `kept warm for ${span} with ${refreshes} ${refreshes === 1 ? "refresh" : "refreshes"}`;
118
+ return notice.code === "keptWarm"
119
+ ? `Picked up warm: ${tokens} tokens read from the cache, ${held}.`
120
+ : `Picked up cold despite being ${held}: ${tokens} tokens sent again, because this turn's prompt no longer matched the cache.`;
108
121
  };
109
122
 
110
123
  // A turn's prefix, part by part, so two turns that should share a cache can say which part stopped matching.
@@ -0,0 +1,45 @@
1
+ // Loopback catch: how a sign-in whose redirect lands on http://localhost:<port>/… finishes without a paste. The daemon
2
+ // is not on the machine the browser is on, so something that is (a device agent, the browser extension) watches that
3
+ // one address for the attempt's lifetime and hands back where the browser landed. Both peers answer the same
4
+ // procedure (`catchLoopback`) with the same frames, so the daemon drives them identically.
5
+ // Named imports rather than the `z` namespace: the extension bundles this module (see webext.ts).
6
+ import { discriminatedUnion, enum as zEnum, literal, number, object, string } from "zod";
7
+ import type * as z from "zod";
8
+
9
+ // What one catcher is asked to watch. The state is deliberately absent: the daemon checks it on what comes back, so a
10
+ // catcher can hold nothing that tells a real landing from a forged one, and needs nothing to.
11
+ export const LoopbackCatchSchema = object({
12
+ // The attempt's own id, for logs on both ends; not redeemable.
13
+ id: string().min(1),
14
+ // The host the provider redirects to, spelled as its registration spells it: some are registered as
15
+ // `localhost`, some as `127.0.0.1`, and a browser treats the two as different origins.
16
+ host: zEnum(["localhost", "127.0.0.1"]),
17
+ port: number().int().min(1).max(65535),
18
+ // The path only, leading slash included; the query is the grant and is never matched.
19
+ path: string().startsWith("/"),
20
+ // Epoch ms; a catcher stops watching by itself here even if nobody aborts the stream.
21
+ expiresAt: number(),
22
+ // Who the page thanks once the browser lands ("Claude"), so the tab says what just happened.
23
+ title: string().min(1),
24
+ }).strict();
25
+ export type LoopbackCatch = z.infer<typeof LoopbackCatchSchema>;
26
+
27
+ // What a catcher streams back. `busy` ends its part (the port is taken on that machine, or it cannot watch at all);
28
+ // `landed` may come more than once, since anything on the machine can hit a loopback port and only the daemon can
29
+ // tell which hit is the sign-in.
30
+ export const LoopbackCatchEventSchema = discriminatedUnion("type", [
31
+ object({ type: literal("listening") }),
32
+ object({ type: literal("busy"), reason: string() }),
33
+ object({ type: literal("landed"), url: string().min(1) }),
34
+ ]);
35
+ export type LoopbackCatchEvent = z.infer<typeof LoopbackCatchEventSchema>;
36
+
37
+ // Advertised in a device's `features` and a browser's facts; only a peer that says it is asked.
38
+ export const LOOPBACK_CATCH_FEATURE = "loopback-catch";
39
+
40
+ // Who is watching for this attempt's landing, for the card to say "finishes by itself on rog".
41
+ export const SignInCatcherSchema = object({
42
+ kind: zEnum(["device", "browser"]),
43
+ label: string().min(1),
44
+ });
45
+ export type SignInCatcher = z.infer<typeof SignInCatcherSchema>;
@@ -137,7 +137,7 @@ export const MutationScoreSchema = z.object({
137
137
  .number()
138
138
  .describe("The share of injected faults the suite caught. Not a coverage figure: coverage says a line ran, this says an assertion depended on it."),
139
139
  killed: z.number().int().nonnegative().describe("Faults the suite caught."),
140
- survived: z.number().int().nonnegative().describe("Faults it did not: code that can be broken with every test still green."),
140
+ survived: z.number().int().nonnegative().describe("Faults it did not: code that can be broken with every test still passing."),
141
141
  // A timeout counts as detected here (Stryker's own convention), not as inconclusive.
142
142
  inconclusive: z
143
143
  .number()
@@ -256,6 +256,12 @@ export const ConversationGrantSchema = z.object({
256
256
  capabilities: z.array(z.string()).default([]).describe("Connected capabilities it may use although its persona leaves them out."),
257
257
  folders: z.array(z.string()).default([]).describe("Workspace folders its file tools may touch beyond its fence."),
258
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."),
259
265
  updatedAt: z.number().describe("When it last changed, in milliseconds."),
260
266
  by: z.string().optional().describe("Who last allowed something here."),
261
267
  });
@@ -270,11 +276,12 @@ export const StandingGrantsSchema = z.object({
270
276
  capabilities: z.array(z.string()).describe("Connected capabilities allowed although its persona leaves them out."),
271
277
  folders: z.array(z.string()).describe("Workspace folders its file tools may touch beyond its fence."),
272
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."),
273
280
  by: z.string().optional().describe("Who last allowed one of those."),
274
281
  updatedAt: z.number().optional().describe("When one of those last changed, in milliseconds."),
275
282
  releases: z
276
283
  .array(z.object({ subject: z.string(), approvedBy: z.string(), at: z.number() }))
277
- .describe("Gated credentials released to it, until the daemon restarts or somebody takes one back."),
284
+ .describe("Gated credentials released to it, until somebody takes one back."),
278
285
  }),
279
286
  ),
280
287
  });
@@ -282,8 +289,10 @@ export type StandingGrants = z.infer<typeof StandingGrantsSchema>;
282
289
 
283
290
  export const GrantRevokeSchema = z.object({
284
291
  conversationId: z.string(),
285
- kind: z.enum(["capability", "folder", "shelf", "release"]).describe("Which kind of yes: a grant past the persona or area, or a credential's release."),
286
- what: z.string().describe("The capability id, folder, shelf or released credential it named."),
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."),
287
296
  });
288
297
  export type GrantRevoke = z.infer<typeof GrantRevokeSchema>;
289
298
 
@@ -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:
@@ -104,6 +105,66 @@ export const CredentialGrantSchema = z.object({
104
105
  message: z.string().describe("What the grant means in practice, and what to do next."),
105
106
  });
106
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
+
107
168
  // Across every store: env/generated secrets, capability credentials, AI-provider accounts. Values never ride this
108
169
  // shape; `revealable` says whether `reveal` can return one (everything but provider accounts).
109
170
  export const SecretInventoryEntrySchema = z.object({
@@ -151,6 +212,15 @@ export const SecretInventoryEntrySchema = z.object({
151
212
  })
152
213
  .optional()
153
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."),
154
224
  });
155
225
  export type SecretInventoryEntry = z.infer<typeof SecretInventoryEntrySchema>;
156
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
+ });