@intentic/sandbox-contract 1.330.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 (337) 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 +15 -0
  5. package/dist/contracts/accounts.contract.d.ts.map +1 -1
  6. package/dist/contracts/agent.contract.d.ts +38 -0
  7. package/dist/contracts/agent.contract.d.ts.map +1 -1
  8. package/dist/contracts/agent.contract.js +14 -3
  9. package/dist/contracts/agent.contract.js.map +1 -1
  10. package/dist/contracts/agents.contract.d.ts +29 -0
  11. package/dist/contracts/agents.contract.d.ts.map +1 -1
  12. package/dist/contracts/automations.contract.d.ts +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 +106 -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 +74 -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 +20 -0
  41. package/dist/contracts/system.contract.d.ts.map +1 -1
  42. package/dist/contracts/translator.contract.d.ts +8 -0
  43. package/dist/contracts/translator.contract.d.ts.map +1 -1
  44. package/dist/events/agent-events.d.ts +47 -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 +4 -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 +327 -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 +32 -17
  83. package/dist/models/model-roles.d.ts.map +1 -1
  84. package/dist/models/model-roles.js +30 -26
  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 +5 -1
  91. package/dist/models/plan-pools.d.ts.map +1 -1
  92. package/dist/models/plan-pools.js +41 -13
  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 +35 -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/environment.d.ts +30 -0
  156. package/dist/schemas/environment.d.ts.map +1 -1
  157. package/dist/schemas/environment.js +13 -0
  158. package/dist/schemas/environment.js.map +1 -1
  159. package/dist/schemas/git/git-scope.d.ts +20 -0
  160. package/dist/schemas/git/git-scope.d.ts.map +1 -0
  161. package/dist/schemas/git/git-scope.js +37 -0
  162. package/dist/schemas/git/git-scope.js.map +1 -0
  163. package/dist/schemas/git/git.d.ts +1 -0
  164. package/dist/schemas/git/git.d.ts.map +1 -1
  165. package/dist/schemas/git/git.js +9 -1
  166. package/dist/schemas/git/git.js.map +1 -1
  167. package/dist/schemas/hosts.d.ts +42 -0
  168. package/dist/schemas/hosts.d.ts.map +1 -1
  169. package/dist/schemas/hosts.js +3 -0
  170. package/dist/schemas/hosts.js.map +1 -1
  171. package/dist/schemas/keep-warm.d.ts +1 -0
  172. package/dist/schemas/keep-warm.d.ts.map +1 -1
  173. package/dist/schemas/keep-warm.js +1 -1
  174. package/dist/schemas/keep-warm.js.map +1 -1
  175. package/dist/schemas/logs.d.ts +21 -0
  176. package/dist/schemas/logs.d.ts.map +1 -1
  177. package/dist/schemas/logs.js +19 -0
  178. package/dist/schemas/logs.js.map +1 -1
  179. package/dist/schemas/metrics.d.ts +12 -0
  180. package/dist/schemas/metrics.d.ts.map +1 -1
  181. package/dist/schemas/metrics.js +26 -8
  182. package/dist/schemas/metrics.js.map +1 -1
  183. package/dist/schemas/needs.d.ts +19 -0
  184. package/dist/schemas/needs.d.ts.map +1 -1
  185. package/dist/schemas/needs.js +17 -3
  186. package/dist/schemas/needs.js.map +1 -1
  187. package/dist/schemas/offload.d.ts +154 -0
  188. package/dist/schemas/offload.d.ts.map +1 -1
  189. package/dist/schemas/offload.js +34 -0
  190. package/dist/schemas/offload.js.map +1 -1
  191. package/dist/schemas/privacy.d.ts +19 -0
  192. package/dist/schemas/privacy.d.ts.map +1 -1
  193. package/dist/schemas/privacy.js +17 -2
  194. package/dist/schemas/privacy.js.map +1 -1
  195. package/dist/schemas/project-bring-back.d.ts +172 -0
  196. package/dist/schemas/project-bring-back.d.ts.map +1 -0
  197. package/dist/schemas/project-bring-back.js +39 -0
  198. package/dist/schemas/project-bring-back.js.map +1 -0
  199. package/dist/schemas/providers/handoff.d.ts +77 -0
  200. package/dist/schemas/providers/handoff.d.ts.map +1 -0
  201. package/dist/schemas/providers/handoff.js +85 -0
  202. package/dist/schemas/providers/handoff.js.map +1 -0
  203. package/dist/schemas/providers/plan-limits.d.ts +34 -0
  204. package/dist/schemas/providers/plan-limits.d.ts.map +1 -1
  205. package/dist/schemas/providers/plan-limits.js +13 -2
  206. package/dist/schemas/providers/plan-limits.js.map +1 -1
  207. package/dist/schemas/providers/provider-oauth.d.ts +8 -0
  208. package/dist/schemas/providers/provider-oauth.d.ts.map +1 -1
  209. package/dist/schemas/providers/usage.d.ts +3 -1
  210. package/dist/schemas/providers/usage.d.ts.map +1 -1
  211. package/dist/schemas/providers/usage.js +7 -2
  212. package/dist/schemas/providers/usage.js.map +1 -1
  213. package/dist/schemas/run-targets.d.ts.map +1 -1
  214. package/dist/schemas/run-targets.js +5 -2
  215. package/dist/schemas/run-targets.js.map +1 -1
  216. package/dist/schemas/settings-history.d.ts +6 -1
  217. package/dist/schemas/settings-history.d.ts.map +1 -1
  218. package/dist/schemas/settings-history.js +25 -1
  219. package/dist/schemas/settings-history.js.map +1 -1
  220. package/dist/schemas/settings.d.ts +69 -100
  221. package/dist/schemas/settings.d.ts.map +1 -1
  222. package/dist/schemas/settings.js +41 -24
  223. package/dist/schemas/settings.js.map +1 -1
  224. package/dist/schemas/short-codes.d.ts +31 -0
  225. package/dist/schemas/short-codes.d.ts.map +1 -0
  226. package/dist/schemas/short-codes.js +28 -0
  227. package/dist/schemas/short-codes.js.map +1 -0
  228. package/dist/schemas/system.d.ts +130 -0
  229. package/dist/schemas/system.d.ts.map +1 -1
  230. package/dist/schemas/system.js +37 -1
  231. package/dist/schemas/system.js.map +1 -1
  232. package/dist/state/definition.d.ts +64 -40
  233. package/dist/state/definition.d.ts.map +1 -1
  234. package/dist/state/history-state.d.ts.map +1 -1
  235. package/dist/state/history-state.js +1 -0
  236. package/dist/state/history-state.js.map +1 -1
  237. package/dist/state/runtime-state.d.ts +3 -0
  238. package/dist/state/runtime-state.d.ts.map +1 -1
  239. package/dist/state/runtime-state.js +1 -0
  240. package/dist/state/runtime-state.js.map +1 -1
  241. package/dist/state/versions.d.ts +6 -0
  242. package/dist/state/versions.d.ts.map +1 -1
  243. package/dist/state/versions.js +47 -0
  244. package/dist/state/versions.js.map +1 -1
  245. package/dist/text/embed.d.ts +2 -1
  246. package/dist/text/embed.d.ts.map +1 -1
  247. package/dist/text/embed.js +10 -5
  248. package/dist/text/embed.js.map +1 -1
  249. package/dist/text/title.d.ts.map +1 -1
  250. package/dist/text/title.js +4 -7
  251. package/dist/text/title.js.map +1 -1
  252. package/dist/text/transcript-fold.d.ts +2 -1
  253. package/dist/text/transcript-fold.d.ts.map +1 -1
  254. package/dist/text/transcript-fold.js +21 -6
  255. package/dist/text/transcript-fold.js.map +1 -1
  256. package/package.json +28 -6
  257. package/src/chores/probes.ts +3 -1
  258. package/src/contracts/agent.contract.ts +24 -3
  259. package/src/contracts/offload.contract.ts +21 -1
  260. package/src/contracts/privacy.contract.ts +13 -0
  261. package/src/contracts/settings.contract.ts +23 -0
  262. package/src/events/agent-events.ts +13 -0
  263. package/src/events/agent-words.ts +2 -3
  264. package/src/events/resume.test.ts +14 -2
  265. package/src/events/resume.ts +32 -3
  266. package/src/events/sandbox-notice.ts +31 -1
  267. package/src/events/transcript.ts +27 -2
  268. package/src/index.ts +5 -0
  269. package/src/models/agent-catalog.test.ts +4 -3
  270. package/src/models/agent-catalog.ts +10 -7
  271. package/src/models/agent-runtimes.ts +30 -1
  272. package/src/models/capability-ledger.test.ts +4 -0
  273. package/src/models/model-order.ts +13 -0
  274. package/src/models/model-pins.ts +5 -4
  275. package/src/models/model-roles.test.ts +9 -31
  276. package/src/models/model-roles.ts +42 -49
  277. package/src/models/model-setups.test.ts +100 -0
  278. package/src/models/model-setups.ts +151 -0
  279. package/src/models/plan-pools.test.ts +85 -0
  280. package/src/models/plan-pools.ts +76 -17
  281. package/src/models/provider-specs.ts +20 -2
  282. package/src/policy/command-classes.ts +34 -3
  283. package/src/policy/credential-material.ts +58 -16
  284. package/src/policy/search-globs.ts +135 -0
  285. package/src/policy/secret-hosts.ts +7 -0
  286. package/src/policy/tests/command-classes.test.ts +35 -0
  287. package/src/policy/tests/credential-material.test.ts +20 -0
  288. package/src/policy/tests/search-globs.test.ts +46 -1
  289. package/src/policy/tests/secret-hosts.test.ts +17 -1
  290. package/src/policy/turned-away.ts +2 -0
  291. package/src/protocol/doors/runner-protocol.ts +14 -1
  292. package/src/protocol/pairing-redeem.test.ts +86 -0
  293. package/src/protocol/pairing-redeem.ts +79 -0
  294. package/src/protocol/peer-mcp-server.ts +1 -2
  295. package/src/protocol/raw/raw-json-routes.ts +3 -0
  296. package/src/protocol/raw/raw-routes.ts +5 -0
  297. package/src/protocol/sse.test.ts +50 -1
  298. package/src/protocol/sse.ts +172 -26
  299. package/src/schemas/agent.ts +23 -2
  300. package/src/schemas/automations.ts +6 -2
  301. package/src/schemas/daemon-health.test.ts +28 -0
  302. package/src/schemas/devices.ts +11 -2
  303. package/src/schemas/engines.ts +1 -1
  304. package/src/schemas/environment.ts +25 -0
  305. package/src/schemas/git/git-scope.test.ts +91 -0
  306. package/src/schemas/git/git-scope.ts +71 -0
  307. package/src/schemas/git/git.ts +15 -1
  308. package/src/schemas/hosts.ts +7 -0
  309. package/src/schemas/keep-warm.ts +2 -1
  310. package/src/schemas/logs.ts +25 -0
  311. package/src/schemas/metrics.test.ts +80 -0
  312. package/src/schemas/metrics.ts +31 -9
  313. package/src/schemas/needs.ts +29 -3
  314. package/src/schemas/offload.ts +76 -0
  315. package/src/schemas/privacy.ts +27 -3
  316. package/src/schemas/project-bring-back.test.ts +15 -0
  317. package/src/schemas/project-bring-back.ts +78 -0
  318. package/src/schemas/providers/handoff.test.ts +42 -0
  319. package/src/schemas/providers/handoff.ts +128 -0
  320. package/src/schemas/providers/plan-limits.ts +23 -2
  321. package/src/schemas/providers/usage.ts +13 -7
  322. package/src/schemas/run-targets.ts +5 -2
  323. package/src/schemas/settings-history.ts +43 -1
  324. package/src/schemas/settings.test.ts +84 -6
  325. package/src/schemas/settings.ts +74 -44
  326. package/src/schemas/short-codes.ts +48 -0
  327. package/src/schemas/system.ts +63 -1
  328. package/src/state/history-state.ts +4 -0
  329. package/src/state/runtime-state.test.ts +4 -0
  330. package/src/state/runtime-state.ts +4 -0
  331. package/src/state/versions.test.ts +30 -1
  332. package/src/state/versions.ts +68 -0
  333. package/src/text/embed.test.ts +14 -0
  334. package/src/text/embed.ts +19 -10
  335. package/src/text/title.ts +5 -9
  336. package/src/text/transcript-fold.test.ts +61 -0
  337. package/src/text/transcript-fold.ts +31 -6
@@ -1,3 +1,5 @@
1
+ import { escapeRegExp } from "@intentic/base/escape";
2
+
1
3
  // Whether a file's text actually holds a credential, the fact command-classes.ts's path table can only guess at from a
2
4
  // name. Pure over the text (no fs, no config), so it stays testable and works where there's no filesystem. Biased
3
5
  // toward yes: a false positive costs one card, a false negative un-gates a real credential read.
@@ -25,24 +27,62 @@ const STRUCTURAL_SHAPES = [
25
27
  /\b[a-z][a-z0-9+.-]*:\/\/[^\s/:@]+:(?!\*+@)[^\s/@]{3,}@/i,
26
28
  ];
27
29
 
28
- const ISSUED_TOKENS = [
29
- /\bnpm_[A-Za-z0-9]{30,}/,
30
- /\bgh[pousr]_[A-Za-z0-9]{20,}/,
31
- /\bgithub_pat_[A-Za-z0-9_]{50,}/,
32
- /\bglpat-[A-Za-z0-9_-]{16,}/,
33
- /\bxox[baprs]-[A-Za-z0-9-]{10,}/,
34
- /\bsk-[A-Za-z0-9_-]{20,}/,
35
- /\b(?:sk|rk)_live_[A-Za-z0-9]{16,}/,
36
- /\bAKIA[0-9A-Z]{16}\b/,
37
- /\bASIA[0-9A-Z]{16}\b/,
38
- /\bAIza[0-9A-Za-z_-]{35}\b/,
39
- /\bhf_[A-Za-z0-9]{30,}/,
40
- /\bdop_v1_[a-f0-9]{60,}/,
41
- // A JWT: three base64url segments, the first two of which decode from {" and so always begin "ey".
42
- /\bey[A-Za-z0-9_-]{10,}\.ey[A-Za-z0-9_-]{10,}\./,
30
+ // An issuer's literal prefix and the regex source of the body it mints after it.
31
+ export interface CredentialIssuer {
32
+ readonly prefix: string;
33
+ readonly body: string;
34
+ }
35
+
36
+ // Credentials that name their own issuer. Kept as data, not regexes, so the terminal filter's copy
37
+ // (CREDENTIAL_ISSUERS in _sandbox/output-cleaners/src/cleaners.mjs, builtins only, so it cannot import this) can be
38
+ // compared entry by entry: the daemon's src/secrets/terminal-masking-parity.test.ts fails the moment the two part. Only
39
+ // the whole shape counts here, never the prefix alone: the outbox reads text nobody said holds a credential, where a
40
+ // bare `sk-` in prose is not one. The terminal side also takes the prefix alone beside a credential-shaped name, where
41
+ // the name already said so.
42
+ export const CREDENTIAL_ISSUERS: readonly CredentialIssuer[] = [
43
+ { prefix: "npm_", body: "[A-Za-z0-9]{30,}" },
44
+ { prefix: "ghp_", body: "[A-Za-z0-9]{20,}" },
45
+ { prefix: "gho_", body: "[A-Za-z0-9]{20,}" },
46
+ { prefix: "ghu_", body: "[A-Za-z0-9]{20,}" },
47
+ { prefix: "ghs_", body: "[A-Za-z0-9]{20,}" },
48
+ { prefix: "ghr_", body: "[A-Za-z0-9]{20,}" },
49
+ { prefix: "github_pat_", body: "[A-Za-z0-9_]{50,}" },
50
+ { prefix: "glpat-", body: "[A-Za-z0-9_-]{16,}" },
51
+ { prefix: "xoxa-", body: "[A-Za-z0-9-]{10,}" },
52
+ { prefix: "xoxb-", body: "[A-Za-z0-9-]{10,}" },
53
+ { prefix: "xoxo-", body: "[A-Za-z0-9-]{10,}" },
54
+ { prefix: "xoxp-", body: "[A-Za-z0-9-]{10,}" },
55
+ { prefix: "xoxr-", body: "[A-Za-z0-9-]{10,}" },
56
+ { prefix: "xoxs-", body: "[A-Za-z0-9-]{10,}" },
57
+ { prefix: "sk-", body: "[A-Za-z0-9_-]{20,}" },
58
+ { prefix: "pk-", body: "[A-Za-z0-9_-]{20,}" },
59
+ { prefix: "rk-", body: "[A-Za-z0-9_-]{20,}" },
60
+ { prefix: "sk_live_", body: "[A-Za-z0-9]{16,}" },
61
+ { prefix: "rk_live_", body: "[A-Za-z0-9]{16,}" },
62
+ { prefix: "AKIA", body: String.raw`[0-9A-Z]{16}\b` },
63
+ { prefix: "ASIA", body: String.raw`[0-9A-Z]{16}\b` },
64
+ { prefix: "AIza", body: String.raw`[0-9A-Za-z_-]{35}\b` },
65
+ { prefix: "ya29.", body: "[0-9A-Za-z_-]{20,}" },
66
+ { prefix: "hf_", body: "[A-Za-z0-9]{30,}" },
67
+ { prefix: "dop_v1_", body: "[a-f0-9]{60,}" },
68
+ { prefix: "shpat_", body: "[a-fA-F0-9]{32,}" },
69
+ // SendGrid: a key id and its secret, dot-separated.
70
+ { prefix: "SG.", body: String.raw`[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}` },
71
+ // A JWT: three base64url segments, the first two of which decode from `{"` and so always begin `eyJ` and `ey`.
72
+ { prefix: "eyJ", body: String.raw`[A-Za-z0-9_-]{9,}\.ey[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]*` },
43
73
  ];
74
+ export const CREDENTIAL_ISSUER_PREFIXES: readonly string[] = CREDENTIAL_ISSUERS.map(({ prefix }) => prefix);
75
+
76
+ // Starts where no word or kebab-case word runs into it: `task-sk-runner-config-file` is a name, not a key. The other
77
+ // side builds its patterns the same way.
78
+ const ISSUED_TOKENS = CREDENTIAL_ISSUERS.map(({ prefix, body }) => new RegExp(String.raw`(?<![\w-])${escapeRegExp(prefix)}${body}`));
79
+
80
+ // An opaque bearer credential, which names no issuer: the scheme word, then an RFC 6750 token holding a letter and a
81
+ // digit and longer than prose puts after "Bearer" (`Bearer <token>`, `Bearer YOUR_TOKEN`, an already-masked `***`).
82
+ const BEARER_VALUE = String.raw`(?=[\w.~+/-]*\d)(?=[\w.~+/-]*[A-Za-z])[\w.~+/-]{16,}=*`;
83
+ const BEARER_TOKEN = new RegExp(String.raw`\bBearer[ \t]+${BEARER_VALUE}`, "i");
44
84
 
45
- const TOKEN_SHAPES = [...STRUCTURAL_SHAPES, ...ISSUED_TOKENS];
85
+ const TOKEN_SHAPES = [...STRUCTURAL_SHAPES, BEARER_TOKEN, ...ISSUED_TOKENS];
46
86
 
47
87
  // Deferred references, stripped first: unstripped, each reads as a credential twice over.
48
88
  const DEFERRED_VALUE = /\{\{[^}\n]*\}\}|\$\{[^}\n]*\}/g;
@@ -91,6 +131,7 @@ const PRIVATE_KEY_BLOCK = new RegExp(
91
131
  );
92
132
  const PUTTY_KEY = new RegExp(String.raw`(PuTTY-User-Key-File-\d:)${MASKED_NEXT}[^\n\\]*${ARMOR_LINES}`, "g");
93
133
  const URL_PASSWORD = /(\b[a-z][a-z0-9+.-]*:\/\/[^\s/:@]+:)(?!\*+@)[^\s/@]{3,}@/gi;
134
+ const BEARER_TOKEN_G = new RegExp(String.raw`(\bBearer[ \t]+)${BEARER_VALUE}`, "gi");
94
135
 
95
136
  // The truncated opener left once the bare-value arm stops at its first `}`, anchored at the start.
96
137
  const DEFERRED_OPENER = /^(?:\{\{|\$)/;
@@ -106,6 +147,7 @@ export const maskCredentialMaterial = (text: string): string =>
106
147
  .replace(PUTTY_KEY, `$1 ${MASK}`),
107
148
  )
108
149
  .replace(URL_PASSWORD, `$1${MASK}@`)
150
+ .replace(BEARER_TOKEN_G, `$1${MASK}`)
109
151
  // Masks just the value, leaving key/separator/quoting readable; skips mirror the detector's own.
110
152
  .replace(CREDENTIAL_ASSIGNMENT_G, (match, ...groups: unknown[]) => {
111
153
  const value = (groups[0] ?? groups[1] ?? groups[2] ?? "") as string;
@@ -58,3 +58,138 @@ export const includeGlobs = (include: string | undefined): IncludeGlobs => {
58
58
  notGlobs: segments.filter((segment) => segment.startsWith(`!`)).flatMap((segment) => expand(segment.slice(1))),
59
59
  };
60
60
  };
61
+
62
+ // ONE GLOB MATCHER, with Bun.Glob's reading of every construct, so the daemon, the desktop folder server (which runs
63
+ // Bun.Glob itself) and the demo admit the same files for the same pattern. Before this the daemon's copy took
64
+ // `[!t]` as a set of the characters `!` and `t`, where Bun.Glob reads "anything but t".
65
+ // - `*` is any run within one path segment, `?` one character of it
66
+ // - `**` as a whole segment is any number of segments, none included (`**/x` matches `x`); inside a segment it is `*`
67
+ // - `[abc]`, `[a-z]`, and `[!abc]` or `[^abc]` for "none of these"; a `]` first in the set is literal
68
+ // - `{a,b}` is either, nested groups and globs inside included; `\` makes the next character literal
69
+ // - an unclosed `[` or `{` is literal, where Bun.Glob matches nothing: a half-typed pattern then still finds its text
70
+ const REGEXP_SPECIAL = /[.*+?^${}()|[\]\\/]/;
71
+
72
+ const literal = (char: string): string => (REGEXP_SPECIAL.test(char) ? `\\${char}` : char);
73
+
74
+ // The index of the `]` closing the set opened at `open`, or -1.
75
+ const setEnd = (glob: string, open: number): number => {
76
+ let index = open + 1;
77
+ if (glob[index] === `!` || glob[index] === `^`) {
78
+ index += 1;
79
+ }
80
+ // A `]` straight after the opening (or its negation) is a member, not the close.
81
+ if (glob[index] === `]`) {
82
+ index += 1;
83
+ }
84
+ for (; index < glob.length; index += 1) {
85
+ if (glob[index] === `\\`) {
86
+ index += 1;
87
+ } else if (glob[index] === `]`) {
88
+ return index;
89
+ }
90
+ }
91
+ return -1;
92
+ };
93
+
94
+ const setSource = (body: string): string => {
95
+ const negated = body.startsWith(`!`) || body.startsWith(`^`);
96
+ const members = (negated ? body.slice(1) : body).replaceAll(/\\(.)/g, `$1`).replaceAll(/[\\\]^[]/g, (char) => `\\${char}`);
97
+ return `[${negated ? `^` : ``}${members}]`;
98
+ };
99
+
100
+ // The index of the `}` closing the group opened at `open`, and where its top-level commas fall; undefined if unclosed.
101
+ const groupOf = (glob: string, open: number): { readonly end: number; readonly commas: readonly number[] } | undefined => {
102
+ const commas: number[] = [];
103
+ let depth = 0;
104
+ for (let index = open; index < glob.length; index += 1) {
105
+ const char = glob[index];
106
+ if (char === `\\`) {
107
+ index += 1;
108
+ } else if (char === `[`) {
109
+ const end = setEnd(glob, index);
110
+ index = end === -1 ? index : end;
111
+ } else if (char === `{`) {
112
+ depth += 1;
113
+ } else if (char === `}`) {
114
+ depth -= 1;
115
+ if (depth === 0) {
116
+ return { end: index, commas };
117
+ }
118
+ } else if (char === `,` && depth === 1) {
119
+ commas.push(index);
120
+ }
121
+ }
122
+ return undefined;
123
+ };
124
+
125
+ // `**` filling a whole segment: the source it stands for and how many characters of the glob it consumed.
126
+ const globstar = (glob: string, index: number, segmentStart: boolean): { readonly source: string; readonly length: number } | undefined => {
127
+ if (!segmentStart || glob[index + 1] !== `*`) {
128
+ return undefined;
129
+ }
130
+ const after = glob[index + 2];
131
+ if (after === `/`) {
132
+ return { source: `(?:[^/]*/)*`, length: 3 };
133
+ }
134
+ return after === undefined ? { source: `.*`, length: 2 } : undefined;
135
+ };
136
+
137
+ const compileGlob = (glob: string, segmentStart: boolean): string => {
138
+ let source = ``;
139
+ let atStart = segmentStart;
140
+ for (let index = 0; index < glob.length; index += 1) {
141
+ const char = glob[index]!;
142
+ const star = char === `*` ? globstar(glob, index, atStart) : undefined;
143
+ if (star !== undefined) {
144
+ source += star.source;
145
+ index += star.length - 1;
146
+ atStart = true;
147
+ continue;
148
+ }
149
+ if (char === `*`) {
150
+ // A run of stars inside a segment is one star.
151
+ while (glob[index + 1] === `*`) {
152
+ index += 1;
153
+ }
154
+ source += `[^/]*`;
155
+ } else if (char === `?`) {
156
+ source += `[^/]`;
157
+ } else if (char === `\\` && index + 1 < glob.length) {
158
+ index += 1;
159
+ source += literal(glob[index]!);
160
+ } else if (char === `[` && setEnd(glob, index) !== -1) {
161
+ const end = setEnd(glob, index);
162
+ source += setSource(glob.slice(index + 1, end));
163
+ index = end;
164
+ } else if (char === `{` && groupOf(glob, index) !== undefined) {
165
+ const group = groupOf(glob, index)!;
166
+ const bounds = [index, ...group.commas, group.end];
167
+ const alternatives = bounds.slice(1).map((end, at) => compileGlob(glob.slice(bounds[at]! + 1, end), atStart));
168
+ source += `(?:${alternatives.join(`|`)})`;
169
+ index = group.end;
170
+ } else {
171
+ source += literal(char);
172
+ }
173
+ atStart = char === `/`;
174
+ }
175
+ return source;
176
+ };
177
+
178
+ // The whole path must match: `*.ts` is a name at the root, not a suffix anywhere.
179
+ export const globRegExp = (glob: string): RegExp => new RegExp(`^${compileGlob(glob, true)}$`);
180
+
181
+ // A matcher for slash-separated relative paths, compiled once.
182
+ export const globMatcher = (glob: string): ((path: string) => boolean) => {
183
+ const pattern = globRegExp(glob);
184
+ return (path) => pattern.test(path);
185
+ };
186
+
187
+ // The search box's whole second field as one test over a relative path: admitted by any include (or by none being
188
+ // typed), and by no exclude. An anchored include's `./` is the root, so it is dropped before matching.
189
+ export const includeMatcher = (include: string | undefined): ((path: string) => boolean) => {
190
+ const { globs, notGlobs } = includeGlobs(include);
191
+ const rooted = (glob: string): string => glob.replace(/^\.\//, ``);
192
+ const admits = globs.map((glob) => globMatcher(rooted(glob)));
193
+ const refuses = notGlobs.map((glob) => globMatcher(rooted(glob)));
194
+ return (path) => (admits.length === 0 || admits.some((test) => test(path))) && !refuses.some((test) => test(path));
195
+ };
@@ -42,6 +42,13 @@ export const normalizeHost = (raw: string): string | undefined => {
42
42
  export const hostMatches = (pattern: string, host: string): boolean =>
43
43
  pattern.startsWith(WILDCARD) ? host.endsWith(pattern.slice(1)) : host === pattern;
44
44
 
45
+ // Whether one entry of a domain list covers a host: a plain entry names a domain and every host under it at any depth
46
+ // (`example.com` covers `example.com` and `api.example.com`, never `notexample.com`), a wildcard only the hosts under it.
47
+ // Both sides are expected normalized. The wallet's allow and deny lists read their entries this way; a secret's host
48
+ // guard reads its own exactly (hostMatches).
49
+ export const hostCovers = (entry: string, host: string): boolean =>
50
+ entry.startsWith(WILDCARD) ? hostMatches(entry, host) : host === entry || host.endsWith(`.${entry}`);
51
+
45
52
  export const hostAllowed = (patterns: readonly string[], host: string): boolean => patterns.some((pattern) => hostMatches(pattern, host));
46
53
 
47
54
  // Whether every host `pattern` matches is already matched by `patterns`: an exact host by any pattern that matches it, a
@@ -656,3 +656,38 @@ describe("git.branch-switch", () => {
656
656
  expect(marked("cd repo && git switch main && pnpm build", "git.branch-switch")).toEqual(["git switch"]);
657
657
  });
658
658
  });
659
+
660
+ describe("Windows' own shells", () => {
661
+ test("PowerShell's and cmd's recursive deletes are files.destructive, the way a device's run_command meets them", () => {
662
+ for (const command of [
663
+ "Remove-Item -Recurse -Force C:\\Users\\me\\thesis",
664
+ "Remove-Item C:\\x -Recurse",
665
+ "Remove-Item -Recurse:$true x",
666
+ "ri -r C:\\x",
667
+ "rm -Recurse x",
668
+ "rd /s /q C:\\x",
669
+ "rmdir /S C:\\x",
670
+ "del /s /q *.tmp",
671
+ "[System.IO.Directory]::Delete('C:\\x', $true)",
672
+ "powershell -Command 'Remove-Item -Recurse -Force C:\\x'",
673
+ "cmd /c 'rmdir /s /q C:\\x'",
674
+ ]) {
675
+ expect(classify(command), command).toContain("files.destructive");
676
+ }
677
+ });
678
+
679
+ test("what only looks like one is not: a single file, a read that recurses, POSIX rm's asking -r", () => {
680
+ for (const command of ["Remove-Item C:\\x\\file.txt", "del file.txt", "Get-ChildItem -Recurse C:\\x", "Copy-Item -Recurse a b", "rmdir empty", "rm -r build", "git rm -r --cached x"]) {
681
+ expect(classify(command), command).not.toContain("files.destructive");
682
+ }
683
+ });
684
+
685
+ test("a volume formatted or a disk cleared is system.destructive", () => {
686
+ for (const command of ["Format-Volume -DriveLetter D", "Clear-Disk -Number 1 -RemoveData", "format D: /q"]) {
687
+ expect(classify(command), command).toContain("system.destructive");
688
+ }
689
+ for (const command of ["npm run format", "prettier --write . && echo format done", "dotnet format"]) {
690
+ expect(classify(command), command).not.toContain("system.destructive");
691
+ }
692
+ });
693
+ });
@@ -202,6 +202,26 @@ describe("a credential token in text that is not a credential file", () => {
202
202
  expect(holdsCredentialToken("-----BEGIN PRIVATE KEY-----\nMIIEvQ")).toBe(true);
203
203
  });
204
204
 
205
+ // The issuers the terminal filter already knew; the two tables are one list now (ISSUERS).
206
+ test.each([
207
+ ["an opaque bearer", "curl -H 'Authorization: Bearer 8f3kQ2mZ9xLp4RtV7wNb' https://api.example.com", "curl -H 'Authorization: Bearer ***' https://api.example.com"],
208
+ // Prefixes split from bodies so push protection does not read these fixtures as live keys.
209
+ ["a shopify admin token", `token ${"shpat_"}0123456789abcdef0123456789abcdef in use`, "token *** in use"],
210
+ ["a sendgrid key", `key ${"SG."}aB3dE5fG7hJ9kL1mN3pQ5r.sT7uV9wX1yZ3aB5cD7eF9gH1jK3lM5nP7qR9sT1uV3w set`, "key *** set"],
211
+ ["a google oauth token", "refreshed ya29.a0AfB_byC3dE5fG7hJ9kL1mN3pQ5 ok", "refreshed *** ok"],
212
+ ["a public-prefixed key", "langfuse pk-lf-0a1b2c3d-4e5f-6a7b-8c9d now", "langfuse *** now"],
213
+ ])("names itself and masks: %s", (_label, text, masked) => {
214
+ expect(holdsCredentialToken(text)).toBe(true);
215
+ expect(maskCredentialMaterial(text)).toBe(masked);
216
+ expect(holdsCredentialToken(masked)).toBe(false);
217
+ });
218
+
219
+ test("a bearer in prose or a template is not one", () => {
220
+ for (const text of ["Authorization: Bearer <token>", "Authorization: Bearer YOUR_API_TOKEN_HERE", "Bearer ${TOKEN}", "the Bearer scheme"]) {
221
+ expect(holdsCredentialToken(text), text).toBe(false);
222
+ }
223
+ });
224
+
205
225
  test("is never a credential-named key beside a value", () => {
206
226
  expect(holdsCredentialToken("const config={apiKey:process.env.API_KEY,password:hunter22x}")).toBe(false);
207
227
  expect(holdsCredentialMaterial("const config={apiKey:process.env.API_KEY,password:hunter22x}")).toBe(true);
@@ -1,4 +1,4 @@
1
- import { includeGlobs } from "../search-globs.js";
1
+ import { globMatcher, includeGlobs, includeMatcher } from "../search-globs.js";
2
2
 
3
3
  test("an empty field scopes nothing", () => {
4
4
  expect(includeGlobs(undefined)).toEqual({ globs: [], notGlobs: [] });
@@ -43,3 +43,48 @@ test("a leading ! excludes instead", () => {
43
43
  // A lone "!" excludes nothing: expanded, it would have matched everything.
44
44
  expect(includeGlobs(`!`)).toEqual({ globs: [], notGlobs: [] });
45
45
  });
46
+
47
+ describe("globMatcher", () => {
48
+ // Every construct, held against Bun.Glob itself: the desktop folder server matches with Bun.Glob, so any
49
+ // disagreement here is two machines admitting different files for one pattern.
50
+ const patterns = [
51
+ `*.ts`, `**/*.ts`, `**/*.[!t]s`, `**/*.[^t]s`, `src/**`, `src/**/*.ts`, `a/**/b`, `a?c`, `[a-c]x`, `[!a]`, `[]]`,
52
+ `*.{ts,js}`, `{a,{b,c}}/*`, `a/{b,c/**}`, `{**/x,y}`, `a**b`, `**`, `**/`, `**/**/x`, `{,a}b`, `\\*x`, `**.ts`, `a/**b`,
53
+ ];
54
+ const paths = [
55
+ `a.ts`, `a.js`, `a/b.ts`, `a/b.js`, `.hidden.ts`, `.x/y.ts`, `src`, `src/a.ts`, `src/a/b.ts`, `a/b`, `a/x/b`, `abc`, `a/c`,
56
+ `bx`, `dx`, `/`, `a`, `]`, `c/x`, `a/c/d`, `a/x`, `x`, `y`, `axxb`, `ax/xb`, `b`, `ab`, `*x`, `a/xb`, `a/c/xb`, `a/`, `a/b/`,
57
+ ];
58
+ for (const pattern of patterns) {
59
+ test(`reads ${pattern} as Bun.Glob does`, () => {
60
+ const glob = new Bun.Glob(pattern);
61
+ const matches = globMatcher(pattern);
62
+ expect(paths.filter((path) => matches(path))).toEqual(paths.filter((path) => glob.match(path)));
63
+ });
64
+ }
65
+
66
+ test("[!t] means anything but t", () => {
67
+ const matches = globMatcher(`**/*.[!t]s`);
68
+ expect([matches(`src/a.js`), matches(`src/a.ts`)]).toEqual([true, false]);
69
+ });
70
+
71
+ test("an unclosed set or group is literal", () => {
72
+ expect([globMatcher(`[ab`)(`[ab`), globMatcher(`{a,b`)(`{a,b`)]).toEqual([true, true]);
73
+ });
74
+ });
75
+
76
+ describe("includeMatcher", () => {
77
+ test("admits by any include and refuses by any exclude", () => {
78
+ const matches = includeMatcher(`src, !*.test.ts`);
79
+ expect([`src/a.ts`, `src/a.test.ts`, `lib/src/b.ts`, `docs/c.md`].map(matches)).toEqual([true, false, true, false]);
80
+ });
81
+
82
+ test("an anchored include matches from the root only", () => {
83
+ const matches = includeMatcher(`./src`);
84
+ expect([matches(`src/a.ts`), matches(`lib/src/a.ts`)]).toEqual([true, false]);
85
+ });
86
+
87
+ test("nothing typed admits every path", () => {
88
+ expect(includeMatcher(undefined)(`any/where.ts`)).toBe(true);
89
+ });
90
+ });
@@ -1,4 +1,4 @@
1
- import { hostAllowed, hostMatches, normalizeHost, normalizeHostPattern, tightensHostGuard } from "../secret-hosts.js";
1
+ import { hostAllowed, hostCovers, hostMatches, normalizeHost, normalizeHostPattern, tightensHostGuard } from "../secret-hosts.js";
2
2
 
3
3
  // The pattern a person writes against the host a use would reach. Every boundary a limit turns on is pinned by value:
4
4
  // the apex a wildcard leaves out, the look-alike that only shares a suffix, and which edits take hosts away.
@@ -87,3 +87,19 @@ describe("tightensHostGuard", () => {
87
87
  expect(tightensHostGuard(on("api.github.com"), on("*.api.github.com"))).toBe(false);
88
88
  });
89
89
  });
90
+
91
+ describe("hostCovers", () => {
92
+ it("reads a plain entry as a domain and every host under it, never a lookalike", () => {
93
+ expect(hostCovers("example.com", "example.com")).toBe(true);
94
+ expect(hostCovers("example.com", "api.example.com")).toBe(true);
95
+ expect(hostCovers("example.com", "a.b.example.com")).toBe(true);
96
+ expect(hostCovers("example.com", "notexample.com")).toBe(false);
97
+ expect(hostCovers("example.com", "example.com.evil.net")).toBe(false);
98
+ });
99
+
100
+ it("reads a wildcard as the hosts under it only", () => {
101
+ expect(hostCovers("*.example.com", "api.example.com")).toBe(true);
102
+ expect(hostCovers("*.example.com", "example.com")).toBe(false);
103
+ expect(hostCovers("*.example.com", "notexample.com")).toBe(false);
104
+ });
105
+ });
@@ -13,6 +13,8 @@ const TURNED_AWAY: ReadonlySet<string> = new Set([
13
13
  "model-helper-only",
14
14
  // The privacy shield refused an untrusted provider on a runtime it cannot read by content at all.
15
15
  "privacy-unshielded",
16
+ // The turn carries a tool list its runtime cannot keep.
17
+ "tools-unenforced",
16
18
  // The protected execution boundary refused admission, not an executed provider/runtime failure.
17
19
  "agent-domain-refused",
18
20
  // The privacy shield found personal data in instructions a runtime loads itself, where it could not mask them.
@@ -20,6 +20,10 @@ export const RunnerHelloSchema = z.object({
20
20
  // Compared against what the parent runs to report parity (outdated → rebuild action); an outdated runner still runs
21
21
  // turns rather than being refused.
22
22
  image: z.string(),
23
+ // The image `image` was composed from (SANDBOX_BASE_IMAGE). Parity compares this, not `image`: an overlay build's
24
+ // tag names its own container (`intentic-sandbox-env-<slug>:<hash>`), so two sides on one base and one overlay
25
+ // never share it. Absent on an old image, which is then compared by `image` alone.
26
+ baseImage: z.string().optional(),
23
27
  channel: z.string().optional(),
24
28
  overlayHash: z.string().optional(),
25
29
  // Runner's declared settings-only sandbox.toml (definition.ts), diffed against the parent's to itemize drift;
@@ -106,13 +110,22 @@ export type RunnerCredentialRefreshRequest = z.infer<typeof RunnerCredentialRefr
106
110
  export const RunnerCredentialRefreshSchema = z.object({ accessToken: z.string().optional() });
107
111
  export type RunnerCredentialRefresh = z.infer<typeof RunnerCredentialRefreshSchema>;
108
112
 
109
- // What a runner is, hardware-wise: what the placement picker shows and a future scheduler would weigh.
113
+ // What a runner is, hardware-wise, and how full it is right now: what the placement picker shows and the fleet advice
114
+ // (runners/fleet-advice.ts) weighs. Re-asked on demand rather than only at connect, so the live half stays live.
110
115
  export const RunnerFactsSchema = z.object({
111
116
  cpus: z.number().int().positive(),
112
117
  memoryMb: z.number().int().positive(),
113
118
  freeDiskMb: z.number().int().nonnegative(),
114
119
  // 0..1 over the last minute; a coarse busy signal for a picker, not a metrics feed.
115
120
  load: z.number().nonnegative(),
121
+ // The runner's own resource budget, the figures its turn door admits by: what is free under its memory limit, and
122
+ // that limit. Absent from a runner that predates them, which is then sized by `memoryMb` alone.
123
+ memoryFreeMb: z.number().int().nonnegative().optional(),
124
+ memoryLimitMb: z.number().int().positive().optional(),
125
+ // Pressure stall over the last ten seconds, in percent: anything waiting for a CPU, everything waiting on memory.
126
+ // Absent where the kernel does not report it.
127
+ cpuPressure: z.number().nonnegative().optional(),
128
+ memoryStall: z.number().nonnegative().optional(),
116
129
  });
117
130
  export type RunnerFacts = z.infer<typeof RunnerFactsSchema>;
118
131
 
@@ -0,0 +1,86 @@
1
+ import { redeemWhileWarming, type RedeemRetry } from "./pairing-redeem.js";
2
+
3
+ /* The redeem loop both pairing peers run, over a sandbox the test plays one answer at a time. */
4
+
5
+ const scripted = (answers: readonly (number | "offline" | "hang")[]) => {
6
+ const tries: number[] = [];
7
+ const send = async (attempt: number, signal: AbortSignal | undefined): Promise<Response> => {
8
+ tries.push(attempt);
9
+ const next = answers[attempt - 1] ?? answers.at(-1) ?? "offline";
10
+ if (next === "offline") {
11
+ throw new TypeError("fetch failed");
12
+ }
13
+ if (next === "hang") {
14
+ return new Promise<Response>((_resolve, reject) => signal?.addEventListener("abort", () => reject(signal.reason)));
15
+ }
16
+ return new Response(`status ${next}`, { status: next });
17
+ };
18
+ return { tries, send };
19
+ };
20
+
21
+ const statusOf = (outcome: Awaited<ReturnType<typeof redeemWhileWarming>>): number | undefined =>
22
+ outcome.kind === "answered" ? outcome.response.status : undefined;
23
+
24
+ describe("redeemWhileWarming", () => {
25
+ it("answers the first reply below 500 without asking again", async () => {
26
+ const { tries, send } = scripted([200]);
27
+ expect(statusOf(await redeemWhileWarming(send, { attempts: 3, delayMs: 0 }))).toBe(200);
28
+ expect(tries).toEqual([1]);
29
+ });
30
+
31
+ it("asks again through a warming sandbox and an unreachable one, saying why each time", async () => {
32
+ const { tries, send } = scripted(["offline", 502, 200]);
33
+ const heard: { attempt: number; why: RedeemRetry["kind"] }[] = [];
34
+ const outcome = await redeemWhileWarming(send, { attempts: 5, delayMs: 0, onRetry: (attempt, why) => heard.push({ attempt, why: why.kind }) });
35
+ expect(statusOf(outcome)).toBe(200);
36
+ expect(tries).toEqual([1, 2, 3]);
37
+ expect(heard).toEqual([
38
+ { attempt: 1, why: "unreachable" },
39
+ { attempt: 2, why: "warming" },
40
+ ]);
41
+ });
42
+
43
+ it("hands back a 401 at once: an expired code is an answer, not a blip", async () => {
44
+ const { tries, send } = scripted([401, 200]);
45
+ expect(statusOf(await redeemWhileWarming(send, { attempts: 5, delayMs: 0 }))).toBe(401);
46
+ expect(tries).toEqual([1]);
47
+ });
48
+
49
+ it("answers the last 5xx when the sandbox is still warming after every try", async () => {
50
+ const { tries, send } = scripted([503]);
51
+ const heard: number[] = [];
52
+ const outcome = await redeemWhileWarming(send, { attempts: 3, delayMs: 0, onRetry: (attempt) => heard.push(attempt) });
53
+ expect(outcome.kind).toBe("answered");
54
+ expect(outcome.kind === "answered" ? await outcome.response.text() : undefined).toBe("status 503");
55
+ expect(tries).toEqual([1, 2, 3]);
56
+ // The last try is not retried, so it is not narrated as one.
57
+ expect(heard).toEqual([1, 2]);
58
+ });
59
+
60
+ it("is unreachable, with the last try's failure, when nothing ever answered", async () => {
61
+ const { tries, send } = scripted(["offline"]);
62
+ const outcome = await redeemWhileWarming(send, { attempts: 2, delayMs: 0 });
63
+ expect(outcome.kind).toBe("unreachable");
64
+ expect(outcome.kind === "unreachable" ? outcome.error : undefined).toBeInstanceOf(TypeError);
65
+ expect(tries).toEqual([1, 2]);
66
+ });
67
+
68
+ it("abandons a try that outlives its timeout and asks again", async () => {
69
+ const { tries, send } = scripted(["hang", 200]);
70
+ expect(statusOf(await redeemWhileWarming(send, { attempts: 2, delayMs: 0, timeoutMs: 10 }))).toBe(200);
71
+ expect(tries).toEqual([1, 2]);
72
+ });
73
+
74
+ it("rejects with the stop's reason rather than answering when its signal aborts", async () => {
75
+ const stop = new AbortController();
76
+ const { tries, send } = scripted([502]);
77
+ const running = redeemWhileWarming(send, {
78
+ attempts: 5,
79
+ delayMs: 60_000,
80
+ signal: stop.signal,
81
+ onRetry: () => stop.abort(new Error("stopped")),
82
+ });
83
+ await expect(running).rejects.toThrow("stopped");
84
+ expect(tries).toEqual([1]);
85
+ });
86
+ });
@@ -0,0 +1,79 @@
1
+ import { retry, withDeadline } from "@intentic/base/async";
2
+
3
+ // Redeeming a pairing code's one-time token, as every peer that takes one does it (the machine agent's
4
+ // `postWhileWarming`, the webext's `redeemPairing`): a sandbox still warming up answers 5xx through its tunnel, or
5
+ // nothing at all, and is asked again. Any other answer, a 401 above all (the code has expired), is the caller's to read
6
+ // at once: asking again would only delay the fresh code the person has to fetch.
7
+
8
+ // Why a try is being asked again: no answer at all, or a 5xx from a sandbox not ready yet.
9
+ export type RedeemRetry = { readonly kind: "unreachable"; readonly error: unknown } | { readonly kind: "warming"; readonly status: number };
10
+
11
+ export type RedeemOutcome =
12
+ // The sandbox's answer: below 500 at once, or still 5xx when the tries ran out.
13
+ | { readonly kind: "answered"; readonly response: Response }
14
+ // No answer at all on the last try; `error` is that try's failure.
15
+ | { readonly kind: "unreachable"; readonly error: unknown };
16
+
17
+ export interface RedeemOptions {
18
+ // Tries in all, the first included.
19
+ readonly attempts: number;
20
+ // The wait between tries.
21
+ readonly delayMs: number;
22
+ // How long one try may take before it is abandoned and counts as unreachable; none when absent.
23
+ readonly timeoutMs?: number | undefined;
24
+ // Stops the tries: a stop rejects with the signal's reason rather than answering.
25
+ readonly signal?: AbortSignal | undefined;
26
+ // Hears each try that is about to be asked again, before the wait.
27
+ readonly onRetry?: ((attempt: number, why: RedeemRetry) => void) | undefined;
28
+ }
29
+
30
+ // A 5xx carried out of a try so `retry` asks again, and handed back as the answer when the tries run out.
31
+ class Warming extends Error {
32
+ constructor(readonly response: Response) {
33
+ super(`the sandbox is warming up (HTTP ${response.status})`);
34
+ }
35
+ }
36
+
37
+ // Runs `send` (one POST, handed the try's number and a signal to pass to its fetch, undefined when there is neither a
38
+ // deadline nor a stop) until the sandbox answers below 500 or the tries run out.
39
+ export const redeemWhileWarming = async (
40
+ send: (attempt: number, signal: AbortSignal | undefined) => Promise<Response>,
41
+ options: RedeemOptions,
42
+ ): Promise<RedeemOutcome> => {
43
+ const { attempts, delayMs, timeoutMs, signal, onRetry } = options;
44
+ const once = (attempt: number): Promise<Response> =>
45
+ timeoutMs === undefined ? send(attempt, signal) : withDeadline((bounded) => send(attempt, bounded), timeoutMs, { signal });
46
+ try {
47
+ const response = await retry(
48
+ async (attempt) => {
49
+ let answer: Response;
50
+ try {
51
+ answer = await once(attempt);
52
+ } catch (error) {
53
+ if (attempt < attempts && signal?.aborted !== true) {
54
+ onRetry?.(attempt, { kind: "unreachable", error });
55
+ }
56
+ throw error;
57
+ }
58
+ if (answer.status < 500) {
59
+ return answer;
60
+ }
61
+ if (attempt < attempts) {
62
+ onRetry?.(attempt, { kind: "warming", status: answer.status });
63
+ }
64
+ throw new Warming(answer);
65
+ },
66
+ { attempts, delayMs: () => delayMs, signal },
67
+ );
68
+ return { kind: "answered", response };
69
+ } catch (error) {
70
+ if (signal?.aborted === true) {
71
+ // oxlint-disable-next-line typescript/only-throw-error -- the stop's own reason, as an aborted fetch rejects with it
72
+ throw signal.reason;
73
+ }
74
+ if (error instanceof Warming) {
75
+ return { kind: "answered", response: error.response };
76
+ }
77
+ return { kind: "unreachable", error };
78
+ }
79
+ };
@@ -2,6 +2,7 @@
2
2
  // namespace keeps zod's 60 locales (~250 kB) that esbuild can otherwise drop. _devices/webext/scripts/size-budget.mjs holds the ceiling.
3
3
  import { prettifyError, toJSONSchema } from "zod";
4
4
  import type * as z from "zod";
5
+ import { isRecord } from "@intentic/base/escape";
5
6
 
6
7
  // The MCP version every peer announces, here rather than in host-protocol.ts because a browser extension bundles this
7
8
  // module and would otherwise carry the machine handshake's schemas to a store. Also read by the daemon's peer bridge,
@@ -99,8 +100,6 @@ export interface McpServerSpec<Ctx> {
99
100
  readonly audit: (entry: McpAuditEntry) => Promise<void> | void;
100
101
  }
101
102
 
102
- const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
103
-
104
103
  // Handles one JSON-RPC message; returns undefined for a notification. `ctx` is read per call, so a grant pushed
105
104
  // mid-session takes effect on the next call.
106
105
  export const createMcpServer = <Ctx>(spec: McpServerSpec<Ctx>): ((message: unknown, ctx: Ctx) => Promise<Record<string, unknown> | undefined>) => {
@@ -10,6 +10,7 @@ import {
10
10
  } from "../../schemas/environment.js";
11
11
  import { MemberEmailSchema, MemberGrantSchema, MembersListSchema, MembersRosterSchema } from "../../schemas/members.js";
12
12
  import { OkSchema } from "../../schemas/shared.js";
13
+ import { EnrollCodeAnswerSchema, EnrollCodeRequestSchema } from "../../schemas/short-codes.js";
13
14
  import { DefinitionDiffSchema, DefinitionExportSchema, WorkspacePublishResultSchema, WorkspacePublishSchema, WorkspaceRemoteSchema } from "../../state/definition.js";
14
15
  import type { RawRouteKey } from "./raw-routes.js";
15
16
 
@@ -24,6 +25,8 @@ export interface RawJsonRoute {
24
25
  }
25
26
 
26
27
  export const RAW_JSON_ROUTES = {
28
+ // A server-connect code for the owner's command (short-codes.ts).
29
+ "POST /system/enroll/code": { input: EnrollCodeRequestSchema, output: EnrollCodeAnswerSchema },
27
30
  // The roster is ownership's; every write answers the roster after it.
28
31
  "GET /members": { output: MembersRosterSchema },
29
32
  "POST /members": { input: MemberGrantSchema, output: MembersListSchema },