@intentic/sandbox-contract 1.312.0 → 1.313.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 (352) hide show
  1. package/README.md +26 -3
  2. package/dist/chores/index.d.ts +0 -1
  3. package/dist/chores/index.d.ts.map +1 -1
  4. package/dist/chores/index.js +0 -1
  5. package/dist/chores/index.js.map +1 -1
  6. package/dist/contracts/accounts.contract.d.ts +57 -0
  7. package/dist/contracts/accounts.contract.d.ts.map +1 -1
  8. package/dist/contracts/agent.contract.d.ts +436 -0
  9. package/dist/contracts/agent.contract.d.ts.map +1 -1
  10. package/dist/contracts/agent.contract.js +11 -1
  11. package/dist/contracts/agent.contract.js.map +1 -1
  12. package/dist/contracts/agents.contract.d.ts +123 -96
  13. package/dist/contracts/agents.contract.d.ts.map +1 -1
  14. package/dist/contracts/capabilities.contract.js +1 -1
  15. package/dist/contracts/capabilities.contract.js.map +1 -1
  16. package/dist/contracts/device.contract.d.ts +19 -4
  17. package/dist/contracts/device.contract.d.ts.map +1 -1
  18. package/dist/contracts/device.contract.js +1 -1
  19. package/dist/contracts/device.contract.js.map +1 -1
  20. package/dist/contracts/extensions.contract.d.ts +11 -0
  21. package/dist/contracts/extensions.contract.d.ts.map +1 -1
  22. package/dist/contracts/git.contract.d.ts +2 -0
  23. package/dist/contracts/git.contract.d.ts.map +1 -1
  24. package/dist/contracts/personas.contract.d.ts +2 -0
  25. package/dist/contracts/personas.contract.d.ts.map +1 -1
  26. package/dist/contracts/runner.contract.d.ts +138 -0
  27. package/dist/contracts/runner.contract.d.ts.map +1 -1
  28. package/dist/contracts/secrets.contract.d.ts.map +1 -1
  29. package/dist/contracts/secrets.contract.js +1 -0
  30. package/dist/contracts/secrets.contract.js.map +1 -1
  31. package/dist/contracts/sessions.contract.d.ts +59 -0
  32. package/dist/contracts/sessions.contract.d.ts.map +1 -1
  33. package/dist/contracts/settings.contract.d.ts +12 -8
  34. package/dist/contracts/settings.contract.d.ts.map +1 -1
  35. package/dist/contracts/settings.contract.js +2 -2
  36. package/dist/contracts/settings.contract.js.map +1 -1
  37. package/dist/contracts/system.contract.d.ts +139 -19
  38. package/dist/contracts/system.contract.d.ts.map +1 -1
  39. package/dist/contracts/translator.contract.d.ts +72 -0
  40. package/dist/contracts/translator.contract.d.ts.map +1 -1
  41. package/dist/contracts/workspace.contract.d.ts +85 -14
  42. package/dist/contracts/workspace.contract.d.ts.map +1 -1
  43. package/dist/documents/index.d.ts +1 -0
  44. package/dist/documents/index.d.ts.map +1 -1
  45. package/dist/documents/index.js +1 -0
  46. package/dist/documents/index.js.map +1 -1
  47. package/dist/documents/read.d.ts +24 -0
  48. package/dist/documents/read.d.ts.map +1 -0
  49. package/dist/documents/read.js +81 -0
  50. package/dist/documents/read.js.map +1 -0
  51. package/dist/events/agent-events.d.ts +235 -0
  52. package/dist/events/agent-events.d.ts.map +1 -1
  53. package/dist/events/agent-events.js +3 -0
  54. package/dist/events/agent-events.js.map +1 -1
  55. package/dist/events/child-run.d.ts +9 -0
  56. package/dist/events/child-run.d.ts.map +1 -0
  57. package/dist/events/child-run.js +41 -0
  58. package/dist/events/child-run.js.map +1 -0
  59. package/dist/events/errands.d.ts +5 -0
  60. package/dist/events/errands.d.ts.map +1 -0
  61. package/dist/events/errands.js +19 -0
  62. package/dist/events/errands.js.map +1 -0
  63. package/dist/events/requests.d.ts +152 -0
  64. package/dist/events/requests.d.ts.map +1 -1
  65. package/dist/events/requests.js +26 -1
  66. package/dist/events/requests.js.map +1 -1
  67. package/dist/events/system-events.d.ts +8 -12
  68. package/dist/events/system-events.d.ts.map +1 -1
  69. package/dist/events/transcript.d.ts +387 -0
  70. package/dist/events/transcript.d.ts.map +1 -1
  71. package/dist/events/transcript.js +3 -0
  72. package/dist/events/transcript.js.map +1 -1
  73. package/dist/front/browser-wire.d.ts +6 -0
  74. package/dist/front/browser-wire.d.ts.map +1 -0
  75. package/dist/front/browser-wire.js +4 -0
  76. package/dist/front/browser-wire.js.map +1 -0
  77. package/dist/front/front-wire.d.ts +6 -0
  78. package/dist/front/front-wire.d.ts.map +1 -0
  79. package/dist/front/front-wire.js +5 -0
  80. package/dist/front/front-wire.js.map +1 -0
  81. package/dist/front/generated/browser-wire.d.ts +19 -0
  82. package/dist/front/generated/browser-wire.d.ts.map +1 -0
  83. package/dist/front/generated/browser-wire.js +2 -0
  84. package/dist/front/generated/browser-wire.js.map +1 -0
  85. package/dist/front/generated/wire.d.ts +36 -30
  86. package/dist/front/generated/wire.d.ts.map +1 -1
  87. package/dist/ids/conversation-ids.d.ts +1 -0
  88. package/dist/ids/conversation-ids.d.ts.map +1 -1
  89. package/dist/ids/conversation-ids.js +2 -2
  90. package/dist/ids/conversation-ids.js.map +1 -1
  91. package/dist/ids/session-names.d.ts +3 -0
  92. package/dist/ids/session-names.d.ts.map +1 -1
  93. package/dist/ids/session-names.js +3 -0
  94. package/dist/ids/session-names.js.map +1 -1
  95. package/dist/index.d.ts +1036 -168
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.js +8 -0
  98. package/dist/index.js.map +1 -1
  99. package/dist/models/agent-catalog.d.ts.map +1 -1
  100. package/dist/models/agent-catalog.js +4 -6
  101. package/dist/models/agent-catalog.js.map +1 -1
  102. package/dist/models/agent-runtimes.d.ts +2 -1
  103. package/dist/models/agent-runtimes.d.ts.map +1 -1
  104. package/dist/models/agent-runtimes.js +7 -1
  105. package/dist/models/agent-runtimes.js.map +1 -1
  106. package/dist/models/plan-pools.d.ts +22 -1
  107. package/dist/models/plan-pools.d.ts.map +1 -1
  108. package/dist/models/plan-pools.js +86 -0
  109. package/dist/models/plan-pools.js.map +1 -1
  110. package/dist/policy/request-status.d.ts +1 -1
  111. package/dist/policy/request-status.d.ts.map +1 -1
  112. package/dist/policy/request-status.js +4 -11
  113. package/dist/policy/request-status.js.map +1 -1
  114. package/dist/policy/reserved-servers.d.ts.map +1 -1
  115. package/dist/policy/reserved-servers.js +2 -1
  116. package/dist/policy/reserved-servers.js.map +1 -1
  117. package/dist/policy/turned-away.d.ts.map +1 -1
  118. package/dist/policy/turned-away.js +1 -0
  119. package/dist/policy/turned-away.js.map +1 -1
  120. package/dist/protocol/edge-verdict.d.ts +2 -1
  121. package/dist/protocol/edge-verdict.d.ts.map +1 -1
  122. package/dist/protocol/edge-verdict.js +1 -1
  123. package/dist/protocol/edge-verdict.js.map +1 -1
  124. package/dist/protocol/ingress-contract.d.ts +1 -1
  125. package/dist/protocol/ingress-contract.d.ts.map +1 -1
  126. package/dist/protocol/ingress-contract.js +1 -1
  127. package/dist/protocol/ingress-contract.js.map +1 -1
  128. package/dist/protocol/raw-routes.d.ts +18 -40
  129. package/dist/protocol/raw-routes.d.ts.map +1 -1
  130. package/dist/protocol/raw-routes.js +5 -16
  131. package/dist/protocol/raw-routes.js.map +1 -1
  132. package/dist/protocol/route-meta.d.ts +20 -0
  133. package/dist/protocol/route-meta.d.ts.map +1 -1
  134. package/dist/protocol/route-meta.js +16 -0
  135. package/dist/protocol/route-meta.js.map +1 -1
  136. package/dist/protocol/tunnel-bulk.d.ts +2 -0
  137. package/dist/protocol/tunnel-bulk.d.ts.map +1 -0
  138. package/dist/protocol/tunnel-bulk.js +3 -0
  139. package/dist/protocol/tunnel-bulk.js.map +1 -0
  140. package/dist/protocol/x402.d.ts +17 -0
  141. package/dist/protocol/x402.d.ts.map +1 -0
  142. package/dist/protocol/x402.js +37 -0
  143. package/dist/protocol/x402.js.map +1 -0
  144. package/dist/schemas/agent.d.ts +25 -0
  145. package/dist/schemas/agent.d.ts.map +1 -1
  146. package/dist/schemas/agent.js +14 -2
  147. package/dist/schemas/agent.js.map +1 -1
  148. package/dist/schemas/agents.d.ts +17 -18
  149. package/dist/schemas/agents.d.ts.map +1 -1
  150. package/dist/schemas/agents.js +16 -3
  151. package/dist/schemas/agents.js.map +1 -1
  152. package/dist/schemas/automations.d.ts +4 -6
  153. package/dist/schemas/automations.d.ts.map +1 -1
  154. package/dist/schemas/automations.js.map +1 -1
  155. package/dist/schemas/devices.d.ts +132 -7
  156. package/dist/schemas/devices.d.ts.map +1 -1
  157. package/dist/schemas/devices.js +43 -18
  158. package/dist/schemas/devices.js.map +1 -1
  159. package/dist/schemas/extension-updates.d.ts +22 -0
  160. package/dist/schemas/extension-updates.d.ts.map +1 -1
  161. package/dist/schemas/extension-updates.js +4 -0
  162. package/dist/schemas/extension-updates.js.map +1 -1
  163. package/dist/schemas/git/git.d.ts +3 -0
  164. package/dist/schemas/git/git.d.ts.map +1 -1
  165. package/dist/schemas/hosts.d.ts +29 -5
  166. package/dist/schemas/hosts.d.ts.map +1 -1
  167. package/dist/schemas/hosts.js +20 -7
  168. package/dist/schemas/hosts.js.map +1 -1
  169. package/dist/schemas/keep-warm.d.ts +2 -12
  170. package/dist/schemas/keep-warm.d.ts.map +1 -1
  171. package/dist/schemas/keep-warm.js +8 -10
  172. package/dist/schemas/keep-warm.js.map +1 -1
  173. package/dist/schemas/metrics.d.ts +22 -0
  174. package/dist/schemas/metrics.d.ts.map +1 -1
  175. package/dist/schemas/metrics.js +18 -3
  176. package/dist/schemas/metrics.js.map +1 -1
  177. package/dist/schemas/personas.d.ts +3 -0
  178. package/dist/schemas/personas.d.ts.map +1 -1
  179. package/dist/schemas/personas.js +9 -0
  180. package/dist/schemas/personas.js.map +1 -1
  181. package/dist/schemas/providers/plan-limits.d.ts +140 -0
  182. package/dist/schemas/providers/plan-limits.d.ts.map +1 -1
  183. package/dist/schemas/providers/plan-limits.js +47 -1
  184. package/dist/schemas/providers/plan-limits.js.map +1 -1
  185. package/dist/schemas/providers/provider-oauth.d.ts +57 -0
  186. package/dist/schemas/providers/provider-oauth.d.ts.map +1 -1
  187. package/dist/schemas/providers/provider-oauth.js +6 -1
  188. package/dist/schemas/providers/provider-oauth.js.map +1 -1
  189. package/dist/schemas/settings-history.d.ts +28 -1
  190. package/dist/schemas/settings-history.d.ts.map +1 -1
  191. package/dist/schemas/settings-history.js +35 -1
  192. package/dist/schemas/settings-history.js.map +1 -1
  193. package/dist/schemas/settings.d.ts +184 -4
  194. package/dist/schemas/settings.d.ts.map +1 -1
  195. package/dist/schemas/settings.js +31 -23
  196. package/dist/schemas/settings.js.map +1 -1
  197. package/dist/schemas/speaker.d.ts +36 -0
  198. package/dist/schemas/speaker.d.ts.map +1 -0
  199. package/dist/schemas/speaker.js +62 -0
  200. package/dist/schemas/speaker.js.map +1 -0
  201. package/dist/schemas/state-plan.d.ts +46 -0
  202. package/dist/schemas/state-plan.d.ts.map +1 -0
  203. package/dist/schemas/state-plan.js +31 -0
  204. package/dist/schemas/state-plan.js.map +1 -0
  205. package/dist/schemas/system.d.ts +48 -8
  206. package/dist/schemas/system.d.ts.map +1 -1
  207. package/dist/schemas/system.js +10 -14
  208. package/dist/schemas/system.js.map +1 -1
  209. package/dist/schemas/workspace/mainline.d.ts +206 -64
  210. package/dist/schemas/workspace/mainline.d.ts.map +1 -1
  211. package/dist/schemas/workspace/mainline.js +73 -25
  212. package/dist/schemas/workspace/mainline.js.map +1 -1
  213. package/dist/state/contract-lock.d.ts +1 -0
  214. package/dist/state/contract-lock.d.ts.map +1 -1
  215. package/dist/state/contract-lock.js +47 -1
  216. package/dist/state/contract-lock.js.map +1 -1
  217. package/dist/state/definition.d.ts +24 -16
  218. package/dist/state/definition.d.ts.map +1 -1
  219. package/dist/state/fix-stance.d.ts +1 -1
  220. package/dist/state/fix-stance.d.ts.map +1 -1
  221. package/dist/state/fix-stance.js +2 -1
  222. package/dist/state/fix-stance.js.map +1 -1
  223. package/dist/state/history-state.d.ts.map +1 -1
  224. package/dist/state/history-state.js +2 -0
  225. package/dist/state/history-state.js.map +1 -1
  226. package/dist/state/landed-commit.d.ts +1 -0
  227. package/dist/state/landed-commit.d.ts.map +1 -1
  228. package/dist/state/landed-commit.js +2 -0
  229. package/dist/state/landed-commit.js.map +1 -1
  230. package/dist/state/red-streak.d.ts +7 -0
  231. package/dist/state/red-streak.d.ts.map +1 -0
  232. package/dist/state/red-streak.js +11 -0
  233. package/dist/state/red-streak.js.map +1 -0
  234. package/dist/state/workspace-state.d.ts +1 -0
  235. package/dist/state/workspace-state.d.ts.map +1 -1
  236. package/dist/state/workspace-state.js +6 -2
  237. package/dist/state/workspace-state.js.map +1 -1
  238. package/dist/text/shell-regions.d.ts +1 -0
  239. package/dist/text/shell-regions.d.ts.map +1 -1
  240. package/dist/text/shell-regions.js +1 -1
  241. package/dist/text/shell-regions.js.map +1 -1
  242. package/dist/text/transcript-fold.d.ts.map +1 -1
  243. package/dist/text/transcript-fold.js +14 -7
  244. package/dist/text/transcript-fold.js.map +1 -1
  245. package/package.json +28 -29
  246. package/src/chores/index.ts +0 -1
  247. package/src/contracts/agent.contract.ts +14 -0
  248. package/src/contracts/capabilities.contract.ts +2 -2
  249. package/src/contracts/device.contract.ts +5 -3
  250. package/src/contracts/secrets.contract.ts +2 -0
  251. package/src/contracts/settings.contract.ts +2 -1
  252. package/src/documents/conversions.ts +3 -3
  253. package/src/documents/index.ts +3 -1
  254. package/src/documents/read.ts +114 -0
  255. package/src/events/agent-events.ts +5 -0
  256. package/src/events/child-run.test.ts +81 -0
  257. package/src/events/child-run.ts +73 -0
  258. package/src/events/errands.test.ts +22 -0
  259. package/src/events/errands.ts +27 -0
  260. package/src/events/requests.ts +40 -1
  261. package/src/events/transcript.ts +8 -0
  262. package/src/front/browser-wire.ts +20 -0
  263. package/src/front/front-wire.ts +15 -0
  264. package/src/front/generated/browser-wire.json +138 -0
  265. package/src/front/generated/browser-wire.ts +16 -0
  266. package/src/front/generated/front-wire.json +793 -0
  267. package/src/front/generated/tunnel.json +38 -0
  268. package/src/front/generated/wire.ts +21 -24
  269. package/src/front/wire-manifests.test.ts +61 -0
  270. package/src/ids/conversation-ids.ts +6 -5
  271. package/src/ids/session-names.ts +7 -0
  272. package/src/index.ts +8 -1
  273. package/src/models/agent-catalog.test.ts +6 -4
  274. package/src/models/agent-catalog.ts +4 -6
  275. package/src/models/agent-runtimes.ts +22 -7
  276. package/src/models/capability-ledger.test.ts +2 -0
  277. package/src/models/plan-pools.test.ts +121 -2
  278. package/src/models/plan-pools.ts +152 -2
  279. package/src/policy/request-status.ts +4 -12
  280. package/src/policy/reserved-servers.test.ts +6 -0
  281. package/src/policy/reserved-servers.ts +13 -10
  282. package/src/policy/turned-away.ts +3 -0
  283. package/src/protocol/edge-verdict.ts +13 -11
  284. package/src/protocol/ingress-contract.fixture.json +13 -0
  285. package/src/protocol/ingress-contract.ts +9 -5
  286. package/src/protocol/raw-routes.ts +10 -21
  287. package/src/protocol/route-meta.ts +36 -1
  288. package/src/protocol/tunnel-bulk.test.ts +37 -0
  289. package/src/protocol/tunnel-bulk.ts +7 -0
  290. package/src/protocol/x402.test.ts +29 -0
  291. package/src/protocol/x402.ts +61 -0
  292. package/src/schemas/agent.ts +28 -3
  293. package/src/schemas/agents.ts +32 -6
  294. package/src/schemas/automations.ts +4 -2
  295. package/src/schemas/devices.ts +106 -39
  296. package/src/schemas/extension-updates.ts +6 -0
  297. package/src/schemas/hosts.ts +62 -19
  298. package/src/schemas/ic-shape-args.test.ts +64 -0
  299. package/src/schemas/keep-warm.test.ts +6 -13
  300. package/src/schemas/keep-warm.ts +15 -20
  301. package/src/schemas/metrics.ts +26 -3
  302. package/src/schemas/personas.ts +15 -0
  303. package/src/schemas/providers/plan-limits.ts +71 -1
  304. package/src/schemas/providers/provider-oauth.ts +10 -1
  305. package/src/schemas/settings-history.ts +50 -3
  306. package/src/schemas/settings.test.ts +54 -1
  307. package/src/schemas/settings.ts +49 -31
  308. package/src/schemas/speaker.ts +89 -0
  309. package/src/schemas/state-plan.test.ts +69 -0
  310. package/src/schemas/state-plan.ts +47 -0
  311. package/src/schemas/system.ts +20 -16
  312. package/src/schemas/workspace/mainline.test.ts +28 -0
  313. package/src/schemas/workspace/mainline.ts +111 -44
  314. package/src/state/contract-lock.test.ts +65 -4
  315. package/src/state/contract-lock.ts +70 -2
  316. package/src/state/fix-stance.test.ts +8 -0
  317. package/src/state/fix-stance.ts +3 -3
  318. package/src/state/history-state.ts +5 -1
  319. package/src/state/landed-commit.test.ts +12 -1
  320. package/src/state/landed-commit.ts +5 -0
  321. package/src/state/red-streak.test.ts +28 -0
  322. package/src/state/red-streak.ts +28 -0
  323. package/src/state/workspace-state.test.ts +7 -1
  324. package/src/state/workspace-state.ts +10 -2
  325. package/src/text/shell-regions.test.ts +38 -0
  326. package/src/text/shell-regions.ts +3 -3
  327. package/src/text/transcript-fold.test.ts +13 -0
  328. package/src/text/transcript-fold.ts +17 -8
  329. package/dist/chores/fix-deps.d.ts +0 -10
  330. package/dist/chores/fix-deps.d.ts.map +0 -1
  331. package/dist/chores/fix-deps.js +0 -19
  332. package/dist/chores/fix-deps.js.map +0 -1
  333. package/dist/front/terminal-frames.d.ts +0 -17
  334. package/dist/front/terminal-frames.d.ts.map +0 -1
  335. package/dist/front/terminal-frames.js +0 -79
  336. package/dist/front/terminal-frames.js.map +0 -1
  337. package/dist/protocol/tunnel-lanes.d.ts +0 -10
  338. package/dist/protocol/tunnel-lanes.d.ts.map +0 -1
  339. package/dist/protocol/tunnel-lanes.js +0 -16
  340. package/dist/protocol/tunnel-lanes.js.map +0 -1
  341. package/dist/protocol/upgrade-relay.d.ts +0 -4
  342. package/dist/protocol/upgrade-relay.d.ts.map +0 -1
  343. package/dist/protocol/upgrade-relay.js +0 -25
  344. package/dist/protocol/upgrade-relay.js.map +0 -1
  345. package/src/chores/fix-deps.ts +0 -23
  346. package/src/front/terminal-frames.fixture.json +0 -55
  347. package/src/front/terminal-frames.test.ts +0 -63
  348. package/src/front/terminal-frames.ts +0 -102
  349. package/src/protocol/tunnel-lanes.json +0 -136
  350. package/src/protocol/tunnel-lanes.test.ts +0 -30
  351. package/src/protocol/tunnel-lanes.ts +0 -32
  352. package/src/protocol/upgrade-relay.ts +0 -28
@@ -1,10 +1,11 @@
1
- import type { AccountUsage, UsageWindow, WindowGates } from "../schemas/providers/plan-limits.js";
1
+ import type { AccountState, AccountUsage, ProviderRefusal, UsageWindow, WindowGates } from "../schemas/providers/plan-limits.js";
2
2
 
3
3
  // Single reading of which account pool gates a given model (or, with none named, the account's tightest pool), shared
4
4
  // by the daemon's picker/refusal logic and the browser's rings and rail. A pool gated `none` is excluded from the
5
5
  // no-model case: visible to the account, never a gate on a turn.
6
6
 
7
- // 100 = exhaustion (the call will be refused), not the browser's 90% warning threshold (usageStatus SPENT_PERCENT).
7
+ // 100 = exhaustion (the call will be refused). The one spent line: the daemon's pickers and every surface read it, and
8
+ // anything short of it still has room.
8
9
  export const SPENT_UTILIZATION = 100;
9
10
 
10
11
  export interface ModelRef {
@@ -132,3 +133,152 @@ export const windowLive = (window: UsageWindow, measuredAt: number, now: number)
132
133
  const period = windowPeriod(window);
133
134
  return period === undefined || measuredAt + period.seconds * 1000 > now;
134
135
  };
136
+
137
+ // Whether an account can serve a turn: the one rule every picker and surface reads (AccountState). The daemon runs it and
138
+ // publishes the verdict on each account row; the editor runs it only on a row from a daemon older than that field.
139
+
140
+ /** Everything the serviceability rule reads of one account, as the account rows already carry it. */
141
+ export interface ServiceFacts {
142
+ // The daemon's key: a native account's id, a routed credential's auth-file name.
143
+ readonly account: string;
144
+ readonly needsReauth?: boolean | undefined;
145
+ // Why the sign-in needs renewing, where the provider said.
146
+ readonly detail?: string | undefined;
147
+ readonly seatRefusal?: string | undefined;
148
+ readonly cooling?: { readonly until?: number | undefined; readonly reason?: string | undefined } | undefined;
149
+ readonly usage?: AccountUsage | undefined;
150
+ }
151
+
152
+ // A spent allowance is over once a reading with room lands after it; a refused credential once any reading does (the
153
+ // token worked). Nothing a reading says answers an entitlement refusal: a seatless account authenticates and reads fine.
154
+ const answers = (refusal: ProviderRefusal, facts: ServiceFacts): boolean => {
155
+ const measuredAt = facts.usage?.measuredAt;
156
+ if (refusal.kind === "entitlement" || measuredAt === undefined || measuredAt <= refusal.at) {
157
+ return false;
158
+ }
159
+ if (refusal.kind === "auth") {
160
+ return facts.needsReauth !== true;
161
+ }
162
+ const binding = bindingWindow(facts.usage);
163
+ return binding !== undefined && binding.utilization < SPENT_UTILIZATION;
164
+ };
165
+
166
+ /**
167
+ * How a provider's last refusal stands against everything read since, over every account the provider holds: `gone` once
168
+ * the account it named is disconnected, `answered` once that account (or, for a refusal naming none, any account) has a
169
+ * later reading that contradicts it, else `standing`. An empty list is one not loaded yet, so the refusal stands.
170
+ */
171
+ export const refusalVerdict = (refusal: ProviderRefusal, accounts: readonly ServiceFacts[]): "standing" | "answered" | "gone" => {
172
+ const named = accounts.filter((facts) => facts.account === refusal.account);
173
+ if (refusal.account !== undefined && named.length === 0) {
174
+ return accounts.length === 0 ? "standing" : "gone";
175
+ }
176
+ return (refusal.account === undefined ? accounts : named).some((facts) => answers(refusal, facts)) ? "answered" : "standing";
177
+ };
178
+
179
+ // The account reopens for this question when the pools holding it shut do: all of them for a model's own question (or
180
+ // the pools gating every model), the first to reopen when only per-model slices are full. Unknown if any is unpublished.
181
+ const reopening = (full: readonly UsageWindow[], first: boolean): number | undefined => {
182
+ const resets = full.flatMap((window) => (window.resetsAt === undefined ? [] : [window.resetsAt]));
183
+ return resets.length === full.length && resets.length > 0 ? (first ? Math.min(...resets) : Math.max(...resets)) : undefined;
184
+ };
185
+
186
+ const spent = (reopensAt: number | undefined): AccountState => (reopensAt === undefined ? { kind: "spent" } : { kind: "spent", reopensAt });
187
+
188
+ /**
189
+ * The plan-limit half of the verdict, at the contract's one spent line. With a model, the pools that model spends: spent
190
+ * once any is full, else the room the fullest leaves. With none, whether the account can serve SOME turn: spent once a
191
+ * pool gating every model is full, or every per-model slice is; a full slice alone (Opus at 100%) leaves the other
192
+ * models their room, which is then read off the fullest pool still open.
193
+ */
194
+ export const headroomState = (usage: AccountUsage | undefined, model?: ModelRef): AccountState => {
195
+ const windows = gatingWindows(usage, model);
196
+ if (windows.length === 0) {
197
+ return { kind: "unknown" };
198
+ }
199
+ const full = windows.filter((window) => window.utilization >= SPENT_UTILIZATION);
200
+ const open = windows.filter((window) => window.utilization < SPENT_UTILIZATION);
201
+ if (model !== undefined || full.some((window) => window.gates === "all") || open.length === 0) {
202
+ const holding = model !== undefined ? full : full.some((window) => window.gates === "all") ? full.filter((window) => window.gates === "all") : full;
203
+ if (holding.length > 0) {
204
+ return spent(reopening(holding, model === undefined && !holding.some((window) => window.gates === "all")));
205
+ }
206
+ }
207
+ return { kind: "ready", room: SPENT_UTILIZATION - Math.max(...open.map((window) => window.utilization)) };
208
+ };
209
+
210
+ // A standing limit refusal reads its pool as full, since a polled reading freezes the moment a pool actually empties: the
211
+ // pool the refused turn's model spends, else the one asked about, else the tightest. Never another model's allowance.
212
+ const refusedPool = (usage: AccountUsage | undefined, refusal: ProviderRefusal, model: ModelRef | undefined): AccountState => {
213
+ const refusedModel = refusal.model === undefined ? model : { id: refusal.model };
214
+ const binding = bindingWindow(usage, refusedModel);
215
+ if (usage === undefined || binding === undefined) {
216
+ // No pool to pin: the refusal itself is the evidence, unless it was about a different model than this question.
217
+ const sameModel = refusal.model === undefined || model === undefined || refusal.model === model.id;
218
+ return sameModel ? { kind: "spent" } : headroomState(usage, model);
219
+ }
220
+ const pinned = usage.windows.map((window) => (window === binding ? { ...window, utilization: Math.max(window.utilization, SPENT_UTILIZATION) } : window));
221
+ return headroomState({ ...usage, windows: pinned }, model);
222
+ };
223
+
224
+ /**
225
+ * One account's verdict. `refusal` is the provider's last refusal only when it still stands and covers this account
226
+ * (serviceStates decides that). Precedence: a revoked sign-in outranks a lost seat (reconnecting fixes one, nothing here
227
+ * fixes the other), both outrank a standing refusal, then a translator bench, then the plan limits.
228
+ */
229
+ export const serviceState = (facts: ServiceFacts, refusal?: ProviderRefusal, model?: ModelRef, now: number = Date.now()): AccountState => {
230
+ if (facts.needsReauth === true) {
231
+ return { kind: "blocked", fix: "reconnect", reason: facts.detail ?? "sign-in expired" };
232
+ }
233
+ if (facts.seatRefusal !== undefined) {
234
+ return { kind: "blocked", fix: "admin", reason: facts.seatRefusal };
235
+ }
236
+ if (refusal?.kind === "auth") {
237
+ return { kind: "blocked", fix: "reconnect", reason: refusal.message };
238
+ }
239
+ if (refusal?.kind === "entitlement") {
240
+ return { kind: "blocked", fix: "admin", reason: refusal.message };
241
+ }
242
+ const cooling = facts.cooling;
243
+ // A bench with no instant is one no wait lifts (a Google account with no project): somebody has to connect again.
244
+ if (cooling !== undefined && cooling.until === undefined) {
245
+ return { kind: "blocked", fix: "reconnect", reason: cooling.reason ?? "benched by the translator" };
246
+ }
247
+ if (cooling?.until !== undefined && cooling.until * 1000 > now) {
248
+ return { kind: "blocked", fix: "wait", reason: cooling.reason ?? "cooling down", until: cooling.until };
249
+ }
250
+ return refusal?.kind === "limit" ? refusedPool(facts.usage, refusal, model) : headroomState(facts.usage, model);
251
+ };
252
+
253
+ /** Every account of one provider, judged together, since a refusal naming no account is answered by any of them. */
254
+ export const serviceStates = (
255
+ accounts: readonly ServiceFacts[],
256
+ refusal: ProviderRefusal | undefined,
257
+ model?: ModelRef,
258
+ now: number = Date.now(),
259
+ ): ReadonlyMap<string, AccountState> => {
260
+ const standing = refusal !== undefined && refusalVerdict(refusal, accounts) === "standing" ? refusal : undefined;
261
+ return new Map(
262
+ accounts.map((facts) => [
263
+ facts.account,
264
+ serviceState(facts, standing !== undefined && (standing.account ?? facts.account) === facts.account ? standing : undefined, model, now),
265
+ ]),
266
+ );
267
+ };
268
+
269
+ // Worst last: proven room, no reading either way, known spent, and nothing a turn can run on.
270
+ const PREFERENCE: Record<AccountState["kind"], number> = { ready: 0, unknown: 1, spent: 2, blocked: 3 };
271
+
272
+ /** The account an unnamed turn runs on: ready by most room, then unmeasured, then spent, blocked only when that is all there is. Ties keep the caller's order. */
273
+ export const preferredAccount = <T extends { readonly state: AccountState }>(entries: readonly T[]): T | undefined =>
274
+ entries.reduce<T | undefined>((best, next) => {
275
+ if (best === undefined) {
276
+ return next;
277
+ }
278
+ const tier = PREFERENCE[next.state.kind] - PREFERENCE[best.state.kind];
279
+ return tier < 0 || (tier === 0 && next.state.kind === "ready" && best.state.kind === "ready" && next.state.room > best.state.room) ? next : best;
280
+ }, undefined);
281
+
282
+ /** The roomiest account with proven room, or none: what a move off a refused account may land on. Ties keep the caller's order. */
283
+ export const roomiestAccount = <T extends { readonly state: AccountState }>(entries: readonly T[]): T | undefined =>
284
+ preferredAccount(entries.filter((entry) => entry.state.kind === "ready"));
@@ -1,5 +1,6 @@
1
- import type { RequestField, TranscriptRequests, TranscriptRow } from "../events/transcript.js";
1
+ import { REQUEST_FIELDS, type RequestField, type TranscriptRequests, type TranscriptRow } from "../events/transcript.js";
2
2
  import type { AgentReply } from "../schemas/providers/plan-limits.js";
3
+ import { startedChildCard } from "../events/child-run.js";
3
4
 
4
5
  // How a reply settles the card it answers: one rule used both by the fold and by a chat freezing its own card
5
6
  // optimistically before the frame returns. No reply (turn stopped or died) reads as cancelled for every card; a reply
@@ -22,7 +23,7 @@ export const settledRequests = (cards: TranscriptRequests, reply: AgentReply | u
22
23
  }
23
24
  if (permission !== undefined) {
24
25
  out.permission = {
25
- ...permission,
26
+ ...startedChildCard(permission, reply),
26
27
  status:
27
28
  reply?.kind !== "permission"
28
29
  ? "cancelled"
@@ -65,16 +66,7 @@ export const settledRequests = (cards: TranscriptRequests, reply: AgentReply | u
65
66
  export const cancelledRequests = (cards: TranscriptRequests): TranscriptRequests => {
66
67
  const out: Cards = {};
67
68
  let changed = false;
68
- for (const field of [
69
- "plan",
70
- "question",
71
- "permission",
72
- "browserHelp",
73
- "terminalHelp",
74
- "capabilityOffer",
75
- "paymentOffer",
76
- "credentialOffer",
77
- ] as const) {
69
+ for (const field of REQUEST_FIELDS) {
78
70
  const card = cards[field];
79
71
  if (card === undefined) {
80
72
  continue;
@@ -24,6 +24,12 @@ describe("reserved MCP server names", () => {
24
24
  });
25
25
 
26
26
  describe("collidesWithReservedServer", () => {
27
+ // Derived from the manifest schema's meanings: a cli card mints a server when it serves tools, a device and a
28
+ // connected browser always do, and `mcp` is the core kind that is its own endpoint.
29
+ test("the minting kinds are exactly the ones whose id becomes a server name", () => {
30
+ expect([...MCP_SERVER_MINTING_KINDS].toSorted()).toEqual(["cli", "device", "mcp", "webext"]);
31
+ });
32
+
27
33
  test("a kind whose id becomes a server name may not reuse a reserved one", () => {
28
34
  for (const kind of MCP_SERVER_MINTING_KINDS) {
29
35
  for (const name of RESERVED_MCP_SERVER_NAMES) {
@@ -1,4 +1,5 @@
1
- // The MCP servers the daemon mounts in its OWN process, and whether each carries content from outside this container.
1
+ import { contributedServerMintingKinds } from "@intentic/extension-manifest";
2
+ // The MCP servers the daemon mounts under its own names, and whether each carries content from outside this container.
2
3
  // One source of truth: the outside-content guard's exemption list is the `control` subset of this, and a capability
3
4
  // whose id would become `mcp__<id>__…` is refused when the id is one of these, so nothing the owner configures can
4
5
  // shadow a daemon server. A daemon server's name is what reaches the model as the `mcp__<name>__` prefix.
@@ -8,9 +9,9 @@
8
9
  // - outside: the server carries content from beyond the container (a web page, a provider's verbatim sentence)
9
10
  export type ServerProvenance = "control" | "outside";
10
11
 
11
- // Every server agent/run/agent.ts and agent/run/harness/harness-servers.ts mount themselves; the browser router's two
12
- // names (web, browser) come in through `...turn.browser.servers`, so they are named here rather than discovered by the
13
- // text scan. A server added anywhere else without a row here fails the conformance test in the sandbox guard package.
12
+ // Every server agent/run/agent.ts and agent/run/harness/harness-servers.ts mount themselves, plus the browser router's two
13
+ // names (web, browser), which every turn mounts on its lease at the daemon's MCP door (browser-tools.ts) and which the
14
+ // text scan reads from their constants. A server added anywhere else without a row here fails the conformance test in the sandbox guard package.
14
15
  export const DAEMON_MCP_SERVERS: Readonly<Record<string, ServerProvenance>> = {
15
16
  ui: "control", // agent.ts: AskUserQuestion
16
17
  accounts: "control", // agent.ts: the account roster and the credential typists
@@ -22,8 +23,8 @@ export const DAEMON_MCP_SERVERS: Readonly<Record<string, ServerProvenance>> = {
22
23
  watch: "control", // harness: condition watches
23
24
  deps: "control", // harness: dependency readiness
24
25
  diagnostics: "outside", // harness: two tools relay a provider's own sentence verbatim
25
- web: "outside", // harness browser: the anonymous router; the page is the internet
26
- browser: "outside", // harness browser: the signed-in router; the page is the internet
26
+ web: "outside", // the turn's mounts: the anonymous browser router; the page is the internet
27
+ browser: "outside", // the turn's mounts: the signed-in browser router; the page is the internet
27
28
  };
28
29
 
29
30
  // Every daemon-mounted server name, the set a capability id may not collide with.
@@ -37,10 +38,12 @@ export const CONTROL_MCP_SERVERS: ReadonlySet<string> = new Set(
37
38
  .map(([name]) => name),
38
39
  );
39
40
 
40
- // Capability kinds whose id becomes an MCP server name for the turn (`mcp` → its own endpoint; `device`/`webext` → the
41
- // loopback peer bridge), and so whose id must not collide with a daemon server. Other kinds mint accounts, providers or
42
- // infrastructure, never a `mcp__<id>__` server keyed by the id.
43
- export const MCP_SERVER_MINTING_KINDS: ReadonlySet<string> = new Set(["mcp", "device", "webext"]);
41
+ // Capability kinds whose id becomes an MCP server name for the turn, and so whose id must not collide with a daemon
42
+ // server: `mcp`, a core kind that is its own endpoint, and every contributable kind the manifest schema says mints one
43
+ // (a device's and a connected browser's peer bridge, a cli card that serves tools), read off the schema's own meaning
44
+ // (`contributedServerMintingKinds`), so a kind that starts minting is refused here without a second list. Other kinds
45
+ // mint accounts, providers or infrastructure, never a `mcp__<id>__` server keyed by the id.
46
+ export const MCP_SERVER_MINTING_KINDS: ReadonlySet<string> = new Set(["mcp", ...contributedServerMintingKinds()]);
44
47
 
45
48
  // Whether adding or renaming a capability of this kind to this id would shadow a daemon server. The one predicate the
46
49
  // capability routes consult, so the refusal and this list can't drift.
@@ -5,6 +5,9 @@
5
5
  // A set rather than a run of case labels: what they share is a fact about the turn, not a shape.
6
6
  const TURNED_AWAY: ReadonlySet<string> = new Set([
7
7
  "claude-reauth",
8
+ // An organisation turned the account's Claude Code access off, or no account of the provider can serve: the next
9
+ // turn is routed off it (the daemon's blocked-account route), so the words wait for it rather than being spent.
10
+ "claude-not-entitled",
8
11
  "unknown-command",
9
12
  "context-window-too-small",
10
13
  "sandbox-memory-low",
@@ -3,19 +3,21 @@
3
3
  // JavaScript unless the responder allows it — so the verdict rides an exposed header and the edge CORS-allows its own
4
4
  // errors. Ingress writes it, the editor's connection machine reads it; both sides import this file so they cannot drift.
5
5
 
6
+ import type { EdgeVerdict } from "../front/generated/browser-wire.js";
7
+
6
8
  export const EDGE_VERDICT_HEADER = "x-intentic-edge";
7
9
 
8
- export type EdgeVerdict =
9
- // No tunnel is registered for this sandbox: its container is not running, or the machine it runs on is off.
10
- // Retrying is right — a container that comes back redials within seconds.
11
- | "no-tunnel"
12
- // The platform has no such sandbox. The row is deleted, no tunnel for this id will ever be accepted again, and
13
- // nothing the reader waits for can change that.
14
- | "unknown-sandbox"
15
- // A tunnel was held and the forward failed mid-flight: the box went away between being routed to and answering.
16
- | "dropped";
17
-
18
- const VERDICTS: ReadonlySet<string> = new Set(["no-tunnel", "unknown-sandbox", "dropped"]);
10
+ // The verdicts themselves are the edge's (browser-wire's `EdgeVerdict`), each documented there:
11
+ // - no-tunnel: no tunnel is registered for this sandbox, and retrying is right, since a container that comes back redials
12
+ // within seconds;
13
+ // - unknown-sandbox: the platform has no such sandbox, and nothing the reader waits for can change that;
14
+ // - dropped: a tunnel was held and the forward failed mid-flight.
15
+ export type { EdgeVerdict };
16
+
17
+ // Every verdict, so a new one in the Rust enum fails this file's typecheck until it is read here too.
18
+ const VERDICTS: ReadonlySet<string> = new Set(
19
+ Object.keys({ "no-tunnel": true, "unknown-sandbox": true, dropped: true } satisfies Record<EdgeVerdict, true>),
20
+ );
19
21
 
20
22
  // A header value only counts when it is one of ours: a 502 from anything else on the path (a corporate proxy, a CDN)
21
23
  // carries no verdict and must not be read as one.
@@ -44,6 +44,19 @@
44
44
  { "host": "127.0.0.1:8788", "owner": null },
45
45
  { "host": "", "owner": null }
46
46
  ],
47
+ "bulk": [
48
+ "GET /diff/raw",
49
+ "GET /workspace/raw",
50
+ "GET /workspace/media",
51
+ "POST /workspace/upload",
52
+ "POST /workspace/upload-archive",
53
+ "GET /system/sync/ssh",
54
+ "GET /bundles/download",
55
+ "GET /extensions/{id}/bundle",
56
+ "GET /system/runners/git/{repo}/info/refs",
57
+ "POST /system/runners/git/{repo}/git-upload-pack",
58
+ "POST /system/runners/git/{repo}/git-receive-pack"
59
+ ],
47
60
  "lanes": [
48
61
  { "host": "sandbox-abcdef012345.sbx.example.test", "method": "GET", "path": "/workspace/media?path=a.mp4", "lane": "bulk" },
49
62
  { "host": "sandbox-abcdef012345.sbx.example.test", "method": "POST", "path": "/workspace/upload?path=a", "lane": "bulk" },
@@ -63,8 +63,9 @@ export const hostOwnerId = (host: string): string | undefined => {
63
63
 
64
64
  // Wire constants
65
65
 
66
- // Tunnel door on the ingress; versioned so a v2 session shape adds a new path instead of replacing this one.
67
- export const INGRESS_TUNNEL_PATH = "/tunnel/v1";
66
+ // Tunnel door on the ingress (the tunnel crate's TUNNEL_PATH); versioned, so a new session shape adds a path. `/tunnel/v1`
67
+ // is the edge's alone to answer now, for fronts that predate this one.
68
+ export const INGRESS_TUNNEL_PATH = "/tunnel/v2";
68
69
 
69
70
  // Grant travels as a header on the tunnel upgrade; this connection is never a browser's.
70
71
  export const INGRESS_GRANT_HEADER = "x-intentic-grant";
@@ -77,9 +78,12 @@ export const ENV_SANDBOX_GRANT = "SANDBOX_GRANT";
77
78
  // - Register: verify the grant offline; if PLATFORM_URL is set, check the sandbox exists (GET /api/reachability/<id>)
78
79
  // and cache the answer, failing open if the platform doesn't respond. A 404 refuses the tunnel.
79
80
  // - Displacement: a new tunnel for an id closes the old session (code 4001) and takes the registration.
80
- // - Lanes: a tunnel naming the bulk lane (tunnel-lanes.ts) is held beside the interactive one and displaces only its
81
- // own kind; only the interactive lane is the sandbox's home in the cluster.
82
- // - Liveness: WebSocket ping every 15s; a peer silent for 45s is unregistered.
81
+ // - Streams: every exchange is a stream of its own carrying HTTP/1.1 (yamux over a WebSocket, or QUIC). Over TCP a
82
+ // transfer rides the bulk socket: a preview's request, or a route the front announced (tunnel-bulk.ts); over QUIC a
83
+ // stream that sends a megabyte without pausing yields to the others.
84
+ // - Liveness: the front pings every 15s; a peer silent for 45s is unregistered.
83
85
  // - Routing: host maps via hostOwnerId to a registered tunnel; no tunnel answers 502 naming the sandbox label, with
84
86
  // the verdict header and CORS of edge-verdict.ts, and admits the preflight of the request it is about to refuse.
85
87
  // - The tunnel door and any host without a sandbox id are served directly by the ingress, never routed.
88
+ // - `/health` lists the tunnel doors the build serves (`"doors":["/tunnel/v1","/tunnel/v2"]`); a sandbox image moves
89
+ // onto a tag sandboxes pull only once the live edge lists INGRESS_TUNNEL_PATH (_tools/scripts/image/require-edge-door.sh).
@@ -81,11 +81,6 @@ export const RAW_ROUTES = {
81
81
  "GET /extensions/{id}/bundle": { lane: "bulk" },
82
82
  // An extension backend's own namespace, proxied verbatim.
83
83
  "ALL /x/*": {},
84
- // An extension card's MCP endpoint (its manifest's `mcp`), forwarded to the backend host; the per-boot token its
85
- // handler checks is the one the turn's tool config carries.
86
- "POST /mcp/extensions/{id}": { auth: "door" },
87
- "GET /mcp/extensions/{id}": { auth: "door" },
88
- "DELETE /mcp/extensions/{id}": { auth: "door" },
89
84
  // The `capabilities` CLI: discovery by name, and the ask that parks on an owner-decided card.
90
85
  "GET /capabilities/connectable": { floor: "maintainer", agent: true, control: "never" },
91
86
  "POST /capabilities/ask": { floor: "maintainer", agent: true, control: "never" },
@@ -107,11 +102,12 @@ export const RAW_ROUTES = {
107
102
  "GET /fleet": { agent: true },
108
103
  "POST /fleet/message": { agent: true },
109
104
  "GET /fleet/{handle}": { agent: true },
110
- // An extension gateway's realtime-listener control, reached with the panel token.
111
- "GET /listeners/{provider}/state": {},
112
- "POST /listeners/{provider}/dispatch": {},
113
- "POST /listeners/{provider}/failure": {},
114
- "POST /listeners/{provider}/status": {},
105
+ // An extension gateway's realtime-listener control: /state hands back its connectors' stored credentials, so only the
106
+ // extension token of the extension declaring that `listener.provider` reaches them, checked in the handler too.
107
+ "GET /listeners/{provider}/state": { floor: "maintainer", panel: false, control: "never" },
108
+ "POST /listeners/{provider}/dispatch": { panel: false, control: "never" },
109
+ "POST /listeners/{provider}/failure": { panel: false, control: "never" },
110
+ "POST /listeners/{provider}/status": { panel: false, control: "never" },
115
111
  // The CI webhook receiver, gated by the per-sandbox webhook secret.
116
112
  "POST /ci/webhook/{host}": { auth: "door" },
117
113
  // Below maintainer a pairing is capped to port-mirror, so a collaborator may mint a preview tunnel.
@@ -123,27 +119,20 @@ export const RAW_ROUTES = {
123
119
  "DELETE /system/hosts/{id}": { control: "never" },
124
120
  // A peer's socket reconnects on its own backoff, so it answers before boot.
125
121
  "GET /system/hosts/connect": { auth: "door", beforeBoot: true, control: "never" },
126
- // The MCP bridge onto a peer, carrying the per-boot bridge token its handler checks.
127
- "POST /mcp/hosts/{id}": { auth: "door" },
128
- "GET /mcp/hosts/{id}": { auth: "door" },
129
- "DELETE /mcp/hosts/{id}": { auth: "door" },
130
122
  "POST /system/webext/pair": { control: "never" },
131
123
  "POST /system/webext/enroll": { auth: "door", control: "never" },
132
124
  "GET /system/webext": { control: "never" },
133
125
  "DELETE /system/webext/{id}": { control: "never" },
134
126
  "GET /system/webext/connect": { auth: "door", beforeBoot: true, control: "never" },
135
- "POST /mcp/webext/{id}": { auth: "door" },
136
- "GET /mcp/webext/{id}": { auth: "door" },
137
- "DELETE /mcp/webext/{id}": { auth: "door" },
138
127
  "POST /system/runners/pair": { control: "never" },
139
128
  "POST /system/runners/enroll": { auth: "door", control: "never" },
140
129
  "GET /system/runners": { control: "never" },
141
130
  "DELETE /system/runners/{id}": { control: "never" },
142
131
  "GET /system/runners/connect": { auth: "door", beforeBoot: true, control: "never" },
143
- // A turn's browser routers, hosted in the daemon; each carries its own per-turn bearer, checked by the handler.
144
- "POST /mcp/browser/{id}": { auth: "door", control: "never" },
145
- "GET /mcp/browser/{id}": { auth: "door", control: "never" },
146
- "DELETE /mcp/browser/{id}": { auth: "door", control: "never" },
132
+ // Every MCP server the daemon hosts for a turn (its browser routers, the machines and browsers it was granted, its
133
+ // extension cards' endpoints), by server name: the handler checks the conversation's mount bearer the turn's tool
134
+ // config carries, and that the running turn mounted that name.
135
+ "ALL /mcp/{mount}": { auth: "door", control: "never" },
147
136
  // A connected browser's credential doors, on its durable token: a sign-in moves in (`session`) or out (`lend`).
148
137
  "POST /system/webext/session": { auth: "door", control: "never" },
149
138
  "POST /system/webext/lend": { auth: "door", control: "never" },
@@ -33,11 +33,46 @@ export interface RouteMeta {
33
33
  // Withheld from the panel token, which reaches every other route: it puts a stored credential in motion.
34
34
  readonly panel?: false;
35
35
  readonly control?: ControlReach;
36
- // Rides the tunnel's bulk lane: a transfer, kept off the connection every keystroke and call waits on.
36
+ // A transfer: announced to the edge through the front (tunnel-bulk.ts), which sends it down the tunnel's bulk socket,
37
+ // off the connection every keystroke and call waits on.
37
38
  readonly lane?: "bulk";
38
39
  // Served by intentic-front itself (_sandbox/front, term/): the daemon only answers the front's question about it.
39
40
  readonly front?: true;
40
41
  }
41
42
 
43
+ // The floor a route gets when it declares none: a read (GET, HEAD) floors at viewer, anything else (`ALL` included) at
44
+ // maintainer, so a route nobody classified can under-serve, never over-grant.
45
+ export const defaultFloor = (method: string): MemberRole => (method === "GET" || method === "HEAD" ? "viewer" : "maintainer");
46
+
47
+ // The fields that say how a request travels, not who may send it; every other RouteMeta field decides reach.
48
+ type TransportField = "lane" | "front";
49
+
50
+ // Every field that decides who reaches a route, with its default resolved, so an absent field and a declared one read
51
+ // alike and loosening either is a changed value: what contract.lock.json records per route (`access:METHOD /path`),
52
+ // where the lock check and the push gate see it. Keyed by the interface: a RouteMeta field added above fails this
53
+ // package's typecheck until it is read here or named a TransportField.
54
+ const ACCESS = {
55
+ auth: (meta) => meta.auth ?? "session",
56
+ embedded: (meta) => meta.embedded === true,
57
+ beforeBoot: (meta) => meta.beforeBoot === true,
58
+ stream: (meta) => meta.stream === true,
59
+ floor: (meta, method) => meta.floor ?? defaultFloor(method),
60
+ guest: (meta) => meta.guest === true,
61
+ attachmentFloor: (meta) => meta.attachmentFloor ?? "none",
62
+ enrolment: (meta) => meta.enrolment === true,
63
+ agent: (meta) => meta.agent === true,
64
+ sync: (meta) => meta.sync ?? "none",
65
+ panel: (meta) => meta.panel !== false,
66
+ // `floor`: no reach of its own, the control rungs read the route's floor alone.
67
+ control: (meta) => meta.control ?? "floor",
68
+ } as const satisfies { readonly [K in Exclude<keyof RouteMeta, TransportField>]-?: (meta: RouteMeta, method: string) => string | boolean };
69
+
70
+ export type RouteAccess = { readonly [K in keyof typeof ACCESS]: ReturnType<(typeof ACCESS)[K]> };
71
+
72
+ // One route's reach as its declaration states it, every default filled in.
73
+ export const routeAccess = (method: string, meta: RouteMeta): RouteAccess =>
74
+ // SAFETY: every entry is one of ACCESS's own keys paired with what its reader returned, which RouteAccess spells.
75
+ Object.fromEntries(Object.entries(ACCESS).map(([field, read]) => [field, read(meta, method)])) as RouteAccess;
76
+
42
77
  // The builder every sandbox procedure starts from, so each carries a RouteMeta and whatever it leaves out defaults.
43
78
  export const procedure = oc.$meta<RouteMeta>({});
@@ -0,0 +1,37 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { servedRoute } from "./routes.js";
3
+ import { RAW_ROUTE_LIST } from "./raw-routes.js";
4
+ import { hostOwnerId } from "./ingress-contract.js";
5
+ import { tunnelBulkRoutes } from "./tunnel-bulk.js";
6
+
7
+ // The shared fixture the edge reads too (the tunnel crate's bulk.rs): `bulk` is what the daemon announces, and every
8
+ // case is the lane the daemon's own router gives the request, which the edge's reading of `bulk` must match.
9
+ // SAFETY: the fixture is this repository's own file, and both tests below fail on a key it lacks.
10
+ const FIXTURE = JSON.parse(readFileSync(new URL("./ingress-contract.fixture.json", import.meta.url), "utf8")) as {
11
+ readonly bulk: readonly string[];
12
+ readonly lanes: readonly { readonly host: string; readonly method: string; readonly path: string; readonly lane: "interactive" | "bulk" }[];
13
+ };
14
+
15
+ // A preview's dev server rides bulk; a daemon request rides its route's lane, the route found as the router finds it.
16
+ const laneOf = (host: string, method: string, path: string): "interactive" | "bulk" => {
17
+ const id = hostOwnerId(host);
18
+ const label = host.split(":")[0]?.split(".")[0] ?? "";
19
+ if (id === undefined || label !== `sandbox-${id}`) {
20
+ return "bulk";
21
+ }
22
+ return servedRoute(RAW_ROUTE_LIST, [], method, path)?.meta.lane ?? "interactive";
23
+ };
24
+
25
+ test("the fixture's announcement is what the daemon announces today", () => {
26
+ expect(tunnelBulkRoutes()).toEqual([...FIXTURE.bulk]);
27
+ for (const route of tunnelBulkRoutes()) {
28
+ expect(route).toMatch(/^[A-Z]+ \/\S*$/);
29
+ }
30
+ });
31
+
32
+ test("every request the shared fixture names rides the lane the daemon's router gives it", () => {
33
+ expect(FIXTURE.lanes.length).toBeGreaterThan(20);
34
+ for (const { host, method, path, lane } of FIXTURE.lanes) {
35
+ expect({ host, method, path, lane: laneOf(host, method, path) }).toEqual({ host, method, path, lane });
36
+ }
37
+ });
@@ -0,0 +1,7 @@
1
+ import { RAW_ROUTE_LIST } from "./raw-routes.js";
2
+
3
+ // What the daemon announces to the edge as its transfers (RouteMeta `lane`), handed to the front with the tunnel's door
4
+ // and carried on each of its sockets' upgrades (the tunnel crate's `x-intentic-bulk`), so the edge sends a transfer
5
+ // down the bulk socket and a keystroke never queues behind it on one TCP connection. The edge compiles in nothing of it.
6
+ export const tunnelBulkRoutes = (): string[] =>
7
+ RAW_ROUTE_LIST.filter(({ meta }) => meta.lane === "bulk").map(({ method, path }) => `${method} ${path}`);
@@ -0,0 +1,29 @@
1
+ import { atomicToUsd, USDC_NETWORKS, usdcNetworkOf, usdToAtomic } from "./x402.js";
2
+
3
+ it("converts USD and atomic units without floats, in both directions", () => {
4
+ expect(usdToAtomic("1.00")).toBe(1_000_000n);
5
+ expect(usdToAtomic("0.1")).toBe(100_000n);
6
+ expect(usdToAtomic("0.000001")).toBe(1n);
7
+ // The classic float trap: 0.1 + 0.2 in binary floating point is not 0.3, and money must not care.
8
+ expect(usdToAtomic("0.1") + usdToAtomic("0.2")).toBe(usdToAtomic("0.3"));
9
+ expect(atomicToUsd(1_000_000n)).toBe("1.00");
10
+ expect(atomicToUsd(100_000n)).toBe("0.10");
11
+ expect(atomicToUsd(1n)).toBe("0.000001");
12
+ });
13
+
14
+ it("reads a bare whole or fraction, cuts past the sixth decimal, and writes zero as cents", () => {
15
+ expect(usdToAtomic("5")).toBe(5_000_000n);
16
+ expect(usdToAtomic(".5")).toBe(500_000n);
17
+ expect(usdToAtomic("0.0000019")).toBe(1n);
18
+ expect(atomicToUsd(0n)).toBe("0.00");
19
+ expect(atomicToUsd(12_345_670n)).toBe("12.34567");
20
+ });
21
+
22
+ it("knows USDC on Base and Base Sepolia by CAIP-2 id, and nothing else", () => {
23
+ expect(USDC_NETWORKS.map(({ network, chainId, asset }) => [network, chainId, asset])).toEqual([
24
+ ["eip155:8453", 8453, "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"],
25
+ ["eip155:84532", 84532, "0x036CbD53842c5426634e7929541eC2318f3dCF7e"],
26
+ ]);
27
+ expect(usdcNetworkOf("eip155:84532")?.v1Network).toBe("base-sepolia");
28
+ expect(usdcNetworkOf("base")).toBeUndefined();
29
+ });
@@ -0,0 +1,61 @@
1
+ // The money side of x402 every tier states alike: which USDC contracts a payment may move, and dollars as atomic units.
2
+
3
+ // One chain's USDC: token contract, EIP-712 domain defaults, explorer; an asset not on the list below is refused.
4
+ export interface UsdcNetwork {
5
+ // CAIP-2 ("eip155:8453"), the v2 vocabulary and the wallet config's.
6
+ readonly network: string;
7
+ // How v1 challenges spell the same chain ("base"), matched on parse, echoed on the v1 retry header.
8
+ readonly v1Network: string;
9
+ readonly chainId: number;
10
+ readonly asset: string;
11
+ // EIP-712 domain fallbacks; `extra.{name,version}` on the challenge wins when present.
12
+ readonly domainName: string;
13
+ readonly domainVersion: string;
14
+ readonly label: string;
15
+ readonly explorer: string;
16
+ readonly rpc: string;
17
+ }
18
+
19
+ export const USDC_NETWORKS: readonly UsdcNetwork[] = [
20
+ {
21
+ network: "eip155:8453",
22
+ v1Network: "base",
23
+ chainId: 8453,
24
+ asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
25
+ domainName: "USD Coin",
26
+ domainVersion: "2",
27
+ label: "Base",
28
+ explorer: "https://basescan.org/tx/",
29
+ rpc: "https://mainnet.base.org",
30
+ },
31
+ {
32
+ network: "eip155:84532",
33
+ v1Network: "base-sepolia",
34
+ chainId: 84532,
35
+ asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
36
+ domainName: "USDC",
37
+ domainVersion: "2",
38
+ label: "Base Sepolia (test)",
39
+ explorer: "https://sepolia.basescan.org/tx/",
40
+ rpc: "https://sepolia.base.org",
41
+ },
42
+ ];
43
+
44
+ export const usdcNetworkOf = (network: string): UsdcNetwork | undefined => USDC_NETWORKS.find((entry) => entry.network === network);
45
+
46
+ // Every amount is a bigint of atomic units, never a float; USD strings ("1.50") are the display and policy vocabulary.
47
+ export const USDC_DECIMALS = 6n;
48
+ const ATOMIC_PER_USD = 10n ** USDC_DECIMALS;
49
+
50
+ // Digits past the sixth decimal are cut, not rounded: a price is never read as more than was written.
51
+ export const usdToAtomic = (usd: string): bigint => {
52
+ const [whole, fraction = ""] = usd.split(".");
53
+ return BigInt(whole || "0") * ATOMIC_PER_USD + BigInt(fraction.padEnd(6, "0").slice(0, 6) || "0");
54
+ };
55
+
56
+ // At least cents ("1.50"), and every non-zero atomic digit after them ("0.000001").
57
+ export const atomicToUsd = (atomic: bigint): string => {
58
+ const whole = atomic / ATOMIC_PER_USD;
59
+ const fraction = (atomic % ATOMIC_PER_USD).toString().padStart(6, "0").replace(/0+$/, "");
60
+ return fraction === "" ? `${whole}.00` : `${whole}.${fraction.padEnd(2, "0")}`;
61
+ };