@intentic/sandbox-contract 1.331.0 → 1.332.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 (332) hide show
  1. package/README.md +15 -0
  2. package/dist/chores/probes.d.ts.map +1 -1
  3. package/dist/chores/probes.js.map +1 -1
  4. package/dist/contracts/accounts.contract.d.ts +11 -0
  5. package/dist/contracts/accounts.contract.d.ts.map +1 -1
  6. package/dist/contracts/agent.contract.d.ts +37 -0
  7. package/dist/contracts/agent.contract.d.ts.map +1 -1
  8. package/dist/contracts/agent.contract.js +14 -3
  9. package/dist/contracts/agent.contract.js.map +1 -1
  10. package/dist/contracts/agents.contract.d.ts +29 -0
  11. package/dist/contracts/agents.contract.d.ts.map +1 -1
  12. package/dist/contracts/automations.contract.d.ts +2 -0
  13. package/dist/contracts/automations.contract.d.ts.map +1 -1
  14. package/dist/contracts/device.contract.d.ts +7 -0
  15. package/dist/contracts/device.contract.d.ts.map +1 -1
  16. package/dist/contracts/git.contract.d.ts +2 -0
  17. package/dist/contracts/git.contract.d.ts.map +1 -1
  18. package/dist/contracts/logs.contract.d.ts +10 -0
  19. package/dist/contracts/logs.contract.d.ts.map +1 -1
  20. package/dist/contracts/needs.contract.d.ts +7 -0
  21. package/dist/contracts/needs.contract.d.ts.map +1 -1
  22. package/dist/contracts/offload.contract.d.ts +70 -0
  23. package/dist/contracts/offload.contract.d.ts.map +1 -1
  24. package/dist/contracts/offload.contract.js +10 -1
  25. package/dist/contracts/offload.contract.js.map +1 -1
  26. package/dist/contracts/privacy.contract.d.ts +14 -0
  27. package/dist/contracts/privacy.contract.d.ts.map +1 -1
  28. package/dist/contracts/privacy.contract.js +11 -1
  29. package/dist/contracts/privacy.contract.js.map +1 -1
  30. package/dist/contracts/providers.contract.d.ts +3 -0
  31. package/dist/contracts/providers.contract.d.ts.map +1 -1
  32. package/dist/contracts/runner.contract.d.ts +103 -2
  33. package/dist/contracts/runner.contract.d.ts.map +1 -1
  34. package/dist/contracts/sessions.contract.d.ts +1 -0
  35. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  36. package/dist/contracts/settings.contract.d.ts +72 -94
  37. package/dist/contracts/settings.contract.d.ts.map +1 -1
  38. package/dist/contracts/settings.contract.js +19 -1
  39. package/dist/contracts/settings.contract.js.map +1 -1
  40. package/dist/contracts/system.contract.d.ts +17 -0
  41. package/dist/contracts/system.contract.d.ts.map +1 -1
  42. package/dist/contracts/translator.contract.d.ts +4 -0
  43. package/dist/contracts/translator.contract.d.ts.map +1 -1
  44. package/dist/events/agent-events.d.ts +46 -0
  45. package/dist/events/agent-events.d.ts.map +1 -1
  46. package/dist/events/agent-events.js +6 -0
  47. package/dist/events/agent-events.js.map +1 -1
  48. package/dist/events/agent-words.d.ts.map +1 -1
  49. package/dist/events/agent-words.js +1 -1
  50. package/dist/events/agent-words.js.map +1 -1
  51. package/dist/events/resume.d.ts +3 -0
  52. package/dist/events/resume.d.ts.map +1 -1
  53. package/dist/events/resume.js +17 -2
  54. package/dist/events/resume.js.map +1 -1
  55. package/dist/events/sandbox-notice.d.ts +18 -0
  56. package/dist/events/sandbox-notice.d.ts.map +1 -1
  57. package/dist/events/sandbox-notice.js +27 -1
  58. package/dist/events/sandbox-notice.js.map +1 -1
  59. package/dist/events/system-events.d.ts +2 -0
  60. package/dist/events/system-events.d.ts.map +1 -1
  61. package/dist/events/transcript.d.ts +64 -0
  62. package/dist/events/transcript.d.ts.map +1 -1
  63. package/dist/events/transcript.js +17 -1
  64. package/dist/events/transcript.js.map +1 -1
  65. package/dist/index.d.ts +313 -124
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +5 -0
  68. package/dist/index.js.map +1 -1
  69. package/dist/models/agent-catalog.d.ts.map +1 -1
  70. package/dist/models/agent-catalog.js +8 -7
  71. package/dist/models/agent-catalog.js.map +1 -1
  72. package/dist/models/agent-runtimes.d.ts +3 -1
  73. package/dist/models/agent-runtimes.d.ts.map +1 -1
  74. package/dist/models/agent-runtimes.js +10 -0
  75. package/dist/models/agent-runtimes.js.map +1 -1
  76. package/dist/models/model-order.d.ts +1 -0
  77. package/dist/models/model-order.d.ts.map +1 -1
  78. package/dist/models/model-order.js +9 -0
  79. package/dist/models/model-order.js.map +1 -1
  80. package/dist/models/model-pins.d.ts.map +1 -1
  81. package/dist/models/model-pins.js.map +1 -1
  82. package/dist/models/model-roles.d.ts +25 -18
  83. package/dist/models/model-roles.d.ts.map +1 -1
  84. package/dist/models/model-roles.js +23 -27
  85. package/dist/models/model-roles.js.map +1 -1
  86. package/dist/models/model-setups.d.ts +46 -0
  87. package/dist/models/model-setups.d.ts.map +1 -0
  88. package/dist/models/model-setups.js +86 -0
  89. package/dist/models/model-setups.js.map +1 -0
  90. package/dist/models/plan-pools.d.ts +3 -1
  91. package/dist/models/plan-pools.d.ts.map +1 -1
  92. package/dist/models/plan-pools.js +18 -6
  93. package/dist/models/plan-pools.js.map +1 -1
  94. package/dist/models/provider-specs.d.ts +20 -0
  95. package/dist/models/provider-specs.d.ts.map +1 -1
  96. package/dist/models/provider-specs.js +13 -1
  97. package/dist/models/provider-specs.js.map +1 -1
  98. package/dist/policy/command-classes.d.ts.map +1 -1
  99. package/dist/policy/command-classes.js +26 -3
  100. package/dist/policy/command-classes.js.map +1 -1
  101. package/dist/policy/credential-material.d.ts +6 -0
  102. package/dist/policy/credential-material.d.ts.map +1 -1
  103. package/dist/policy/credential-material.js +37 -15
  104. package/dist/policy/credential-material.js.map +1 -1
  105. package/dist/policy/search-globs.d.ts +3 -0
  106. package/dist/policy/search-globs.d.ts.map +1 -1
  107. package/dist/policy/search-globs.js +118 -0
  108. package/dist/policy/search-globs.js.map +1 -1
  109. package/dist/policy/secret-hosts.d.ts +1 -0
  110. package/dist/policy/secret-hosts.d.ts.map +1 -1
  111. package/dist/policy/secret-hosts.js +1 -0
  112. package/dist/policy/secret-hosts.js.map +1 -1
  113. package/dist/policy/turned-away.d.ts.map +1 -1
  114. package/dist/policy/turned-away.js +1 -0
  115. package/dist/policy/turned-away.js.map +1 -1
  116. package/dist/protocol/doors/runner-protocol.d.ts +9 -0
  117. package/dist/protocol/doors/runner-protocol.d.ts.map +1 -1
  118. package/dist/protocol/doors/runner-protocol.js +5 -0
  119. package/dist/protocol/doors/runner-protocol.js.map +1 -1
  120. package/dist/protocol/pairing-redeem.d.ts +23 -0
  121. package/dist/protocol/pairing-redeem.d.ts.map +1 -0
  122. package/dist/protocol/pairing-redeem.js +44 -0
  123. package/dist/protocol/pairing-redeem.js.map +1 -0
  124. package/dist/protocol/peer-mcp-server.d.ts.map +1 -1
  125. package/dist/protocol/peer-mcp-server.js +1 -1
  126. package/dist/protocol/peer-mcp-server.js.map +1 -1
  127. package/dist/protocol/raw/raw-json-routes.d.ts +16 -0
  128. package/dist/protocol/raw/raw-json-routes.d.ts.map +1 -1
  129. package/dist/protocol/raw/raw-json-routes.js +2 -0
  130. package/dist/protocol/raw/raw-json-routes.js.map +1 -1
  131. package/dist/protocol/raw/raw-routes.d.ts +8 -0
  132. package/dist/protocol/raw/raw-routes.d.ts.map +1 -1
  133. package/dist/protocol/raw/raw-routes.js +2 -0
  134. package/dist/protocol/raw/raw-routes.js.map +1 -1
  135. package/dist/protocol/sse.d.ts +20 -1
  136. package/dist/protocol/sse.d.ts.map +1 -1
  137. package/dist/protocol/sse.js +112 -17
  138. package/dist/protocol/sse.js.map +1 -1
  139. package/dist/schemas/agent.d.ts +32 -0
  140. package/dist/schemas/agent.d.ts.map +1 -1
  141. package/dist/schemas/agent.js +12 -1
  142. package/dist/schemas/agent.js.map +1 -1
  143. package/dist/schemas/automations.d.ts +6 -0
  144. package/dist/schemas/automations.d.ts.map +1 -1
  145. package/dist/schemas/automations.js +3 -1
  146. package/dist/schemas/automations.js.map +1 -1
  147. package/dist/schemas/devices.d.ts +17 -0
  148. package/dist/schemas/devices.d.ts.map +1 -1
  149. package/dist/schemas/devices.js +2 -1
  150. package/dist/schemas/devices.js.map +1 -1
  151. package/dist/schemas/engines.d.ts +8 -1
  152. package/dist/schemas/engines.d.ts.map +1 -1
  153. package/dist/schemas/engines.js +1 -1
  154. package/dist/schemas/engines.js.map +1 -1
  155. package/dist/schemas/git/git-scope.d.ts +20 -0
  156. package/dist/schemas/git/git-scope.d.ts.map +1 -0
  157. package/dist/schemas/git/git-scope.js +37 -0
  158. package/dist/schemas/git/git-scope.js.map +1 -0
  159. package/dist/schemas/git/git.d.ts +1 -0
  160. package/dist/schemas/git/git.d.ts.map +1 -1
  161. package/dist/schemas/git/git.js +9 -1
  162. package/dist/schemas/git/git.js.map +1 -1
  163. package/dist/schemas/hosts.d.ts +42 -0
  164. package/dist/schemas/hosts.d.ts.map +1 -1
  165. package/dist/schemas/hosts.js +3 -0
  166. package/dist/schemas/hosts.js.map +1 -1
  167. package/dist/schemas/keep-warm.d.ts +1 -0
  168. package/dist/schemas/keep-warm.d.ts.map +1 -1
  169. package/dist/schemas/keep-warm.js +1 -1
  170. package/dist/schemas/keep-warm.js.map +1 -1
  171. package/dist/schemas/logs.d.ts +21 -0
  172. package/dist/schemas/logs.d.ts.map +1 -1
  173. package/dist/schemas/logs.js +19 -0
  174. package/dist/schemas/logs.js.map +1 -1
  175. package/dist/schemas/metrics.d.ts +12 -0
  176. package/dist/schemas/metrics.d.ts.map +1 -1
  177. package/dist/schemas/metrics.js +26 -8
  178. package/dist/schemas/metrics.js.map +1 -1
  179. package/dist/schemas/needs.d.ts +19 -0
  180. package/dist/schemas/needs.d.ts.map +1 -1
  181. package/dist/schemas/needs.js +17 -3
  182. package/dist/schemas/needs.js.map +1 -1
  183. package/dist/schemas/offload.d.ts +154 -0
  184. package/dist/schemas/offload.d.ts.map +1 -1
  185. package/dist/schemas/offload.js +34 -0
  186. package/dist/schemas/offload.js.map +1 -1
  187. package/dist/schemas/privacy.d.ts +19 -0
  188. package/dist/schemas/privacy.d.ts.map +1 -1
  189. package/dist/schemas/privacy.js +17 -2
  190. package/dist/schemas/privacy.js.map +1 -1
  191. package/dist/schemas/project-bring-back.d.ts +172 -0
  192. package/dist/schemas/project-bring-back.d.ts.map +1 -0
  193. package/dist/schemas/project-bring-back.js +39 -0
  194. package/dist/schemas/project-bring-back.js.map +1 -0
  195. package/dist/schemas/providers/handoff.d.ts +77 -0
  196. package/dist/schemas/providers/handoff.d.ts.map +1 -0
  197. package/dist/schemas/providers/handoff.js +85 -0
  198. package/dist/schemas/providers/handoff.js.map +1 -0
  199. package/dist/schemas/providers/plan-limits.d.ts +27 -0
  200. package/dist/schemas/providers/plan-limits.d.ts.map +1 -1
  201. package/dist/schemas/providers/plan-limits.js +8 -1
  202. package/dist/schemas/providers/plan-limits.js.map +1 -1
  203. package/dist/schemas/providers/provider-oauth.d.ts +4 -0
  204. package/dist/schemas/providers/provider-oauth.d.ts.map +1 -1
  205. package/dist/schemas/providers/usage.d.ts +3 -1
  206. package/dist/schemas/providers/usage.d.ts.map +1 -1
  207. package/dist/schemas/providers/usage.js +7 -2
  208. package/dist/schemas/providers/usage.js.map +1 -1
  209. package/dist/schemas/run-targets.d.ts.map +1 -1
  210. package/dist/schemas/run-targets.js +5 -2
  211. package/dist/schemas/run-targets.js.map +1 -1
  212. package/dist/schemas/settings-history.d.ts +1 -1
  213. package/dist/schemas/settings-history.d.ts.map +1 -1
  214. package/dist/schemas/settings-history.js +21 -0
  215. package/dist/schemas/settings-history.js.map +1 -1
  216. package/dist/schemas/settings.d.ts +67 -100
  217. package/dist/schemas/settings.d.ts.map +1 -1
  218. package/dist/schemas/settings.js +41 -24
  219. package/dist/schemas/settings.js.map +1 -1
  220. package/dist/schemas/short-codes.d.ts +31 -0
  221. package/dist/schemas/short-codes.d.ts.map +1 -0
  222. package/dist/schemas/short-codes.js +28 -0
  223. package/dist/schemas/short-codes.js.map +1 -0
  224. package/dist/schemas/system.d.ts +130 -0
  225. package/dist/schemas/system.d.ts.map +1 -1
  226. package/dist/schemas/system.js +37 -1
  227. package/dist/schemas/system.js.map +1 -1
  228. package/dist/state/definition.d.ts +64 -40
  229. package/dist/state/definition.d.ts.map +1 -1
  230. package/dist/state/history-state.d.ts.map +1 -1
  231. package/dist/state/history-state.js +1 -0
  232. package/dist/state/history-state.js.map +1 -1
  233. package/dist/state/runtime-state.d.ts +3 -0
  234. package/dist/state/runtime-state.d.ts.map +1 -1
  235. package/dist/state/runtime-state.js +1 -0
  236. package/dist/state/runtime-state.js.map +1 -1
  237. package/dist/state/versions.d.ts +6 -0
  238. package/dist/state/versions.d.ts.map +1 -1
  239. package/dist/state/versions.js +47 -0
  240. package/dist/state/versions.js.map +1 -1
  241. package/dist/text/embed.d.ts +2 -1
  242. package/dist/text/embed.d.ts.map +1 -1
  243. package/dist/text/embed.js +10 -5
  244. package/dist/text/embed.js.map +1 -1
  245. package/dist/text/title.d.ts.map +1 -1
  246. package/dist/text/title.js +4 -7
  247. package/dist/text/title.js.map +1 -1
  248. package/dist/text/transcript-fold.d.ts +2 -1
  249. package/dist/text/transcript-fold.d.ts.map +1 -1
  250. package/dist/text/transcript-fold.js +21 -6
  251. package/dist/text/transcript-fold.js.map +1 -1
  252. package/package.json +28 -6
  253. package/src/chores/probes.ts +3 -1
  254. package/src/contracts/agent.contract.ts +24 -3
  255. package/src/contracts/offload.contract.ts +21 -1
  256. package/src/contracts/privacy.contract.ts +13 -0
  257. package/src/contracts/settings.contract.ts +23 -0
  258. package/src/events/agent-events.ts +13 -0
  259. package/src/events/agent-words.ts +2 -3
  260. package/src/events/resume.test.ts +14 -2
  261. package/src/events/resume.ts +32 -3
  262. package/src/events/sandbox-notice.ts +31 -1
  263. package/src/events/transcript.ts +27 -2
  264. package/src/index.ts +5 -0
  265. package/src/models/agent-catalog.test.ts +4 -3
  266. package/src/models/agent-catalog.ts +10 -7
  267. package/src/models/agent-runtimes.ts +30 -1
  268. package/src/models/capability-ledger.test.ts +4 -0
  269. package/src/models/model-order.ts +13 -0
  270. package/src/models/model-pins.ts +5 -4
  271. package/src/models/model-roles.test.ts +9 -31
  272. package/src/models/model-roles.ts +35 -50
  273. package/src/models/model-setups.test.ts +100 -0
  274. package/src/models/model-setups.ts +151 -0
  275. package/src/models/plan-pools.test.ts +58 -0
  276. package/src/models/plan-pools.ts +38 -7
  277. package/src/models/provider-specs.ts +20 -2
  278. package/src/policy/command-classes.ts +34 -3
  279. package/src/policy/credential-material.ts +58 -16
  280. package/src/policy/search-globs.ts +135 -0
  281. package/src/policy/secret-hosts.ts +7 -0
  282. package/src/policy/tests/command-classes.test.ts +35 -0
  283. package/src/policy/tests/credential-material.test.ts +20 -0
  284. package/src/policy/tests/search-globs.test.ts +46 -1
  285. package/src/policy/tests/secret-hosts.test.ts +17 -1
  286. package/src/policy/turned-away.ts +2 -0
  287. package/src/protocol/doors/runner-protocol.ts +14 -1
  288. package/src/protocol/pairing-redeem.test.ts +86 -0
  289. package/src/protocol/pairing-redeem.ts +79 -0
  290. package/src/protocol/peer-mcp-server.ts +1 -2
  291. package/src/protocol/raw/raw-json-routes.ts +3 -0
  292. package/src/protocol/raw/raw-routes.ts +5 -0
  293. package/src/protocol/sse.test.ts +50 -1
  294. package/src/protocol/sse.ts +172 -26
  295. package/src/schemas/agent.ts +23 -2
  296. package/src/schemas/automations.ts +6 -2
  297. package/src/schemas/daemon-health.test.ts +28 -0
  298. package/src/schemas/devices.ts +11 -2
  299. package/src/schemas/engines.ts +1 -1
  300. package/src/schemas/git/git-scope.test.ts +91 -0
  301. package/src/schemas/git/git-scope.ts +71 -0
  302. package/src/schemas/git/git.ts +15 -1
  303. package/src/schemas/hosts.ts +7 -0
  304. package/src/schemas/keep-warm.ts +2 -1
  305. package/src/schemas/logs.ts +25 -0
  306. package/src/schemas/metrics.test.ts +80 -0
  307. package/src/schemas/metrics.ts +31 -9
  308. package/src/schemas/needs.ts +29 -3
  309. package/src/schemas/offload.ts +76 -0
  310. package/src/schemas/privacy.ts +27 -3
  311. package/src/schemas/project-bring-back.test.ts +15 -0
  312. package/src/schemas/project-bring-back.ts +78 -0
  313. package/src/schemas/providers/handoff.test.ts +42 -0
  314. package/src/schemas/providers/handoff.ts +128 -0
  315. package/src/schemas/providers/plan-limits.ts +14 -1
  316. package/src/schemas/providers/usage.ts +13 -7
  317. package/src/schemas/run-targets.ts +5 -2
  318. package/src/schemas/settings-history.ts +28 -0
  319. package/src/schemas/settings.test.ts +74 -21
  320. package/src/schemas/settings.ts +74 -44
  321. package/src/schemas/short-codes.ts +48 -0
  322. package/src/schemas/system.ts +63 -1
  323. package/src/state/history-state.ts +4 -0
  324. package/src/state/runtime-state.test.ts +4 -0
  325. package/src/state/runtime-state.ts +4 -0
  326. package/src/state/versions.test.ts +30 -1
  327. package/src/state/versions.ts +68 -0
  328. package/src/text/embed.test.ts +14 -0
  329. package/src/text/embed.ts +19 -10
  330. package/src/text/title.ts +5 -9
  331. package/src/text/transcript-fold.test.ts +61 -0
  332. package/src/text/transcript-fold.ts +31 -6
@@ -180,11 +180,15 @@ export type AutomationTarget = z.infer<typeof AutomationTargetSchema>;
180
180
  export const FireOnSchema = z.enum(["pass", "change"]);
181
181
  export type FireOn = z.infer<typeof FireOnSchema>;
182
182
  // What the daemon remembers of a watch between checks (.intentic/records/automation-watch.json), and what the list
183
- // shows under it. Values are kept whole up to a bound, so the next check can say what changed.
183
+ // shows under it. Values are kept whole up to a bound, so the next check can say what changed; whether anything changed
184
+ // is decided on the digest of the whole value, so a page longer than the bound is not "changed" on every check.
184
185
  export const WatchStateSchema = z.object({
185
186
  armedAt: z.number().describe("When it was first checked, in milliseconds."),
186
187
  checkedAt: z.number().describe("When it was last checked, in milliseconds."),
187
- value: z.string().optional().describe("What the last passing check saw. Absent until one has passed."),
188
+ value: z.string().optional().describe("What the last passing check saw, cut to a bound. Absent until one has passed."),
189
+ digest: z.string().optional().describe("sha256 of the whole of what the last passing check saw, before any cut."),
190
+ // A watch edited to check something else starts over: what the old check saw says nothing about the new one.
191
+ condition: z.string().optional().describe("Which check this state belongs to: a digest of the guard or source it was recorded for."),
188
192
  changedAt: z.number().optional().describe("When what it saw last changed, in milliseconds."),
189
193
  firedAt: z.number().optional().describe("When it last set something off, in milliseconds."),
190
194
  // Why the last check did not pass, when it did not.
@@ -0,0 +1,28 @@
1
+ import { type DaemonHealth, DaemonHealthReadingSchema, DaemonHealthSchema } from "./system.js";
2
+
3
+ const answer: DaemonHealth = {
4
+ ok: true,
5
+ sandboxId: "0123456789ab",
6
+ profile: "workspace",
7
+ boot: { ready: true, startedAt: 1, steps: [] },
8
+ state: { journal: "none", engine: 84 },
9
+ announce: { state: "registered", at: 2 },
10
+ reach: { state: "reachable", at: 3 },
11
+ reachedBy: "tunnel",
12
+ build: { version: "1.2.3" },
13
+ };
14
+
15
+ test("the daemon's answer parses whole as the contract's health", () => {
16
+ expect(DaemonHealthSchema.parse(answer)).toEqual(answer);
17
+ });
18
+
19
+ test("a reader takes any daemon's answer, a field of the wrong shape reading as missing", () => {
20
+ expect(DaemonHealthReadingSchema.parse({ ok: true, ready: false, boot: "starting", state: { journal: 7 }, reachedBy: "tunnel" })).toEqual({
21
+ ok: true,
22
+ ready: false,
23
+ boot: undefined,
24
+ state: { journal: undefined },
25
+ reachedBy: "tunnel",
26
+ });
27
+ expect(DaemonHealthReadingSchema.parse(answer)).toMatchObject({ sandboxId: "0123456789ab", boot: { ready: true }, announce: { state: "registered" } });
28
+ });
@@ -101,6 +101,9 @@ export const DeviceSandboxSchema = z.object({
101
101
  // so this side adopted it for its rounds: `keptElsewhere` is left out, and these say whose it was and since when.
102
102
  adoptedFrom: z.string().optional(),
103
103
  keeperSilentSince: z.number().optional(),
104
+ // (2026-10-10) Stopped by the machine's keeper after a stretch nobody needed it (`ic sandbox sleep`), and started
105
+ // again by itself when somebody opens it. `running` is false while this is true.
106
+ asleep: z.boolean().optional(),
104
107
  });
105
108
  export type DeviceSandbox = z.infer<typeof DeviceSandboxSchema>;
106
109
  // One operation on one sandbox, streamed as lines ending in a `result` or `error` frame. `prepare` builds the pending
@@ -318,12 +321,18 @@ export const DeviceSandboxIdSchema = z
318
321
  .regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/);
319
322
  // A folder on the DEVICE for `sync-install` to keep in step. The only caller-supplied string that reaches a command
320
323
  // line, so its shape is the guard: `~`, an absolute POSIX path or a drive letter, and none of the characters that would
321
- // end the argument it sits in. `~` is expanded by the daemon, which is why `$` is not allowed to arrive here.
324
+ // end the argument it sits in. `~` is expanded by the daemon, which is why `$` is not allowed to arrive here. The line
325
+ // may be PowerShell, which reads the typographic quotes “ ” „ as double quotes and ‘ ’ ‚ ‛ as single quotes beside the
326
+ // ASCII ones, so those are refused too: the daemon quotes the folder, and this keeps a second guard where one quoter
327
+ // misses a character.
322
328
  export const DeviceLocalDirSchema = z
323
329
  .string()
324
330
  .min(1)
325
331
  .max(4096)
326
- .regex(/^(?:~|\/|[A-Za-z]:[\\/])[^"'`$;|&\n\r]*$/);
332
+ // The curly quotes as escapes: bun's transpiler rewrites a literal one in a regex to its escape and tsgo keeps it,
333
+ // so spelled literally the pattern reads differently from source and from dist, and contract.lock.json can match
334
+ // only one of them.
335
+ .regex(/^(?:~|\/|[A-Za-z]:[\\/])[^"'`$;|&\n\r\u201C\u201D\u201E\u2018\u2019\u201A\u201B]*$/);
327
336
  // A TCP port number, the one value a caller supplies that reaches a command line as a number rather than a string.
328
337
  export const PortNumberSchema = z.number().int().min(1).max(65535);
329
338
  export const DeviceCommandInputSchema = z.object({
@@ -4,7 +4,7 @@ import { z } from "zod";
4
4
  // An engine is the installed program a runtime (agent-catalog.ts) spawns or imports — the `claude` binary, `codex`
5
5
  // wrapper, `@cursor/sdk`, `opencode`, `cli-proxy-api` — not the loop itself; one engine can back several runtimes.
6
6
 
7
- export const ENGINE_IDS = ["claude", "codex", "cursor", "opencode", "translator"] as const;
7
+ export const ENGINE_IDS = ["claude", "codex", "cursor", "devin", "opencode", "translator"] as const;
8
8
  export type EngineId = (typeof ENGINE_IDS)[number];
9
9
  export const EngineIdSchema = z.enum(ENGINE_IDS);
10
10
 
@@ -0,0 +1,91 @@
1
+ import { type GitChange, UNATTRIBUTED_ORIGIN } from "./git.js";
2
+ import { changeIsFrom, DISCARDABLE_SIDES, discardOutcome, isWholeRepo, scopedPaths, STAGEABLE_SIDES, UNSTAGEABLE_SIDES } from "./git-scope.js";
3
+
4
+ const change = (path: string, extra: Partial<GitChange> = {}): GitChange => ({ path, status: "modified", ...extra });
5
+
6
+ // Shape a scan produces: small enough to read, with something on every side.
7
+ const sides = {
8
+ conflicted: [change("merge.ts", { status: "conflicted" })],
9
+ staged: [change("staged.ts"), change("moved.ts", { status: "renamed", from: "old.ts" })],
10
+ unstaged: [change("edited.ts"), change("new.ts", { status: "added" })],
11
+ };
12
+
13
+ test("a scope with no side reads every side the verb can move, and no others", () => {
14
+ // Staged is deliberately absent from stageable: re-adding it would sweep in worktree edits made after the freeze.
15
+ expect(scopedPaths(sides, STAGEABLE_SIDES, {})).toEqual(["edited.ts", "new.ts", "merge.ts"]);
16
+ expect(scopedPaths(sides, UNSTAGEABLE_SIDES, {})).toEqual(["staged.ts", "moved.ts", "old.ts"]);
17
+ expect(scopedPaths(sides, DISCARDABLE_SIDES, {})).toEqual(["merge.ts", "staged.ts", "moved.ts", "old.ts", "edited.ts", "new.ts"]);
18
+ });
19
+
20
+ test("a scope narrows the verb's sides and can never widen them", () => {
21
+ expect(scopedPaths(sides, STAGEABLE_SIDES, { side: "unstaged" })).toEqual(["edited.ts", "new.ts"]);
22
+ expect(scopedPaths(sides, STAGEABLE_SIDES, { side: "conflicted" })).toEqual(["merge.ts"]);
23
+ // Staging the one side it can't move answers empty, not everything: the honest answer to an impossible request.
24
+ expect(scopedPaths(sides, STAGEABLE_SIDES, { side: "staged" })).toEqual([]);
25
+ });
26
+
27
+ test("both legs of a rename resolve together, so no verb acts on half a move", () => {
28
+ expect(scopedPaths(sides, UNSTAGEABLE_SIDES, { side: "staged" })).toContain("old.ts");
29
+ });
30
+
31
+ test("an origin scope keeps only what that conversation landed, either leg of a rename counting", () => {
32
+ const origins = { "edited.ts": ["agent-a"], "new.ts": ["agent-b"], "old.ts": ["agent-a"] };
33
+ expect(scopedPaths(sides, STAGEABLE_SIDES, { origin: "agent-a" }, origins)).toEqual(["edited.ts"]);
34
+ // `moved.ts` attributes through the path it came from, the leg the land recorded.
35
+ expect(scopedPaths(sides, UNSTAGEABLE_SIDES, { origin: "agent-a" }, origins)).toEqual(["moved.ts", "old.ts"]);
36
+ // A side and an origin compose as an intersection, not either alone.
37
+ expect(scopedPaths(sides, DISCARDABLE_SIDES, { side: "unstaged", origin: "agent-b" }, origins)).toEqual(["new.ts"]);
38
+ // No matching attribution resolves to nothing, not a fallback to everyone's work.
39
+ expect(scopedPaths(sides, STAGEABLE_SIDES, { origin: "agent-a" })).toEqual([]);
40
+ });
41
+
42
+ // The Changes panel's "You" chip sends this; it used to resolve to nothing, so its Stage all, Discard and commit
43
+ // each moved no file at all.
44
+ test("the unattributed origin keeps exactly what no conversation landed, a rename counting only when neither leg was", () => {
45
+ const origins = { "edited.ts": ["agent-a"], "old.ts": ["agent-a"] };
46
+ expect(scopedPaths(sides, STAGEABLE_SIDES, { origin: UNATTRIBUTED_ORIGIN }, origins)).toEqual(["new.ts", "merge.ts"]);
47
+ // `moved.ts` came from a landed path, so it is the agent's work, not the owner's.
48
+ expect(scopedPaths(sides, UNSTAGEABLE_SIDES, { origin: UNATTRIBUTED_ORIGIN }, origins)).toEqual(["staged.ts"]);
49
+ // With nothing attributed, every row is the owner's.
50
+ expect(scopedPaths(sides, STAGEABLE_SIDES, { side: "unstaged", origin: UNATTRIBUTED_ORIGIN })).toEqual(["edited.ts", "new.ts"]);
51
+ });
52
+
53
+ test("a path listed on two sides at once resolves to one entry", () => {
54
+ const both = { conflicted: [], staged: [change("a.ts")], unstaged: [change("a.ts")] };
55
+ expect(scopedPaths(both, DISCARDABLE_SIDES, {})).toEqual(["a.ts"]);
56
+ });
57
+
58
+ test("a scope that narrows nothing is the whole repository", () => {
59
+ expect(isWholeRepo({})).toBe(true);
60
+ expect(isWholeRepo({ side: "unstaged" })).toBe(false);
61
+ expect(isWholeRepo({ origin: "agent-a" })).toBe(false);
62
+ });
63
+
64
+ // The panel's count, its discard question and the daemon's scope all read this one predicate, so a rename out of an
65
+ // agent's file cannot be "yours" in the question and the agent's in the action.
66
+ test("a row is an origin's when either leg was landed by it, and the owner's only when neither was", () => {
67
+ const origins = { "util.ts": ["agent-a"] };
68
+ const renamed = change("helpers.ts", { status: "renamed", from: "util.ts" });
69
+ expect(changeIsFrom(renamed, "agent-a", origins)).toBe(true);
70
+ expect(changeIsFrom(renamed, UNATTRIBUTED_ORIGIN, origins)).toBe(false);
71
+ expect(changeIsFrom(change("mine.ts"), UNATTRIBUTED_ORIGIN, origins)).toBe(true);
72
+ expect(changeIsFrom(change("mine.ts"), "agent-a", origins)).toBe(false);
73
+ });
74
+
75
+ test("a discard names what leaves the disk apart from what returns to its last commit", () => {
76
+ const changed = {
77
+ conflicted: [],
78
+ staged: [
79
+ change("helpers.ts", { status: "renamed", from: "util.ts" }),
80
+ change("fresh.ts", { status: "added" }),
81
+ change("gone.ts", { status: "deleted" }),
82
+ ],
83
+ // `gone.ts` deleted in the index and recreated on disk reads untracked, yet the last commit still holds it.
84
+ unstaged: [change("edited.ts"), change("scratch.ts", { status: "added" }), change("gone.ts", { status: "added" })],
85
+ };
86
+ const paths = scopedPaths(changed, DISCARDABLE_SIDES, {});
87
+ expect(discardOutcome(changed, paths)).toEqual({
88
+ deletes: ["helpers.ts", "fresh.ts", "scratch.ts"],
89
+ restores: ["util.ts", "gone.ts", "edited.ts"],
90
+ });
91
+ });
@@ -0,0 +1,71 @@
1
+ import { type GitChange, type GitDiffSide, type GitScope, UNATTRIBUTED_ORIGIN } from "./git.js";
2
+
3
+ // What a scope names, resolved against a repository's own status. One definition, read by the daemon when it acts on a
4
+ // scope and by the Changes panel when it asks the owner about one, so the question and the action cannot disagree.
5
+ // Sides are the verb's, not the scope's: staging reads the two not yet in the index; a scope narrows, never widens.
6
+
7
+ export interface ChangedSides {
8
+ readonly conflicted: readonly GitChange[];
9
+ readonly staged: readonly GitChange[];
10
+ readonly unstaged: readonly GitChange[];
11
+ }
12
+
13
+ // Who landed each path, newest first, as `RepoChanges.origins` ships it; a rename's old path is keyed too.
14
+ export type ChangeOrigins = Readonly<Record<string, readonly string[]>>;
15
+
16
+ // What each verb can move; `git add` on an unmerged path settles a merge, so conflicts are stageable too.
17
+ export const STAGEABLE_SIDES: readonly GitDiffSide[] = ["unstaged", "conflicted"];
18
+ export const UNSTAGEABLE_SIDES: readonly GitDiffSide[] = ["staged"];
19
+ // Discard rewrites the worktree, which every side has one of.
20
+ export const DISCARDABLE_SIDES: readonly GitDiffSide[] = ["conflicted", "staged", "unstaged"];
21
+
22
+ // The paths one row stands for on disk: a rename is two, and no verb acts on half a move.
23
+ export const legsOf = (change: Pick<GitChange, "path" | "from">): readonly string[] =>
24
+ change.from === undefined ? [change.path] : [change.path, change.from];
25
+
26
+ // Every conversation that landed either leg of a row, newest first per leg, each once.
27
+ export const originsOfChange = (change: Pick<GitChange, "path" | "from">, origins: ChangeOrigins): readonly string[] => [
28
+ ...new Set(legsOf(change).flatMap((path) => origins[path] ?? [])),
29
+ ];
30
+
31
+ // Whether a row is that origin's work: a conversation that landed either leg of it, or for the unattributed origin a row
32
+ // no conversation landed any leg of. A rename out of an agent's file is the agent's row, whoever renamed it, so the
33
+ // panel counts it, the scope moves it and the discard question names it under the same chip.
34
+ export const changeIsFrom = (change: Pick<GitChange, "path" | "from">, origin: string, origins: ChangeOrigins): boolean => {
35
+ const ids = originsOfChange(change, origins);
36
+ return origin === UNATTRIBUTED_ORIGIN ? ids.length === 0 : ids.includes(origin);
37
+ };
38
+
39
+ // Paths a scope resolves to, for a verb that moves `sides`; both legs of a rename included.
40
+ // Deduplicated: a path staged and edited again is two rows over one file, and a rename's legs can be named twice.
41
+ export const scopedPaths = (changed: ChangedSides, sides: readonly GitDiffSide[], scope: GitScope, origins: ChangeOrigins = {}): readonly string[] => {
42
+ const origin = scope.origin;
43
+ const reading = scope.side === undefined ? sides : sides.filter((side) => side === scope.side);
44
+ const rows = reading.flatMap((side) => changed[side]);
45
+ const named = origin === undefined ? rows : rows.filter((change) => changeIsFrom(change, origin, origins));
46
+ return [...new Set(named.flatMap(legsOf))];
47
+ };
48
+
49
+ // A scope that narrows nothing is the whole repository; two of the three verbs say so to git directly, unenumerated.
50
+ export const isWholeRepo = (scope: GitScope): boolean => scope.side === undefined && scope.origin === undefined;
51
+
52
+ // What discarding `paths` does to each, read off the same status: a path the last commit does not hold leaves the disk
53
+ // (an untracked file, a staged new file, a rename's new leg), every other returns to its committed state. Mirrors the
54
+ // daemon's discard, which unstages first and then cleans what reads untracked.
55
+ export const discardOutcome = (changed: ChangedSides, paths: readonly string[]): { readonly deletes: readonly string[]; readonly restores: readonly string[] } => {
56
+ const notInHead = new Set<string>();
57
+ for (const change of changed.staged) {
58
+ if (change.status === "added" || (change.status === "renamed" && change.from !== undefined)) {
59
+ notInHead.add(change.path);
60
+ }
61
+ }
62
+ // Untracked, unless the index deleted it from the last commit: that path is in HEAD and comes back.
63
+ const indexDeleted = new Set(changed.staged.filter((change) => change.status === "deleted").map((change) => change.path));
64
+ for (const change of changed.unstaged) {
65
+ if (change.status === "added" && !indexDeleted.has(change.path)) {
66
+ notInHead.add(change.path);
67
+ }
68
+ }
69
+ const unique = [...new Set(paths)];
70
+ return { deletes: unique.filter((path) => notInHead.has(path)), restores: unique.filter((path) => !notInHead.has(path)) };
71
+ };
@@ -78,9 +78,23 @@ export const DiscardSchema = RepoParamSchema.extend(GitTargetSchema.shape)
78
78
  .refine(oneTarget, ONE_TARGET);
79
79
  // Index-only moves; nothing on disk changes, so neither needs a checkpoint. An empty target means the whole repository
80
80
  // (`git add -A`, or the entire index).
81
+ // `chip` is the reversible form of an origin's move (the Changes panel's origin chips): staging records what the index
82
+ // held at every path it moves, and unstaging the same chip writes exactly that back, so a partial `git add -p` or a
83
+ // merge's clean result survives one on/off. It names its own target, so it goes alone.
84
+ const oneIndexMove = (move: GitTarget & { readonly chip?: string | undefined }): boolean =>
85
+ oneTarget(move) && (move.chip === undefined || (move.paths === undefined && move.scope === undefined));
81
86
  export const GitIndexMoveSchema = RepoParamSchema.extend(GitTargetSchema.shape)
87
+ .extend({
88
+ chip: z
89
+ .string()
90
+ .min(1)
91
+ .optional()
92
+ .describe(
93
+ `Stage one origin's unstaged files (a conversation id, or "${UNATTRIBUTED_ORIGIN}" for the files none landed) so the move can be taken back exactly: unstaging with the same value later restores what the index held at each of those paths before, partial staging included, and leaves alone any path staged differently since or a commit that already took them. Goes without paths or a scope.`,
94
+ ),
95
+ })
82
96
  .describe("What to move across the index. Nothing on disk changes either way.")
83
- .refine(oneTarget, ONE_TARGET);
97
+ .refine(oneIndexMove, { message: "name paths, a scope or a chip, not more than one" });
84
98
  // No separate "set upstream" flag: the daemon runs `push -u` exactly when the branch has none yet, which is never
85
99
  // destructive.
86
100
  export const PushSchema = RepoParamSchema.extend({
@@ -76,6 +76,13 @@ export const DeviceFactsSchema = z.object({
76
76
  // The agent's last upkeep pass in this environment (DeviceUpkeepSchema): whether a release brought this machine to
77
77
  // the current shape, read rather than assumed. Absent before the first pass, and from an agent older than the field.
78
78
  upkeep: DeviceUpkeepSchema.optional(),
79
+ // How the agent before this one ended, as one sentence (watchdog kill, crash, update, reboot), so a device that
80
+ // dropped off says why once it is back. Absent on a first start, and from an agent older than the field.
81
+ previousExit: z.string().optional(),
82
+ // Which ic answers on this machine and what steers it: an old ic.exe installed from a leftover IC_URL looked like
83
+ // a daemon bug for an afternoon. Override values are the agent's own redaction (secret-looking ones by length).
84
+ ic: z.object({ path: z.string(), version: z.string(), modified: z.string().optional() }).optional(),
85
+ icOverrides: z.array(z.string()).optional(),
79
86
  });
80
87
  export type DeviceFacts = z.infer<typeof DeviceFactsSchema>;
81
88
 
@@ -88,7 +88,8 @@ export const PromptCacheOpeningSchema = z.object({
88
88
  });
89
89
  export type PromptCacheOpening = z.infer<typeof PromptCacheOpeningSchema>;
90
90
 
91
- const tokensLabel = (tokens: number): string => (tokens >= 1_000 ? `${Math.round(tokens / 1_000)}k` : String(tokens));
91
+ /** A token count as the transcript words it: "305k", or the bare count under a thousand. */
92
+ export const tokensLabel = (tokens: number): string => (tokens >= 1_000 ? `${Math.round(tokens / 1_000)}k` : String(tokens));
92
93
 
93
94
  const spanLabel = (ms: number): string => {
94
95
  const minutes = Math.max(1, Math.round(ms / 60_000));
@@ -52,10 +52,35 @@ export const ClientDiagnosticSchema = z.object({
52
52
  .describe("Whatever else was worth keeping."),
53
53
  });
54
54
  export type ClientDiagnostic = z.infer<typeof ClientDiagnosticSchema>;
55
+ // Which page a batch came from, said once per batch rather than on every event. Without it two tabs' reports read as
56
+ // one stream, and an agent spent dozens of calls learning a page was the owner's own dev server rather than the build
57
+ // it had just changed.
58
+ export const ClientPageSchema = z.object({
59
+ pageId: z.string().min(1).max(64).describe("This page load's own id: every report from one load shares it, and a reload gets a new one."),
60
+ tabId: z.string().max(64).optional().describe("The browser tab's id, which a reload of the same tab keeps."),
61
+ userAgent: z.string().max(300).optional().describe("The browser's own description of itself."),
62
+ viewport: z.string().max(40).optional().describe("The window's size in CSS pixels and its pixel ratio, e.g. 390x844@3."),
63
+ // The join key into PostHog's session replay: absent where analytics is off (dev, self-hosted) or not loaded yet.
64
+ replay: z.string().max(100).optional().describe("The PostHog session id, whose replay shows what the person did."),
65
+ origin: z.string().max(200).optional().describe("The address the page was loaded from."),
66
+ served: z
67
+ .string()
68
+ .max(300)
69
+ .optional()
70
+ .describe("How the page was served: `dev` (a Vite dev server, with the checkout, branch and who started it), `desktop` (the installed app, with its version), `local` (a window on a folder) or `hosted`."),
71
+ commit: z
72
+ .string()
73
+ .max(40)
74
+ .optional()
75
+ .describe("The short commit the page's build was made from (a dev server's: the one it started on). Absent where the build could not read it."),
76
+ });
77
+ export type ClientPage = z.infer<typeof ClientPageSchema>;
55
78
  // Capped batch: the client already coalesces and drops its own side, and an unbounded array here is a way to fill a
56
79
  // disk with one request.
57
80
  export const ClientDiagnosticsReportSchema = z.object({
58
81
  events: z.array(ClientDiagnosticSchema).min(1).max(50).describe("What the browser has to report, oldest first."),
82
+ // Optional: a page from before it sends none.
83
+ page: ClientPageSchema.optional().describe("Which page load sent the batch."),
59
84
  });
60
85
  export type ClientDiagnosticsReport = z.infer<typeof ClientDiagnosticsReportSchema>;
61
86
  export const ClientDiagnosticsAcceptedSchema = z.object({
@@ -0,0 +1,80 @@
1
+ import { type SandboxMetrics, SandboxMetricsSchema } from "./metrics.js";
2
+
3
+ // A daemon payload from before memoryBreakdown and the other optional accounting fields existed.
4
+ const legacyMetrics = {
5
+ at: 1_000_000,
6
+ sandbox: {
7
+ cores: 2,
8
+ memoryBytes: 7 * 2 ** 30,
9
+ memoryLimitBytes: 16 * 2 ** 30,
10
+ loadAverage: [1.5, 1.25, 1],
11
+ processes: 3,
12
+ },
13
+ daemon: { rssBytes: 300 * 2 ** 20, heapUsedBytes: 120 * 2 ** 20 },
14
+ sessions: { "conv-a": { processes: 2, rssBytes: 400 * 2 ** 20 } },
15
+ roles: {
16
+ agentRuntime: { processes: 1, rssBytes: 300 * 2 ** 20 },
17
+ terminal: { processes: 1, rssBytes: 100 * 2 ** 20 },
18
+ },
19
+ } satisfies SandboxMetrics;
20
+
21
+ describe("the metrics memory accounting contract", () => {
22
+ test("an older payload parses unchanged without the optional breakdown or its categories", () => {
23
+ const parsed = SandboxMetricsSchema.parse(legacyMetrics);
24
+ expect(parsed).toEqual(legacyMetrics);
25
+ expect(Object.hasOwn(parsed.sandbox, "memoryBreakdown")).toBe(false);
26
+ });
27
+
28
+ test("a partial new breakdown preserves zero and leaves unmeasured categories absent", () => {
29
+ const payload = {
30
+ ...legacyMetrics,
31
+ sandbox: {
32
+ ...legacyMetrics.sandbox,
33
+ memoryBreakdown: { anonymousBytes: 0, inactiveFileCacheBytes: 3 * 2 ** 30 },
34
+ },
35
+ };
36
+ expect(SandboxMetricsSchema.parse(payload)).toEqual(payload);
37
+ });
38
+
39
+ test("each category is independently optional, with no zero defaults for the others", () => {
40
+ for (const memoryBreakdown of [
41
+ {},
42
+ { anonymousBytes: 0 },
43
+ { countedFileCacheBytes: 0 },
44
+ { kernelBytes: 0 },
45
+ { inactiveFileCacheBytes: 0 },
46
+ ]) {
47
+ const payload = { ...legacyMetrics, sandbox: { ...legacyMetrics.sandbox, memoryBreakdown } };
48
+ expect(SandboxMetricsSchema.parse(payload)).toEqual(payload);
49
+ }
50
+ });
51
+
52
+ test("measured resident categories remain separate from a headline that also counts full swap", () => {
53
+ const payload = {
54
+ ...legacyMetrics,
55
+ sandbox: {
56
+ ...legacyMetrics.sandbox,
57
+ memoryBytes: 16.5 * 2 ** 30,
58
+ swapBytes: 9.5 * 2 ** 30,
59
+ swapLimitBytes: 10 * 2 ** 30,
60
+ swapFull: true,
61
+ memoryBreakdown: {
62
+ anonymousBytes: 5 * 2 ** 30,
63
+ countedFileCacheBytes: 2 ** 30,
64
+ kernelBytes: 2 ** 30,
65
+ inactiveFileCacheBytes: 3 * 2 ** 30,
66
+ },
67
+ },
68
+ };
69
+ expect(SandboxMetricsSchema.parse(payload)).toEqual(payload);
70
+ });
71
+
72
+ test("present categories must be byte numbers, not numeric strings or non-finite values", () => {
73
+ for (const field of ["anonymousBytes", "countedFileCacheBytes", "kernelBytes", "inactiveFileCacheBytes"]) {
74
+ for (const invalid of ["0", null, Number.NaN, Number.POSITIVE_INFINITY]) {
75
+ const payload = { ...legacyMetrics, sandbox: { ...legacyMetrics.sandbox, memoryBreakdown: { [field]: invalid } } };
76
+ expect(SandboxMetricsSchema.safeParse(payload).success).toBe(false);
77
+ }
78
+ }
79
+ });
80
+ });
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
 
3
3
  // What the sandbox is using right now, measured only when asked (GET /system/metrics). CPU percentages are of ONE core
4
- // unless the field says otherwise; memory is resident bytes.
4
+ // unless the field says otherwise; memory is in bytes, with admission accounting and process RSS distinguished below.
5
5
 
6
6
  // The kinds of process a sandbox runs, as the daemon classifies them by command line.
7
7
  export const PROCESS_ROLES = [
@@ -30,7 +30,7 @@ export const ProcessGroupMetricsSchema = z.object({
30
30
  rssBytes: z
31
31
  .number()
32
32
  .describe(
33
- "Their resident memory added up, in bytes. Memory two processes share is counted in each, so this can exceed what they cost together.",
33
+ "Their process resident memory (RSS) added up, in bytes, including file mappings and shared pages counted once per process. This can exceed what they cost together. It excludes unmapped file cache, kernel memory and swapped-out pages.",
34
34
  ),
35
35
  });
36
36
  export type ProcessGroupMetrics = z.infer<typeof ProcessGroupMetricsSchema>;
@@ -84,24 +84,46 @@ export const SandboxUsageSchema = z.object({
84
84
  memoryBytes: z
85
85
  .number()
86
86
  .describe(
87
- "Memory counted against the limit, as the daemon admits work by it: resident memory less the file cache the kernel takes back on demand, and once swap is full (`swapFull`) what was pushed to swap as well.",
87
+ "Memory counted against the limit, as the daemon admits work by it: the cgroup's resident charge, including the daemon, kernel memory and file cache except inactive_file, plus swapped bytes only when swap is nearly full (`swapFull`). Falls back to the machine's used memory when the budget cannot measure it.",
88
88
  ),
89
89
  memoryLimitBytes: z
90
90
  .number()
91
91
  .describe(
92
92
  "The memory limit work is admitted against: where the kernel starts throttling the container (memory.high), else its hard limit, else the machine's memory.",
93
93
  ),
94
+ memoryBreakdown: z
95
+ .object({
96
+ anonymousBytes: z.number().optional().describe("Anonymous resident memory charged to the cgroup (memory.stat anon), in bytes."),
97
+ countedFileCacheBytes: z
98
+ .number()
99
+ .optional()
100
+ .describe("File cache counted in resident usage (memory.stat file less inactive_file), in bytes. Absent unless both counters are valid."),
101
+ kernelBytes: z
102
+ .number()
103
+ .optional()
104
+ .describe("Kernel memory charged to the cgroup (memory.stat kernel), in bytes. Absent on kernels without that aggregate."),
105
+ inactiveFileCacheBytes: z
106
+ .number()
107
+ .optional()
108
+ .describe("Inactive file cache (memory.stat inactive_file), in bytes, excluded from memoryBytes."),
109
+ })
110
+ .optional()
111
+ .describe(
112
+ "Approximate measured memory.stat categories, not an RSS subtraction or an exact sum of memoryBytes: admission is sampled separately and may also count swap. Missing or invalid categories are omitted, not zero. Absent without cgroup memory, on machine-memory fallback, when no category is valid, and from older daemons.",
113
+ ),
94
114
  swapBytes: z
95
115
  .number()
96
116
  .optional()
97
117
  .describe(
98
- "What was pushed out to swap, counted in `memoryBytes` only once swap is full. Absent where the sandbox cannot see its own memory.",
118
+ "What was pushed out to swap, counted in `memoryBytes` only when swap is nearly full under admission policy. Absent where the sandbox cannot see its own memory.",
99
119
  ),
100
120
  swapLimitBytes: z.number().optional().describe("What swap can hold. Absent where nothing bounds it, and from a daemon that predates it."),
101
121
  swapFull: z
102
122
  .boolean()
103
123
  .optional()
104
- .describe("Swap is nearly at `swapLimitBytes`: nothing more can be parked there, so what is swapped counts against the limit too."),
124
+ .describe(
125
+ "Swap has reached the admission policy's nearly-full threshold against `swapLimitBytes`, so swapped bytes count against the memory limit too, even if swap space remains.",
126
+ ),
105
127
  memoryRoom: MemoryRoomSchema.optional().describe("What admission reads off the same reading. Absent from a daemon that predates it."),
106
128
  diskBytes: z.number().optional().describe("Space used on the volume the workspace lives on. Absent when the volume would not say."),
107
129
  diskTotalBytes: z.number().optional().describe("That volume's size. Absent when the volume would not say."),
@@ -114,7 +136,7 @@ export const SandboxUsageSchema = z.object({
114
136
  .describe(
115
137
  "Cores the machine's load average reads against: every core the sandbox may be scheduled on, before any quota. Absent from a daemon that predates it.",
116
138
  ),
117
- processes: z.number().describe("How many processes are running in the sandbox."),
139
+ processes: z.number().describe("How many processes are running in the sandbox, including the daemon."),
118
140
  pressure: PressureMetricsSchema.optional().describe(
119
141
  "How much work waited on CPU, memory or disk lately. Absent where the kernel does not report it.",
120
142
  ),
@@ -140,12 +162,12 @@ export const SandboxMetricsSchema = z.object({
140
162
  .number()
141
163
  .optional()
142
164
  .describe("How long the CPU figures were measured over, in milliseconds. Absent on a first reading, and then so is every CPU figure."),
143
- sandbox: SandboxUsageSchema.describe("The sandbox as a whole."),
144
- daemon: DaemonUsageSchema.describe("The daemon that runs it, which none of the other figures include."),
165
+ sandbox: SandboxUsageSchema.describe("The sandbox as a whole, including the daemon."),
166
+ daemon: DaemonUsageSchema.describe("The daemon's own usage, included in the sandbox total and process count but excluded from roles and session sums."),
145
167
  sessions: z
146
168
  .record(z.string(), SessionMetricsSchema)
147
169
  .describe(
148
- "What each conversation's processes use, by conversation id: the agent's own process and everything it started. Only conversations with processes running, and only those the caller may see.",
170
+ "What each conversation's processes use, by conversation id: the agent's own process and everything it started, excluding the daemon. These group many of the same process readings as roles, not additional memory. Only conversations with processes running, and only those the caller may see.",
149
171
  ),
150
172
  roles: z
151
173
  .partialRecord(ProcessRoleSchema, ProcessGroupMetricsSchema)
@@ -57,6 +57,13 @@ export type CapabilityNeed = z.infer<typeof CapabilityNeedSchema>;
57
57
  // The alphabet a `{{secret:NAME}}` reference resolves; a pasted value is stored under exactly this name.
58
58
  export const SECRET_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
59
59
 
60
+ // A `{{secret:name}}` reference wherever it stands in text, capturing the name. Wider than SECRET_NAME, since a
61
+ // reference also names the other stores' entries (`<capability>/<field>`). Not global, so a shared instance carries no
62
+ // `lastIndex` between callers: a site that replaces or spans every one derives its own /g copy from it. The terminal
63
+ // filter (@intentic/output-cleaners, builtins only) keeps a copy, pinned to this one by the daemon's
64
+ // src/secrets/terminal-masking-parity.test.ts.
65
+ export const SECRET_REFERENCE = /\{\{secret:([A-Za-z0-9_./-]+)\}\}/;
66
+
60
67
  export const SecretNeedSchema = z.object({
61
68
  kind: z.literal("secret"),
62
69
  name: z.string().regex(SECRET_NAME).describe("The name it is stored under, and what `{{secret:NAME}}` will resolve."),
@@ -283,6 +290,16 @@ export const AgentNeedSchema = z.object({
283
290
  });
284
291
  export type AgentNeed = z.infer<typeof AgentNeedSchema>;
285
292
 
293
+ // A credential gateway card's "Allow requests like this with <name> in this conversation": the `ask` rule it answered
294
+ // stops asking that conversation, until somebody takes it back. Kept by the rule's own content, so editing the rule
295
+ // asks again.
296
+ export const BrokerRulePassSchema = z.object({
297
+ capability: z.string().describe("The capability whose rule asked, by id."),
298
+ rule: z.string().describe("The rule's own identity (its content), so an edited rule is a different one and asks again."),
299
+ said: z.string().describe('The request in a person\'s words, as the card put it: "GET api.github.com/repos/x with GitHub".'),
300
+ });
301
+ export type BrokerRulePass = z.infer<typeof BrokerRulePassSchema>;
302
+
286
303
  // What a person allowed one conversation beyond its persona and area, from a grant need's card
287
304
  // (personas/conversation-grants.ts). Honoured from that conversation's next turn on, and revocable on the Grants page.
288
305
  export const ConversationGrantSchema = z.object({
@@ -306,6 +323,11 @@ export const ConversationGrantSchema = z.object({
306
323
  .boolean()
307
324
  .default(false)
308
325
  .describe("Whether every request an allow-once could settle is allowed without asking, until somebody takes it back."),
326
+ // Answered on a credential gateway card, read live by the gateway as each request its rules would ask about arrives.
327
+ rules: z
328
+ .array(BrokerRulePassSchema)
329
+ .default([])
330
+ .describe("Credential gateway rules that no longer ask before its requests, each with the words a person allowed it in."),
309
331
  updatedAt: z.number().describe("When it last changed, in milliseconds."),
310
332
  by: z.string().optional().describe("Who last allowed something here."),
311
333
  });
@@ -323,6 +345,7 @@ export const StandingGrantsSchema = z.object({
323
345
  installs: z.boolean().default(false).describe("Whether its own dependency installs run without asking."),
324
346
  secrets: z.array(z.string()).default([]).describe("Secrets it may send past their host guard without asking."),
325
347
  everything: z.boolean().default(false).describe("Whether every request an allow-once could settle is allowed without asking."),
348
+ rules: z.array(BrokerRulePassSchema).default([]).describe("Credential gateway rules that no longer ask before its requests."),
326
349
  by: z.string().optional().describe("Who last allowed one of those."),
327
350
  updatedAt: z.number().optional().describe("When one of those last changed, in milliseconds."),
328
351
  releases: z
@@ -336,11 +359,14 @@ export type StandingGrants = z.infer<typeof StandingGrantsSchema>;
336
359
  export const GrantRevokeSchema = z.object({
337
360
  conversationId: z.string(),
338
361
  kind: z
339
- .enum(["capability", "folder", "shelf", "release", "install", "secret", "everything"])
362
+ .enum(["capability", "folder", "shelf", "release", "install", "secret", "everything", "rule"])
340
363
  .describe(
341
- "Which kind of yes: a grant past the persona or area, a credential's release, letting its installs run unasked, a secret sent past its host guard, or allowing everything.",
364
+ "Which kind of yes: a grant past the persona or area, a credential's release, letting its installs run unasked, a secret sent past its host guard, allowing everything, or a credential gateway rule that stopped asking.",
342
365
  ),
343
- what: z.string().describe("The capability id, folder, shelf, released credential or secret it named; empty for installs and everything."),
366
+ what: z
367
+ .string()
368
+ .describe("The capability id, folder, shelf, released credential or secret it named, or the rule's identity; empty for installs and everything."),
369
+ capability: z.string().optional().describe("For a rule, the capability whose rule it is; a rule's identity alone does not say."),
344
370
  });
345
371
  export type GrantRevoke = z.infer<typeof GrantRevokeSchema>;
346
372