toolroll 0.9.54 → 0.9.56

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 (350) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +29 -12
  3. package/dist/action-ledger.d.ts +2 -4
  4. package/dist/action-ledger.js +2 -10
  5. package/dist/agent-onboard.js +4 -1
  6. package/dist/approval-policy.d.ts +5 -5
  7. package/dist/approval-policy.js +4 -4
  8. package/dist/approval-rules-ui.d.ts +3 -2
  9. package/dist/approval-rules-ui.js +17 -22
  10. package/dist/assignment-ui.d.ts +3 -2
  11. package/dist/assignment-ui.js +4 -11
  12. package/dist/backup-ui.d.ts +2 -1
  13. package/dist/backup-ui.js +16 -27
  14. package/dist/brand-mark.d.ts +3 -2
  15. package/dist/brand-mark.js +4 -4
  16. package/dist/browser/workspace.css +1 -1
  17. package/dist/browser/workspace.js +25 -25
  18. package/dist/browser-shell.d.ts +2 -1
  19. package/dist/browser-shell.js +9 -9
  20. package/dist/browser-workspace.d.ts +14 -15
  21. package/dist/browser-workspace.js +0 -3
  22. package/dist/budget-alerts.js +3 -3
  23. package/dist/chat-actions.d.ts +23 -18
  24. package/dist/chat-actions.js +110 -88
  25. package/dist/chat-ask.d.ts +2 -2
  26. package/dist/chat-ask.js +5 -5
  27. package/dist/chat-channel.d.ts +15 -15
  28. package/dist/chat-channel.js +20 -20
  29. package/dist/chat-controls.d.ts +0 -4
  30. package/dist/chat-controls.js +0 -1
  31. package/dist/chat-core.js +1 -1
  32. package/dist/chat-delivery-state.js +2 -2
  33. package/dist/chat-delivery.d.ts +2 -2
  34. package/dist/chat-delivery.js +27 -34
  35. package/dist/chat-flow.js +3 -3
  36. package/dist/chat-messages.d.ts +1 -1
  37. package/dist/chat-messages.js +4 -4
  38. package/dist/chat-migration.d.ts +1 -1
  39. package/dist/chat-migration.js +11 -11
  40. package/dist/chat-polish.d.ts +5 -4
  41. package/dist/chat-polish.js +9 -10
  42. package/dist/chat-rooms.js +3 -3
  43. package/dist/chat-warmth.d.ts +2 -2
  44. package/dist/check-levels-ui.d.ts +2 -1
  45. package/dist/check-levels-ui.js +26 -53
  46. package/dist/cli.d.ts +3 -1
  47. package/dist/cli.js +15 -10
  48. package/dist/coding-shipping-ui.d.ts +2 -1
  49. package/dist/coding-shipping-ui.js +6 -6
  50. package/dist/coding-ui.d.ts +3 -1
  51. package/dist/coding-ui.js +30 -29
  52. package/dist/contracts/chat-actions.d.ts +32 -27
  53. package/dist/contracts/chat-actions.js +29 -11
  54. package/dist/contracts/chat-content.d.ts +1 -1
  55. package/dist/contracts/chat-content.js +1 -1
  56. package/dist/contracts/chat-tables.js +4 -4
  57. package/dist/contracts/cli.d.ts +1 -3
  58. package/dist/contracts/console-api.d.ts +7 -7
  59. package/dist/contracts/console-api.js +13 -12
  60. package/dist/contracts/flow-propose.d.ts +3 -3
  61. package/dist/contracts/flow.d.ts +132 -125
  62. package/dist/contracts/flow.js +27 -12
  63. package/dist/contracts/health.d.ts +1 -3
  64. package/dist/contracts/lead-context.d.ts +1 -1
  65. package/dist/contracts/lead-context.js +2 -2
  66. package/dist/contracts/lead-tools.d.ts +20 -18
  67. package/dist/contracts/lead-tools.js +15 -15
  68. package/dist/contracts/stage-output.d.ts +1 -1
  69. package/dist/contracts/stage-output.js +1 -1
  70. package/dist/contracts/store-json.d.ts +23 -22
  71. package/dist/contracts/store-json.js +17 -17
  72. package/dist/contracts/{teammate-turn.d.ts → subagent-turn.d.ts} +5 -5
  73. package/dist/contracts/{teammate-turn.js → subagent-turn.js} +7 -7
  74. package/dist/control-ui.d.ts +5 -4
  75. package/dist/control-ui.js +9 -34
  76. package/dist/converse.d.ts +23 -23
  77. package/dist/converse.js +28 -28
  78. package/dist/coordinator-proposals.d.ts +2 -2
  79. package/dist/coordinator-proposals.js +1 -1
  80. package/dist/coordinator.js +1 -1
  81. package/dist/credentials-ui.d.ts +5 -4
  82. package/dist/credentials-ui.js +23 -24
  83. package/dist/demo-chat.d.ts +3 -3
  84. package/dist/demo-chat.js +29 -76
  85. package/dist/deprecations.d.ts +12 -0
  86. package/dist/deprecations.js +20 -0
  87. package/dist/desktop-update-gate.js +8 -4
  88. package/dist/desktop-update.js +6 -1
  89. package/dist/discord-chat.js +6 -6
  90. package/dist/discord-settings.d.ts +3 -2
  91. package/dist/discord-settings.js +17 -42
  92. package/dist/dispatch.d.ts +1 -1
  93. package/dist/dispatch.js +1 -1
  94. package/dist/envelope.js +10 -1
  95. package/dist/evidence-pack.d.ts +2 -1
  96. package/dist/evidence-pack.js +45 -49
  97. package/dist/export-ui.d.ts +7 -2
  98. package/dist/export-ui.js +4 -10
  99. package/dist/export.js +7 -7
  100. package/dist/flow-engine.d.ts +2 -2
  101. package/dist/flow-engine.js +21 -21
  102. package/dist/flow-gallery-ui.d.ts +4 -3
  103. package/dist/flow-gallery-ui.js +28 -43
  104. package/dist/flow-insights.d.ts +1 -1
  105. package/dist/flow-share.js +13 -8
  106. package/dist/flow-starters-ui.d.ts +2 -1
  107. package/dist/flow-starters-ui.js +12 -15
  108. package/dist/flow-steps.d.ts +5 -5
  109. package/dist/flow-steps.js +19 -19
  110. package/dist/flow-triggers.d.ts +2 -1
  111. package/dist/flow-triggers.js +17 -15
  112. package/dist/flows-cli.js +1 -1
  113. package/dist/flows-ui.d.ts +4 -3
  114. package/dist/flows-ui.js +38 -49
  115. package/dist/flows.js +21 -20
  116. package/dist/guides.js +112 -6
  117. package/dist/html.d.ts +76 -0
  118. package/dist/html.js +173 -0
  119. package/dist/integrations-ui.d.ts +2 -1
  120. package/dist/integrations-ui.js +19 -23
  121. package/dist/invoke.js +1 -1
  122. package/dist/kits-ui.d.ts +5 -4
  123. package/dist/kits-ui.js +22 -24
  124. package/dist/kits.d.ts +12 -12
  125. package/dist/kits.js +31 -31
  126. package/dist/knowledge-ui.d.ts +6 -5
  127. package/dist/knowledge-ui.js +26 -25
  128. package/dist/{mate-cli.d.ts → lead-cli.d.ts} +11 -11
  129. package/dist/{mate-cli.js → lead-cli.js} +38 -38
  130. package/dist/lead-commitments.js +4 -4
  131. package/dist/lead-context.d.ts +12 -2
  132. package/dist/lead-context.js +15 -17
  133. package/dist/{mate-contract.d.ts → lead-contract.d.ts} +3 -3
  134. package/dist/{mate-contract.js → lead-contract.js} +7 -6
  135. package/dist/{mate-doors.d.ts → lead-doors.d.ts} +6 -6
  136. package/dist/{mate-doors.js → lead-doors.js} +24 -24
  137. package/dist/lead-follow.d.ts +5 -5
  138. package/dist/lead-follow.js +9 -9
  139. package/dist/lead-people.d.ts +5 -5
  140. package/dist/lead-people.js +14 -14
  141. package/dist/{mate-progress.d.ts → lead-progress.d.ts} +9 -9
  142. package/dist/{mate-progress.js → lead-progress.js} +3 -3
  143. package/dist/{mate-tools.d.ts → lead-tools.d.ts} +19 -19
  144. package/dist/{mate-tools.js → lead-tools.js} +109 -101
  145. package/dist/{mate.d.ts → lead.d.ts} +27 -27
  146. package/dist/{mate.js → lead.js} +102 -102
  147. package/dist/ledger-view.d.ts +2 -1
  148. package/dist/ledger-view.js +27 -29
  149. package/dist/limits-ui.d.ts +3 -2
  150. package/dist/limits-ui.js +8 -8
  151. package/dist/live-bus.d.ts +50 -8
  152. package/dist/live-bus.js +133 -50
  153. package/dist/mcp-connect.d.ts +1 -1
  154. package/dist/mcp-connect.js +3 -3
  155. package/dist/mcp-core.js +1 -1
  156. package/dist/mcp-oauth.js +35 -36
  157. package/dist/memory-pass.d.ts +2 -2
  158. package/dist/memory-pass.js +7 -7
  159. package/dist/models-ui.d.ts +2 -1
  160. package/dist/models-ui.js +25 -35
  161. package/dist/monitoring-ui.d.ts +4 -3
  162. package/dist/monitoring-ui.js +13 -26
  163. package/dist/notification-rows.d.ts +2 -2
  164. package/dist/openrouter-models.d.ts +2 -1
  165. package/dist/openrouter-models.js +5 -9
  166. package/dist/operate.d.ts +3 -3
  167. package/dist/operate.js +46 -41
  168. package/dist/people-audit-ui.d.ts +2 -1
  169. package/dist/people-audit-ui.js +13 -13
  170. package/dist/policy-ui.d.ts +3 -2
  171. package/dist/policy-ui.js +17 -23
  172. package/dist/policy.d.ts +1 -1
  173. package/dist/principal.d.ts +1 -1
  174. package/dist/principal.js +2 -2
  175. package/dist/project-delete-ui.d.ts +4 -3
  176. package/dist/project-delete-ui.js +16 -28
  177. package/dist/project-delete.d.ts +1 -1
  178. package/dist/project-delete.js +26 -26
  179. package/dist/project-memory.js +1 -1
  180. package/dist/project-tools.d.ts +1 -1
  181. package/dist/project-tools.js +1 -1
  182. package/dist/pull-request-ui.d.ts +2 -1
  183. package/dist/pull-request-ui.js +17 -32
  184. package/dist/recipe-ui.d.ts +8 -7
  185. package/dist/recipe-ui.js +39 -32
  186. package/dist/release-adapters.d.ts +60 -0
  187. package/dist/release-adapters.js +435 -0
  188. package/dist/release-cli.d.ts +24 -0
  189. package/dist/release-cli.js +181 -0
  190. package/dist/release-gate.d.ts +80 -0
  191. package/dist/release-gate.js +150 -0
  192. package/dist/release-links.d.ts +29 -0
  193. package/dist/release-links.js +102 -0
  194. package/dist/release.d.ts +331 -0
  195. package/dist/release.js +550 -0
  196. package/dist/reply-shape.d.ts +2 -1
  197. package/dist/reply-shape.js +5 -5
  198. package/dist/repository-context-ui.d.ts +2 -1
  199. package/dist/repository-context-ui.js +2 -7
  200. package/dist/request-budget-ui.d.ts +2 -1
  201. package/dist/request-budget-ui.js +16 -16
  202. package/dist/result-review.d.ts +2 -3
  203. package/dist/result-review.js +3 -9
  204. package/dist/retention-ui.d.ts +7 -1
  205. package/dist/retention-ui.js +7 -11
  206. package/dist/retention.js +4 -4
  207. package/dist/scope.d.ts +2 -2
  208. package/dist/scout-report.d.ts +1 -1
  209. package/dist/scout-report.js +1 -1
  210. package/dist/serve.d.ts +11 -2
  211. package/dist/serve.js +105 -99
  212. package/dist/server/chat.d.ts +7 -10
  213. package/dist/server/chat.js +984 -1021
  214. package/dist/server/chrome.d.ts +291 -0
  215. package/dist/server/chrome.js +747 -0
  216. package/dist/server/console.css +2819 -0
  217. package/dist/server/flows.d.ts +2 -6
  218. package/dist/server/flows.js +854 -773
  219. package/dist/server/guards.d.ts +1 -1
  220. package/dist/server/guards.js +8 -5
  221. package/dist/server/handler-context.d.ts +1 -1
  222. package/dist/server/handler-context.js +1 -1
  223. package/dist/server/handler-registry.d.ts +8 -6
  224. package/dist/server/handler-registry.js +14 -2
  225. package/dist/server/http.d.ts +274 -0
  226. package/dist/server/http.js +239 -0
  227. package/dist/server/live.d.ts +34 -0
  228. package/dist/server/live.js +309 -0
  229. package/dist/server/pages.d.ts +2 -6
  230. package/dist/server/pages.js +808 -789
  231. package/dist/server/people-tokens.d.ts +1 -6
  232. package/dist/server/people-tokens.js +581 -573
  233. package/dist/server/remote.d.ts +4 -7
  234. package/dist/server/remote.js +39 -70
  235. package/dist/server/render-chat.d.ts +402 -0
  236. package/dist/server/render-chat.js +927 -0
  237. package/dist/server/render-pages.d.ts +535 -0
  238. package/dist/server/render-pages.js +1277 -0
  239. package/dist/server/render-people.d.ts +32 -0
  240. package/dist/server/render-people.js +121 -0
  241. package/dist/server/render-results.d.ts +744 -0
  242. package/dist/server/render-results.js +2237 -0
  243. package/dist/server/render-settings.d.ts +154 -0
  244. package/dist/server/render-settings.js +580 -0
  245. package/dist/server/render-tasks.d.ts +632 -0
  246. package/dist/server/render-tasks.js +2345 -0
  247. package/dist/server/request-context.d.ts +46 -0
  248. package/dist/server/request-context.js +17 -0
  249. package/dist/server/route-table.d.ts +25 -3
  250. package/dist/server/route-table.js +167 -77
  251. package/dist/server/runtime.d.ts +28 -20
  252. package/dist/server/runtime.js +7 -1
  253. package/dist/server/session.d.ts +138 -0
  254. package/dist/server/session.js +94 -0
  255. package/dist/server/settings.d.ts +1 -5
  256. package/dist/server/settings.js +2186 -2121
  257. package/dist/server/task-views.d.ts +11 -4
  258. package/dist/server/task-views.js +26 -22
  259. package/dist/server/tasks.d.ts +9 -9
  260. package/dist/server/tasks.js +1489 -1420
  261. package/dist/server-telemetry.d.ts +4 -3
  262. package/dist/server-telemetry.js +4 -8
  263. package/dist/setup-guide.d.ts +2 -2
  264. package/dist/skills-ui.d.ts +4 -3
  265. package/dist/skills-ui.js +14 -17
  266. package/dist/skills.d.ts +1 -1
  267. package/dist/skills.js +2 -1
  268. package/dist/slack-chat.d.ts +2 -2
  269. package/dist/slack-chat.js +6 -6
  270. package/dist/slack-settings.d.ts +3 -2
  271. package/dist/slack-settings.js +17 -42
  272. package/dist/spend-ui.d.ts +5 -4
  273. package/dist/spend-ui.js +17 -40
  274. package/dist/spend.d.ts +14 -14
  275. package/dist/spend.js +31 -31
  276. package/dist/sso-ui.d.ts +3 -2
  277. package/dist/sso-ui.js +26 -27
  278. package/dist/storage-ui.d.ts +2 -1
  279. package/dist/storage-ui.js +14 -33
  280. package/dist/store-baseline.d.ts +19 -0
  281. package/dist/store-baseline.js +2873 -0
  282. package/dist/store.d.ts +197 -249
  283. package/dist/store.js +747 -3084
  284. package/dist/subagent-admin.d.ts +53 -0
  285. package/dist/{teammate-admin.js → subagent-admin.js} +37 -37
  286. package/dist/{teammate-desk.d.ts → subagent-desk.d.ts} +22 -23
  287. package/dist/{teammate-desk.js → subagent-desk.js} +26 -35
  288. package/dist/{teammate-memory.d.ts → subagent-memory.d.ts} +17 -17
  289. package/dist/{teammate-memory.js → subagent-memory.js} +41 -41
  290. package/dist/{teammate-question.d.ts → subagent-question.d.ts} +6 -6
  291. package/dist/{teammate-question.js → subagent-question.js} +17 -17
  292. package/dist/{teammate-tools.d.ts → subagent-tools.d.ts} +18 -18
  293. package/dist/{teammate-tools.js → subagent-tools.js} +36 -36
  294. package/dist/{teammate-week.d.ts → subagent-week.d.ts} +10 -10
  295. package/dist/{teammate-week.js → subagent-week.js} +23 -23
  296. package/dist/{teammate-work.d.ts → subagent-work.d.ts} +21 -21
  297. package/dist/{teammate-work.js → subagent-work.js} +50 -50
  298. package/dist/subagents-ui.d.ts +22 -0
  299. package/dist/subagents-ui.js +137 -0
  300. package/dist/{teammates.d.ts → subagents.d.ts} +11 -11
  301. package/dist/{teammates.js → subagents.js} +21 -21
  302. package/dist/subscription-chat.d.ts +13 -13
  303. package/dist/subscription-chat.js +19 -19
  304. package/dist/surface.d.ts +19 -2
  305. package/dist/surface.js +20 -33
  306. package/dist/task-composer.d.ts +2 -1
  307. package/dist/task-composer.js +7 -7
  308. package/dist/task-status.d.ts +4 -3
  309. package/dist/task-status.js +15 -16
  310. package/dist/team-cli.d.ts +1 -0
  311. package/dist/team-cli.js +14 -2
  312. package/dist/team-http.d.ts +5 -9
  313. package/dist/team-http.js +5 -63
  314. package/dist/team-leads.js +13 -13
  315. package/dist/team-runtime.d.ts +2 -2
  316. package/dist/team-runtime.js +8 -8
  317. package/dist/team-ui.d.ts +2 -1
  318. package/dist/team-ui.js +2 -5
  319. package/dist/teams-chat.js +6 -6
  320. package/dist/teams-settings.d.ts +3 -2
  321. package/dist/teams-settings.js +20 -31
  322. package/dist/telegram-flow.d.ts +3 -3
  323. package/dist/telegram-flow.js +3 -3
  324. package/dist/{telegram-mate.d.ts → telegram-lead.d.ts} +5 -5
  325. package/dist/{telegram-mate.js → telegram-lead.js} +27 -27
  326. package/dist/telegram-settings.d.ts +3 -2
  327. package/dist/telegram-settings.js +14 -25
  328. package/dist/telegram-status.js +1 -1
  329. package/dist/telegram-team.d.ts +1 -1
  330. package/dist/telegram-team.js +5 -5
  331. package/dist/telegram.d.ts +1 -1
  332. package/dist/telegram.js +9 -30
  333. package/dist/text-limits.d.ts +18 -18
  334. package/dist/text-limits.js +18 -18
  335. package/dist/toolroll-update-ui.d.ts +3 -2
  336. package/dist/toolroll-update-ui.js +35 -39
  337. package/dist/toolroll-update.d.ts +8 -0
  338. package/dist/toolroll-update.js +45 -8
  339. package/dist/tools-ui.d.ts +5 -2
  340. package/dist/tools-ui.js +31 -31
  341. package/dist/when-html.d.ts +11 -3
  342. package/dist/when-html.js +5 -6
  343. package/dist/workspace-ui.d.ts +4 -2
  344. package/dist/workspace-ui.js +10 -10
  345. package/package.json +1 -2
  346. package/dist/server/shared.d.ts +0 -3151
  347. package/dist/server/shared.js +0 -12001
  348. package/dist/teammate-admin.d.ts +0 -53
  349. package/dist/teammates-ui.d.ts +0 -20
  350. package/dist/teammates-ui.js +0 -166
@@ -0,0 +1,46 @@
1
+ /** What one console request knows about itself, readable anywhere below its handler. */
2
+ import { AsyncLocalStorage } from "node:async_hooks";
3
+ import type { ServerResponse } from "node:http";
4
+ import { type Html } from "../html.js";
5
+ import type { workCountsByProject, WorkIndexPage } from "../work-index.js";
6
+ export type RequestFacts = {
7
+ appOrigins?: readonly (string | null)[];
8
+ sso?: {
9
+ label: string;
10
+ fresh: boolean;
11
+ } | undefined;
12
+ refusal?: (response: ServerResponse, status: number, body: Html) => void;
13
+ theme?: "light" | "dark" | null;
14
+ accent?: string | null;
15
+ updateSeen?: string | null;
16
+ /** The signed-in browser session's form token ("" for an API token): every POST form carries it (postForm). */
17
+ csrf: string;
18
+ returnTo: string;
19
+ actor?: string;
20
+ createdTask?: string;
21
+ browser?: boolean;
22
+ workspaceRead?: boolean;
23
+ workspaceRequest?: string | null;
24
+ workCounts?: ReturnType<typeof workCountsByProject>;
25
+ workCrew?: {
26
+ project: string | null;
27
+ page: WorkIndexPage;
28
+ };
29
+ lens?: string | null;
30
+ workspaceValidator?: {
31
+ key: string;
32
+ revision: string;
33
+ expiresAt: number;
34
+ etag: string;
35
+ };
36
+ };
37
+ /**
38
+ * The request's own session facts, readable from anywhere below the
39
+ * dispatcher without threading them through forty call sites: the csrf
40
+ * token every POST form carries, and the path a switch returns
41
+ * to. AsyncLocalStorage follows the request's own async chain, so two
42
+ * interleaved requests never read each other's token.
43
+ */
44
+ export declare const requestContext: AsyncLocalStorage<RequestFacts>;
45
+ /** Run a render outside a live request (a test, a background render) as if a browser session with this token asked. */
46
+ export declare function withFormToken<T>(csrf: string, render: () => T): T;
@@ -0,0 +1,17 @@
1
+ /** What one console request knows about itself, readable anywhere below its handler. */
2
+ import { AsyncLocalStorage } from "node:async_hooks";
3
+ import { provideFormToken } from "../html.js";
4
+ /**
5
+ * The request's own session facts, readable from anywhere below the
6
+ * dispatcher without threading them through forty call sites: the csrf
7
+ * token every POST form carries, and the path a switch returns
8
+ * to. AsyncLocalStorage follows the request's own async chain, so two
9
+ * interleaved requests never read each other's token.
10
+ */
11
+ export const requestContext = new AsyncLocalStorage();
12
+ provideFormToken(() => requestContext.getStore()?.csrf);
13
+ /** Run a render outside a live request (a test, a background render) as if a browser session with this token asked. */
14
+ export function withFormToken(csrf, render) {
15
+ const facts = requestContext.getStore();
16
+ return requestContext.run({ ...(facts ?? { returnTo: "/" }), csrf }, render);
17
+ }
@@ -9,8 +9,12 @@
9
9
  * account's read/write sets, the viewer's POST set, the selected-project
10
10
  * exceptions) are projections of this table.
11
11
  *
12
- * Handlers still re-prove what can change after admission: passwords, nonces,
13
- * webhook signatures, object membership, stale sessions and revoked tokens.
12
+ * A row's `role` says who among its admitted callers may use it (an approver,
13
+ * or an instance operator), with the words a caller without it hears. Every
14
+ * row has exactly one handler (handler-registry.ts); handlers do not re-match
15
+ * paths or restate these checks. They still re-prove what can change after
16
+ * admission: passwords, nonces, webhook signatures, object membership, stale
17
+ * sessions and revoked tokens.
14
18
  */
15
19
  /** Who presents the request. `service` is a verified external sender (a signed webhook, an OAuth return carrying its one-time state). */
16
20
  export type Caller = "anonymous" | "service" | "cookie" | "bearer" | "oauth" | "coordinator" | "lead";
@@ -29,10 +33,21 @@ export type ProjectResolver = "none" | "session" | "form" | "form-path" | "task"
29
33
  * - `conversation`: a shared room's own audience admission, else instance access.
30
34
  */
31
35
  export type LimitedAccess = "deny" | "collection" | "unscoped" | "resource" | "self" | "proposal" | "conversation";
32
- export type Domain = "tasks" | "flows" | "chat" | "settings" | "people" | "remote" | "pages";
36
+ export type Domain = "tasks" | "flows" | "chat" | "settings" | "people" | "remote" | "pages" | "live";
33
37
  /** `edge`: answered before the console's sign-in (its own protocol proves the caller). `console`: after identify(). */
34
38
  export type Stage = "edge" | "console";
35
39
  export type RouteMethod = "GET" | "POST" | "GET,HEAD";
40
+ /** Who among the admitted callers may use the route: anyone, an approver, or an instance operator (an approver with instance-wide access). */
41
+ export type RouteRole = "any" | "approver" | "operator";
42
+ /** What a caller without the role hears: the console's refusal page (plain text for a token), or a protocol answer. */
43
+ export type RouteRefusal = {
44
+ readonly message: string;
45
+ readonly back?: string;
46
+ } | {
47
+ readonly status: number;
48
+ readonly type: string;
49
+ readonly body: string;
50
+ };
36
51
  export interface RouteDeclaration {
37
52
  readonly id: string;
38
53
  readonly domain: Domain;
@@ -61,6 +76,13 @@ export interface RouteDeclaration {
61
76
  readonly type: string;
62
77
  readonly body: string;
63
78
  };
79
+ /** Checked after caller, scope and project admission, before the handler. */
80
+ readonly role: RouteRole;
81
+ /** The role counts only through a browser sign-in: an API token hears the same refusal. */
82
+ readonly roleBrowser: boolean;
83
+ readonly roleRefusal?: RouteRefusal;
84
+ /** The largest form body a POST here reads (else the console's default). */
85
+ readonly bodyCap?: number;
64
86
  }
65
87
  /** The sole route table, in match order. */
66
88
  export declare const ROUTES: readonly RouteDeclaration[];
@@ -9,8 +9,12 @@
9
9
  * account's read/write sets, the viewer's POST set, the selected-project
10
10
  * exceptions) are projections of this table.
11
11
  *
12
- * Handlers still re-prove what can change after admission: passwords, nonces,
13
- * webhook signatures, object membership, stale sessions and revoked tokens.
12
+ * A row's `role` says who among its admitted callers may use it (an approver,
13
+ * or an instance operator), with the words a caller without it hears. Every
14
+ * row has exactly one handler (handler-registry.ts); handlers do not re-match
15
+ * paths or restate these checks. They still re-prove what can change after
16
+ * admission: passwords, nonces, webhook signatures, object membership, stale
17
+ * sessions and revoked tokens.
14
18
  */
15
19
  const CONSOLE = ["cookie", "bearer"];
16
20
  const exact = (path) => `^${path.replace(/[.*+?^${}()|[\]\\/]/g, m => (m === "/" ? "\\/" : `\\${m}`))}$`;
@@ -36,6 +40,10 @@ function row(stage, domain, id, method, pattern, options = {}) {
36
40
  sample: options.sample ?? sampleOf(pattern),
37
41
  ...(options.scopeRefusal === undefined ? {} : { scopeRefusal: options.scopeRefusal }),
38
42
  ...(options.callerRefusal === undefined ? {} : { callerRefusal: options.callerRefusal }),
43
+ role: options.role ?? "any",
44
+ roleBrowser: options.roleBrowser ?? false,
45
+ ...(options.roleRefusal === undefined ? {} : { roleRefusal: options.roleRefusal }),
46
+ ...(options.bodyCap === undefined ? {} : { bodyCap: options.bodyCap }),
39
47
  });
40
48
  }
41
49
  const edge = (domain, id, method, path, options = {}) => row("edge", domain, id, method, path.startsWith("^") ? path : exact(path), options);
@@ -46,6 +54,14 @@ const FLOW = "[1-9][0-9]{0,9}";
46
54
  const TASK = "[^/]+";
47
55
  /** Free of a project: no opener redirect for a session with nothing open. */
48
56
  const free = { needsProject: false };
57
+ const approver = (message, back) => ({ role: "approver", roleBrowser: true, roleRefusal: back === undefined ? { message } : { message, back } });
58
+ const operator = (message, back) => ({ role: "operator", roleBrowser: true, roleRefusal: back === undefined ? { message } : { message, back } });
59
+ const anyApprover = (message, back) => ({ ...approver(message, back), roleBrowser: false });
60
+ const anyOperator = (message, back) => ({ ...operator(message, back), roleBrowser: false });
61
+ const SESSION_JSON = { status: 403, type: "application/json", body: JSON.stringify({ error: "session" }) };
62
+ // URL encoding can triple UTF-8 bytes. Task forms admit their bounded fields (a goal and exclusions of 32,000 bytes
63
+ // each, paths and rubric) before canonical text validation; an imported flow is a file.
64
+ const LARGE_FORM = 1024 * 1024;
49
65
  // ---- edge: answered before the console's sign-in ---------------------------------------------------------------
50
66
  // Ordered exactly as the server checks them: health and webhooks before the Host check, the rest after it.
51
67
  edge("pages", "edge.healthz", "GET,HEAD", "/healthz", { host: "before" });
@@ -68,7 +84,6 @@ for (const operation of ["list", "show", "changes", "start", "send", "stop", "re
68
84
  }
69
85
  edge("remote", "edge.team-read", "GET", "/api/team", { callers: ["cookie", "bearer"], scope: "read", project: "adapter", proof: "adapter" });
70
86
  edge("remote", "edge.team-send", "POST", "/api/team", { callers: ["cookie", "bearer"], scope: "read", project: "adapter", proof: "adapter" });
71
- edge("remote", "edge.team-events", "GET", "/api/team/events", { callers: ["cookie", "bearer"], scope: "read", project: "adapter", proof: "adapter" });
72
87
  edge("remote", "edge.cli", "POST", "/api/cli", { callers: ["bearer"], scope: "read", project: "adapter", proof: "adapter" });
73
88
  edge("remote", "edge.teams", "POST", "/teams/messages", { callers: ["service"], proof: "adapter" });
74
89
  edge("pages", "edge.browser-asset", "GET,HEAD", "^\\/assets\\/(workspace\\.js|workspace\\.css|THIRD_PARTY_NOTICES\\.txt)$", { sample: "/assets/workspace.js" });
@@ -87,10 +102,18 @@ edge("people", "edge.logout", "POST", "/logout", { callers: ["anonymous", "cooki
87
102
  edge("people", "edge.join", "GET", "^\\/join\\/[A-Za-z0-9_-]{16,64}$", { sample: "/join/abcdefghijklmnop" });
88
103
  edge("people", "edge.join-send", "POST", "^\\/join\\/[A-Za-z0-9_-]{16,64}$", { sample: "/join/abcdefghijklmnop" });
89
104
  // ---- console reads ---------------------------------------------------------------------------------------------
90
- // Coding: the handler answers any other caller in its own format (JSON for the workspace's fetches).
91
- get("tasks", "code.page", "^\\/code(\\/.*)?$", { project: "coding", limited: "self", ...free, sample: "/code" });
105
+ // Coding: the workspace, a session and its reads. The handler answers any other caller in its own format (JSON for the workspace's fetches).
106
+ const CODING = { project: "coding", limited: "self", ...free };
107
+ const SESSION = "[a-f0-9]{32}";
108
+ get("tasks", "code.page", "/code", CODING);
109
+ get("tasks", "code.session", `^\\/code\\/${SESSION}$`, { ...CODING, sample: `/code/${"a".repeat(32)}` });
110
+ for (const one of ["state", "changes", "ship"])
111
+ get("tasks", `code.${one}`, `^\\/code\\/${SESSION}\\/${one}$`, { ...CODING, sample: `/code/${"a".repeat(32)}/${one}` });
112
+ // Any other coding address answers "not found" after the same access check.
113
+ get("tasks", "code.other", "^\\/code\\/.*$", { ...CODING, sample: "/code/unknown" });
92
114
  get("pages", "projects.browse", "/projects/browse", free);
93
- get("pages", "projects.github", "/projects/github", free);
115
+ get("pages", "projects.github", "/projects/github", { ...free, callers: ["cookie"], callerRefusal: { status: 403, type: "text/plain; charset=utf-8", body: "listing repositories feeds a browser session's act" },
116
+ ...anyApprover("your login can watch — adding projects is an approver's act") });
94
117
  get("pages", "projects.page", "/projects", { limited: "unscoped", ...free });
95
118
  get("pages", "home", "/", { limited: "collection", ...free });
96
119
  get("tasks", "inbox", "/inbox", { limited: "collection", ...free });
@@ -100,7 +123,7 @@ get("pages", "morning", "/morning");
100
123
  get("pages", "workbench", "/workbench", free);
101
124
  get("tasks", "board", "/board", { limited: "collection" });
102
125
  get("tasks", "review", "/review", { limited: "collection" });
103
- get("settings", "spend", "/spend", free);
126
+ get("settings", "spend", "/spend", { ...free, ...anyOperator("An instance operator sees spend.", "/") });
104
127
  get("pages", "ledger", "/ledger", { limited: "unscoped", ...free });
105
128
  get("pages", "activity", "/activity");
106
129
  get("tasks", "done", "/done", { limited: "collection" });
@@ -112,7 +135,6 @@ get("tasks", "queue", "/queue");
112
135
  get("pages", "peek", "/peek");
113
136
  get("pages", "fleet", "/fleet", free);
114
137
  get("tasks", "tasks.new", "/tasks/new", { limited: "collection", ...free });
115
- get("tasks", "task.live", `^\\/t\\/${TASK}\\/live$`, { project: "task", limited: "resource", ...free, sample: "/t/one/live" });
116
138
  get("tasks", "task.page", `^\\/t\\/${TASK}$`, { project: "task", limited: "resource", ...free, sample: "/t/one" });
117
139
  get("tasks", "task.evidence", `^\\/t\\/${TASK}\\/evidence$`, { project: "task", limited: "resource", ...free, sample: "/t/one/evidence" });
118
140
  get("pages", "ledger.export", "/ledger/export", { limited: "unscoped", ...free });
@@ -126,12 +148,14 @@ get("flows", "flows.gallery", "^\\/flows\\/new\\/[a-z-]{1,40}$", { limited: "res
126
148
  get("flows", "flows.page", "/flows", { limited: "resource", ...free });
127
149
  get("flows", "kits", "/kits");
128
150
  get("flows", "kit.page", "^\\/kits\\/[a-z-]{1,40}$", { sample: "/kits/support" });
129
- get("flows", "teammates", "/teammates", free);
130
- get("flows", "teammate.page", `^\\/teammates\\/[1-9][0-9]{0,9}(\\/soul\\.md)?$`, { ...free, sample: "/teammates/1/soul.md" });
131
- get("flows", "flow.read", `^\\/flows\\/${FLOW}\\/(insights|runs\\/${FLOW}\\/${FLOW})$`, { project: "flow", limited: "resource", ...free, sample: "/flows/1/runs/2/3" });
151
+ get("flows", "subagents", "/settings/lead/subagents", free);
152
+ // D5: subagents were teammates; old links to them land on the same pages under Settings → Lead.
153
+ get("flows", "subagents.legacy", "^\\/teammates(\\/[1-9][0-9]{0,9}(\\/soul\\.md)?)?$", { ...free, sample: "/teammates/1" });
154
+ get("flows", "subagent.page", `^\\/settings\\/lead\\/subagents\\/[1-9][0-9]{0,9}$`, { ...free, sample: "/settings/lead/subagents/1" });
155
+ get("flows", "subagent.soul-file", `^\\/settings\\/lead\\/subagents\\/[1-9][0-9]{0,9}\\/soul\\.md$`, { ...free, sample: "/settings/lead/subagents/1/soul.md" });
156
+ get("flows", "flow.insights", `^\\/flows\\/${FLOW}\\/insights$`, { project: "flow", limited: "resource", ...free, sample: "/flows/1/insights" });
157
+ get("flows", "flow.run", `^\\/flows\\/${FLOW}\\/runs\\/${FLOW}\\/${FLOW}$`, { project: "flow", limited: "resource", ...free, sample: "/flows/1/runs/2/3" });
132
158
  get("flows", "flow.export", `^\\/flows\\/${FLOW}\\/export$`, { project: "flow", limited: "resource", ...free, sample: "/flows/1/export" });
133
- get("flows", "flow.live", `^\\/flows\\/${FLOW}\\/live$`, { callers: ["cookie"], project: "flow", limited: "resource", ...free, sample: "/flows/1/live",
134
- callerRefusal: { status: 403, type: "application/json", body: JSON.stringify({ error: "session" }) } });
135
159
  get("flows", "flow.page", `^\\/flows\\/${FLOW}$`, { project: "flow", limited: "resource", ...free, sample: "/flows/1" });
136
160
  get("flows", "recipes", "/recipes", { limited: "collection", ...free });
137
161
  for (const one of ["run", "start", "new", "edit", "from-task", "preview", "export"])
@@ -140,26 +164,42 @@ for (const one of ["run", "start", "new", "edit", "from-task", "preview", "expor
140
164
  get("flows", "recipes.other", "^\\/recipes\\/.*$", { sample: "/recipes/unknown" });
141
165
  // v115: routines became scheduled flows; old links to them land on the flows page.
142
166
  get("tasks", "routines", "/routines", { limited: "collection" });
143
- get("chat", "chat.stream", "/chat/stream", free);
144
- get("chat", "chat.mate-status", "/chat/mate/status", free);
167
+ // One live stream per page: every room it shows (server/live.ts). Each room admits its own callers; a token reads only the team room.
168
+ edge("live", "live", "GET", "/live", { callers: ["cookie", "bearer"], scope: "read", project: "adapter", proof: "adapter" });
169
+ get("chat", "chat.mate-status", "/chat/mate/status", { ...free, role: "approver", roleBrowser: true, roleRefusal: SESSION_JSON });
145
170
  get("chat", "chat.demo-live", "/chat/demo/live", free);
146
171
  get("chat", "chat.page", "/chat", { project: "conversation", limited: "conversation", ...free });
147
172
  get("chat", "chat.ack", `^\\/chat\\/ack\\/${ID}$`, { sample: "/chat/ack/1" });
148
173
  get("tasks", "routine.page", `^\\/routines\\/${ID}$`, { limited: "resource", sample: "/routines/1" });
149
- get("settings", "control", "/control");
150
- get("settings", "control.connection", "/control/connection");
151
- get("chat", "chat.action", `^\\/chat\\/action\\/${ID}$`, { project: "proposal", limited: "proposal", ...free, sample: "/chat/action/1" });
174
+ get("settings", "control", "/control", anyApprover("Project setup requires an approver."));
175
+ get("settings", "control.connection", "/control/connection", anyApprover("Project setup requires an approver."));
176
+ get("chat", "chat.action", `^\\/chat\\/action\\/${ID}$`, { project: "proposal", limited: "proposal", ...free, sample: "/chat/action/1", ...approver("Sign in to review this action.", "/projects") });
177
+ const SETTINGS_PAGE_ROLES = {
178
+ integrations: approver("An installation approver sees integrations.", "/settings"),
179
+ monitoring: operator("An instance operator sets up monitoring.", "/settings"),
180
+ updates: operator("An instance operator updates Toolroll.", "/settings"),
181
+ retention: operator("An instance operator sets retention.", "/settings"),
182
+ storage: operator("An instance operator looks after storage.", "/settings"),
183
+ backups: operator("An instance operator looks after backups.", "/settings"),
184
+ data: operator("An instance operator exports data.", "/settings"),
185
+ "sign-in": operator("An instance operator sets up sign-in.", "/settings"),
186
+ lead: approver("An approver sets up the lead.", "/settings"),
187
+ teams: approver("An installation approver can connect Teams.", "/settings"),
188
+ discord: approver("An installation approver can connect Discord.", "/settings"),
189
+ slack: approver("An installation approver can connect Slack.", "/settings"),
190
+ "slack/manifest": approver("An installation approver can connect Slack.", "/settings"),
191
+ };
152
192
  for (const one of ["models", "tools", "project", "approval", "policy", "sessions", "integrations", "monitoring", "updates", "retention", "storage", "pull-requests", "checks", "backups", "data", "sign-in", "lead", "teams", "discord", "slack", "slack/manifest"]) {
153
- get(one === "sessions" ? "people" : "settings", `settings.${one}`, `/settings/${one}`, free);
193
+ get(one === "sessions" ? "people" : "settings", `settings.${one}`, `/settings/${one}`, { ...free, ...SETTINGS_PAGE_ROLES[one] });
154
194
  }
155
195
  get("settings", "settings.skills", "/settings/skills", { limited: "unscoped", ...free });
156
196
  get("settings", "settings.flows", "/settings/flows", { limited: "collection", ...free });
157
197
  get("settings", "settings.knowledge", "/settings/knowledge", { limited: "unscoped", ...free });
158
198
  get("settings", "settings.learning", "/settings/learning", { limited: "unscoped", ...free });
159
- get("settings", "settings.telegram", "/settings/telegram", { limited: "self", ...free });
199
+ get("settings", "settings.telegram", "/settings/telegram", { limited: "self", ...free, ...approver("An approver can pair their own phone.", "/settings") });
160
200
  get("settings", "settings.page", "/settings", { limited: "unscoped", ...free });
161
- get("pages", "health", "/health", free);
162
- get("pages", "metrics", "/metrics", free);
201
+ get("pages", "health", "/health", { ...free, ...anyOperator("An instance operator reads server health.", "/") });
202
+ get("pages", "metrics", "/metrics", { ...free, ...anyOperator("An instance operator reads metrics.", "/") });
163
203
  get("chat", "push.key", "/push/key");
164
204
  get("tasks", "decision.page", `^\\/d\\/${ID}$`, { project: "decision", limited: "resource", ...free, sample: "/d/1" });
165
205
  get("chat", "lead.status", "/lead/status", free);
@@ -167,74 +207,115 @@ get("chat", "chat.task-status", "/chat/task-status", free);
167
207
  get("tasks", "decision.evidence", `^\\/d\\/${ID}\\/evidence\\/${ID}$`, { project: "decision", limited: "resource", ...free, sample: "/d/1/evidence/2" });
168
208
  // ---- console actions -------------------------------------------------------------------------------------------
169
209
  // The scripted demo: the handler answers a non-browser caller "no page here".
170
- post("chat", "chat.demo", "^\\/chat\\/demo\\/(ask|[0-9]{1,9}\\/(approve|change|revise|complete))$", { sample: "/chat/demo/ask" });
210
+ post("chat", "chat.demo.ask", "/chat/demo/ask");
211
+ for (const one of ["approve", "change", "revise", "complete"])
212
+ post("chat", `chat.demo.${one}`, `^\\/chat\\/demo\\/[0-9]{1,9}\\/${one}$`, { sample: `/chat/demo/1/${one}` });
171
213
  post("settings", "provider.resume", "^\\/providers\\/[a-z]+\\/resume$", { sample: "/providers/codex/resume" });
172
214
  post("settings", "settings.updates-dismiss", "/settings/updates/dismiss");
173
215
  post("settings", "settings.updates-checks", "/settings/updates/checks");
174
- post("tasks", "code.act", "^\\/code(\\/.*)?$", { scope: "step-up", project: "coding", limited: "self", sample: "/code/start" });
175
- post("settings", "settings.skills-revise", "/settings/skills/revise", { project: "form", limited: "unscoped" });
176
- post("settings", "settings.skills-import", "/settings/skills/import", { project: "form", limited: "unscoped" });
177
- post("settings", "settings.skills-change", "/settings/skills/change", { project: "form", limited: "unscoped" });
178
- post("flows", "kit.act", "^\\/kits\\/[a-z-]{1,40}\\/(setup|sample|github)$", { project: "form", sample: "/kits/support/setup" });
179
- post("flows", "teammate.new", "/teammates/new", { project: "form" });
180
- post("flows", "teammate.act", "^\\/teammates\\/[1-9][0-9]{0,9}\\/(soul|state|note|settings|summary|tools|memory|routines|week)$", { sample: "/teammates/1/soul" });
181
- post("flows", "teammate.answer", "^\\/teammates\\/questions\\/[1-9][0-9]{0,9}\\/answer$", { sample: "/teammates/questions/1/answer" });
216
+ const CODING_ACT = { scope: "step-up", project: "coding", limited: "self" };
217
+ post("tasks", "code.start", "/code/start", CODING_ACT);
218
+ for (const one of ["send", "stop", "resume", "recover", "continue", "ship", "answer"])
219
+ post("tasks", `code.${one}-send`, `^\\/code\\/${SESSION}\\/${one}$`, { ...CODING_ACT, sample: `/code/${"a".repeat(32)}/${one}` });
220
+ // Any other coding action answers "not found" after the same access check.
221
+ post("tasks", "code.act-other", "^\\/code(\\/.*)?$", { ...CODING_ACT, sample: "/code/unknown" });
222
+ post("settings", "settings.skills-revise", "/settings/skills/revise", { project: "form", limited: "unscoped", ...approver("Sign in to revise a test.", "/settings/skills") });
223
+ post("settings", "settings.skills-import", "/settings/skills/import", { project: "form", limited: "unscoped", bodyCap: 2 * 1024 * 1024, ...approver("Sign in as an approver to manage skills.", "/settings/skills") });
224
+ post("settings", "settings.skills-change", "/settings/skills/change", { project: "form", limited: "unscoped", ...approver("Sign in as an approver to manage skills.", "/settings/skills") });
225
+ for (const one of ["setup", "sample", "github"])
226
+ post("flows", `kit.${one}`, `^\\/kits\\/[a-z-]{1,40}\\/${one}$`, { project: "form", sample: `/kits/support/${one}`, ...approver("Sign in as an approver to set kits up.", "/kits") });
227
+ const SUBAGENTS = anyApprover("Sign in as an approver to look after subagents.", "/settings/lead/subagents");
228
+ post("flows", "subagent.new", "/settings/lead/subagents/new", { project: "form", ...SUBAGENTS });
229
+ for (const one of ["soul", "state", "note", "settings", "summary", "tools", "memory", "routines", "week"])
230
+ post("flows", `subagent.${one}`, `^\\/settings\\/lead\\/subagents\\/[1-9][0-9]{0,9}\\/${one}$`, { sample: `/settings/lead/subagents/1/${one}`, ...SUBAGENTS });
231
+ post("flows", "subagent.answer", "^\\/settings\\/lead\\/subagents\\/questions\\/[1-9][0-9]{0,9}\\/answer$", { sample: "/settings/lead/subagents/questions/1/answer" });
182
232
  post("flows", "flows.gallery-create", "^\\/flows\\/new\\/[a-z-]{1,40}$", { project: "form", limited: "resource", sample: "/flows/new/triage" });
183
- post("flows", "flows.create", "/flows/new", { project: "form", limited: "resource" });
184
- post("flows", "flows.example", "/flows/example", { project: "form" });
185
- post("flows", "flows.import", "/flows/import", { project: "form", limited: "resource" });
186
- post("flows", "flow.act", `^\\/flows\\/${FLOW}\\/(save|cards|archive|scripts)$`, { project: "flow", limited: "resource", sample: "/flows/1/save" });
187
- post("flows", "flow.instance-act", `^\\/flows\\/${FLOW}\\/(linear-key|hooks-address|secrets)$`, { scope: "step-up", project: "flow", sample: "/flows/1/secrets" });
188
- post("flows", "flow.triggers", `^\\/flows\\/${FLOW}\\/triggers$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers" });
189
- post("flows", "flow.card", `^\\/flows\\/${FLOW}\\/cards\\/${FLOW}\\/(move|decide|cancel|comment|assign|watch)$`, { project: "flow", limited: "resource", sample: "/flows/1/cards/2/move" });
190
- post("flows", "flow.card-choose", `^\\/flows\\/${FLOW}\\/cards\\/${FLOW}\\/choose$`, { project: "flow", sample: "/flows/1/cards/2/choose" });
191
- post("flows", "flow.trigger.pause", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/pause$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/pause" });
192
- post("flows", "flow.trigger.resume", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/resume$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/resume" });
193
- post("flows", "flow.trigger.remove", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/remove$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/remove" });
194
- post("flows", "flow.trigger.check", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/check$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/check" });
195
- post("flows", "flow.trigger.press", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/press$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/press" });
196
- post("flows", "flow.trigger.renew", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/renew$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/renew" });
197
- post("flows", "flow.trigger.secret", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/secret$`, { scope: "step-up", project: "flow", limited: "resource", sample: "/flows/1/triggers/2/secret" });
198
- post("flows", "flow.trigger.share", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/share$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/share" });
199
- post("flows", "flow.trigger.unshare", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/unshare$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/unshare" });
200
- post("settings", "spend.budget", "/spend/budget", { scope: "step-up" });
233
+ // Flow editing answers JSON: a caller without approver standing hears it in the editor's own format.
234
+ const CREATE_FLOWS = approver("Sign in as an approver to create flows.", "/flows");
235
+ const EDIT_FLOWS = { role: "approver", roleBrowser: true, roleRefusal: { status: 403, type: "application/json; charset=utf-8", body: JSON.stringify({ ok: false, said: "Sign in as an approver to change flows." }) } };
236
+ post("flows", "flows.create", "/flows/new", { project: "form", limited: "resource", ...CREATE_FLOWS });
237
+ post("flows", "flows.example", "/flows/example", { project: "form", ...CREATE_FLOWS });
238
+ post("flows", "flows.import", "/flows/import", { project: "form", limited: "resource", bodyCap: LARGE_FORM, ...CREATE_FLOWS });
239
+ for (const one of ["save", "cards", "archive", "scripts"])
240
+ post("flows", `flow.${one}`, `^\\/flows\\/${FLOW}\\/${one}$`, { project: "flow", limited: "resource", sample: `/flows/1/${one}`, ...EDIT_FLOWS });
241
+ for (const one of ["linear-key", "hooks-address", "secrets"])
242
+ post("flows", `flow.${one}`, `^\\/flows\\/${FLOW}\\/${one}$`, { scope: "step-up", project: "flow", sample: `/flows/1/${one}`, ...EDIT_FLOWS });
243
+ post("flows", "flow.triggers", `^\\/flows\\/${FLOW}\\/triggers$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers", ...EDIT_FLOWS });
244
+ for (const one of ["move", "decide", "cancel", "comment", "assign", "watch"])
245
+ post("flows", `flow.card.${one}`, `^\\/flows\\/${FLOW}\\/cards\\/${FLOW}\\/${one}$`, { project: "flow", limited: "resource", sample: `/flows/1/cards/2/${one}`, ...EDIT_FLOWS });
246
+ post("flows", "flow.card.choose", `^\\/flows\\/${FLOW}\\/cards\\/${FLOW}\\/choose$`, { project: "flow", sample: "/flows/1/cards/2/choose", ...EDIT_FLOWS });
247
+ post("flows", "flow.trigger.pause", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/pause$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/pause", ...EDIT_FLOWS });
248
+ post("flows", "flow.trigger.resume", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/resume$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/resume", ...EDIT_FLOWS });
249
+ post("flows", "flow.trigger.remove", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/remove$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/remove", ...EDIT_FLOWS });
250
+ post("flows", "flow.trigger.check", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/check$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/check", ...EDIT_FLOWS });
251
+ post("flows", "flow.trigger.press", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/press$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/press", ...EDIT_FLOWS });
252
+ post("flows", "flow.trigger.renew", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/renew$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/renew", ...EDIT_FLOWS });
253
+ post("flows", "flow.trigger.secret", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/secret$`, { scope: "step-up", project: "flow", limited: "resource", sample: "/flows/1/triggers/2/secret", ...EDIT_FLOWS });
254
+ post("flows", "flow.trigger.share", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/share$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/share", ...EDIT_FLOWS });
255
+ post("flows", "flow.trigger.unshare", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/unshare$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/unshare", ...EDIT_FLOWS });
256
+ post("settings", "spend.budget", "/spend/budget", { scope: "step-up", ...operator("An instance operator sets budgets.", "/spend") });
201
257
  post("settings", "settings.project-concurrency", "/settings/project/concurrency");
202
- post("settings", "settings.project-delete", "/settings/project/delete", { scope: "step-up" });
203
- post("pages", "ledger.checkpoint", "/ledger/checkpoint");
258
+ post("settings", "settings.project-delete", "/settings/project/delete", { scope: "step-up", ...operator("An instance operator deletes projects.", "/settings/project") });
259
+ post("pages", "ledger.checkpoint", "/ledger/checkpoint", operator("An instance operator makes ledger checkpoints.", "/ledger"));
260
+ const SETTINGS_ACT_ROLES = {
261
+ policy: operator("An instance operator sets the policy.", "/settings/policy"),
262
+ approval: operator("An instance operator sets approval rules.", "/settings/approval"),
263
+ "request-limits": operator("An instance operator sets request limits.", "/settings/sessions"),
264
+ "sign-in": operator("An instance operator sets up sign-in.", "/settings"),
265
+ updates: operator("An instance operator updates Toolroll.", "/settings"),
266
+ "updates/seen": operator("An instance operator updates Toolroll.", "/settings"),
267
+ "updates/cancel": operator("An instance operator updates Toolroll.", "/settings"),
268
+ retention: operator("An instance operator sets retention.", "/settings"),
269
+ storage: operator("An instance operator looks after storage.", "/settings"),
270
+ "storage/clean": operator("An instance operator looks after storage.", "/settings"),
271
+ "storage/discard": operator("An instance operator looks after storage.", "/settings"),
272
+ "backups/now": operator("An instance operator looks after backups.", "/settings"),
273
+ backups: operator("An instance operator looks after backups.", "/settings"),
274
+ data: operator("An instance operator exports data.", "/settings"),
275
+ "integrations/test": approver("An installation approver tests integrations.", "/settings"),
276
+ monitoring: operator("An instance operator sets up monitoring.", "/settings"),
277
+ "tools/connect": approver("Sign in as an approver to connect tools.", "/settings/tools"),
278
+ "tools/change": approver("Sign in as an approver to manage tools.", "/settings/tools"),
279
+ };
204
280
  for (const one of ["policy", "approval", "request-limits", "sessions", "sign-in", "updates", "updates/seen", "updates/cancel", "retention", "storage", "storage/clean", "storage/discard", "pull-requests", "checks", "backups/now", "backups", "data", "integrations/test", "monitoring", "tools/connect", "tools/change", "appearance"]) {
205
- post(one === "sessions" ? "people" : "settings", `settings.${one.replaceAll("/", "-")}-send`, `/settings/${one}`, { ...(["policy", "approval", "request-limits", "sessions", "sign-in", "updates", "updates/seen", "updates/cancel", "retention", "storage", "storage/clean", "storage/discard", "pull-requests", "checks", "backups", "data", "monitoring", "tools/connect", "tools/change"].includes(one) ? { scope: "step-up" } : {}) });
281
+ post(one === "sessions" ? "people" : "settings", `settings.${one.replaceAll("/", "-")}-send`, `/settings/${one}`, { ...(["policy", "approval", "request-limits", "sessions", "sign-in", "updates", "updates/seen", "updates/cancel", "retention", "storage", "storage/clean", "storage/discard", "pull-requests", "checks", "backups", "data", "monitoring", "tools/connect", "tools/change"].includes(one) ? { scope: "step-up" } : {}), ...SETTINGS_ACT_ROLES[one] });
206
282
  }
207
- post("settings", "settings.flows-on", "/settings/flows/on", { limited: "collection" });
283
+ post("settings", "settings.flows-on", "/settings/flows/on", { limited: "collection", ...approver("An approver switches starter flows on.", "/settings/flows") });
284
+ const MODELS = approver("Sign in as an approver to change models.", "/settings/models");
285
+ for (const one of ["check", "watch", "update", "agent"])
286
+ post("settings", `settings.models-${one}`, `/settings/models/${one}`, MODELS);
208
287
  // Any other models action answers the models area's own refusal.
209
- post("settings", "settings.models-act", "^\\/settings\\/models\\/.*$", { sample: "/settings/models/check" });
210
- post("settings", "settings.knowledge-refresh", "/settings/knowledge/refresh", { project: "form", limited: "unscoped" });
211
- post("settings", "settings.knowledge-proposal", "/settings/knowledge/proposal", { project: "form" });
212
- post("settings", "settings.knowledge-decision", "/settings/knowledge/decision", { project: "form" });
213
- post("settings", "settings.knowledge-change", "/settings/knowledge/change", { project: "form", limited: "unscoped" });
288
+ post("settings", "settings.models-act", "^\\/settings\\/models\\/.*$", { sample: "/settings/models/unknown", ...MODELS });
289
+ post("settings", "settings.knowledge-refresh", "/settings/knowledge/refresh", { project: "form", limited: "unscoped", ...approver("Sign in as an approver to refresh project context.", "/settings/knowledge") });
290
+ post("settings", "settings.knowledge-proposal", "/settings/knowledge/proposal", { project: "form", ...approver("Sign in as an approver to decide memory proposals.", "/settings/knowledge") });
291
+ post("settings", "settings.knowledge-decision", "/settings/knowledge/decision", { project: "form", ...approver("Sign in as an approver to change project decisions.", "/settings/knowledge") });
292
+ post("settings", "settings.knowledge-change", "/settings/knowledge/change", { project: "form", limited: "unscoped", ...approver("Sign in as an approver to change project knowledge.", "/settings/knowledge") });
214
293
  post("settings", "settings.learning-change", "/settings/learning/change", { project: "form", limited: "unscoped" });
215
294
  for (const one of ["setup-preview", "setup-approve", "instructions-preview", "instructions-approve"])
216
295
  post("settings", `control.${one}`, `/control/${one}`, { ...(one.endsWith("approve") ? { scope: "step-up" } : {}) });
217
- for (const service of ["slack", "teams", "discord"])
218
- post("settings", `settings.${service}-send`, `^\\/settings\\/${service}\\/(connect|pair|unpair|disconnect|alerts)$`, { sample: `/settings/${service}/pair`, scope: "step-up" });
219
- post("settings", "settings.telegram-retry", "/settings/telegram/retry", { limited: "self", scope: "step-up" });
296
+ for (const [service, name] of [["slack", "Slack"], ["teams", "Teams"], ["discord", "Discord"]]) {
297
+ for (const one of ["connect", "pair", "unpair", "disconnect", "alerts"])
298
+ post("settings", `settings.${service}-${one}`, `/settings/${service}/${one}`, { scope: "step-up", ...approver(`An installation approver can connect ${name}.`, "/settings") });
299
+ }
300
+ post("settings", "settings.telegram-retry", "/settings/telegram/retry", { limited: "self", scope: "step-up", ...approver("An approver can retry their own replies.", "/settings") });
220
301
  for (const one of ["confirm", "save", "off"])
221
- post("settings", `settings.chat-approval-${one}`, `/settings/chat-approval/${one}`, { limited: "self", callers: ["cookie"], ...(one === "save" ? { scope: "step-up" } : {}) });
222
- post("settings", "settings.telegram-pair", "/settings/telegram/pair", { limited: "self", scope: "step-up" });
223
- post("settings", "settings.telegram-unpair", "/settings/telegram/unpair", { limited: "self", scope: "step-up" });
302
+ post("settings", `settings.chat-approval-${one}`, `/settings/chat-approval/${one}`, { limited: "self", callers: ["cookie"], ...(one === "save" ? { scope: "step-up" } : {}), ...approver("An approver turns on approving from their own chat.", "/settings") });
303
+ post("settings", "settings.telegram-pair", "/settings/telegram/pair", { limited: "self", scope: "step-up", ...approver("An approver can pair their own phone.", "/settings") });
304
+ post("settings", "settings.telegram-unpair", "/settings/telegram/unpair", { limited: "self", scope: "step-up", ...approver("An approver can pair their own phone.", "/settings") });
224
305
  for (const one of ["messaging", "permission-default", "quality-default", "notifications", "notifications/mute", "telegram-digest", "provider-key", "provider-key-clear", "telegram-token", "email", "email-test", "email-read-test", "google", "google/disconnect"]) {
225
306
  post("settings", `settings.${one.replaceAll("/", "-")}-send`, `/settings/${one}`);
226
307
  }
227
308
  post("pages", "projects.select", "/projects/select", { project: "form-path", limited: "unscoped", viewer: true });
228
- post("pages", "projects.remove", "/projects/remove", { project: "form" });
309
+ post("pages", "projects.remove", "/projects/remove", { project: "form", ...operator("An instance operator removes projects.", "/settings/project") });
229
310
  post("pages", "projects.open", "/projects/open", { project: "form-path" });
230
- post("tasks", "tasks.add", "/tasks/add", { project: "form", limited: "collection" });
311
+ post("tasks", "tasks.add", "/tasks/add", { project: "form", limited: "collection", bodyCap: LARGE_FORM });
231
312
  post("tasks", "queue.move", "/queue/move");
232
313
  post("tasks", "queue.note", "/queue/note");
233
314
  post("pages", "fleet.register", "/fleet/runner/register", { scope: "step-up" });
234
315
  post("settings", "mode.confirm", "/mode/confirm", { scope: "step-up" });
235
316
  post("settings", "mode.sign", "/mode/sign", { scope: "step-up" });
236
317
  post("settings", "mode.revoke", "/mode/revoke");
237
- post("people", "people.projects", "/people/projects", { scope: "step-up" });
318
+ post("people", "people.projects", "/people/projects", { scope: "step-up", ...operator("An instance operator manages project access.", "/people") });
238
319
  post("people", "people.invite", "/people/invite", { scope: "step-up" });
239
320
  post("people", "people.invite-revoke", "/people/invite-revoke", { scope: "step-up" });
240
321
  post("people", "people.revoke", "/people/revoke", { scope: "step-up" });
@@ -245,7 +326,7 @@ post("tasks", "task.act.hold", `^\\/t\\/${TASK}\\/hold$`, { project: "task", lim
245
326
  post("tasks", "task.act.unhold", `^\\/t\\/${TASK}\\/unhold$`, { project: "task", limited: "resource", sample: "/t/one/unhold" });
246
327
  post("tasks", "task.act.requeue", `^\\/t\\/${TASK}\\/requeue$`, { project: "task", limited: "resource", sample: "/t/one/requeue" });
247
328
  post("tasks", "task.act.cancel", `^\\/t\\/${TASK}\\/cancel$`, { project: "task", limited: "resource", sample: "/t/one/cancel" });
248
- post("tasks", "task.act.scope", `^\\/t\\/${TASK}\\/scope$`, { project: "task", limited: "resource", sample: "/t/one/scope" });
329
+ post("tasks", "task.act.scope", `^\\/t\\/${TASK}\\/scope$`, { project: "task", limited: "resource", sample: "/t/one/scope", bodyCap: LARGE_FORM });
249
330
  post("tasks", "task.act.approve", `^\\/t\\/${TASK}\\/approve$`, { scope: "step-up", project: "task", limited: "resource", sample: "/t/one/approve" });
250
331
  post("tasks", "task.act.plan", `^\\/t\\/${TASK}\\/plan$`, { project: "task", limited: "resource", sample: "/t/one/plan" });
251
332
  post("tasks", "task.act.plan-edit", `^\\/t\\/${TASK}\\/plan-edit$`, { project: "task", limited: "resource", sample: "/t/one/plan-edit" });
@@ -257,33 +338,38 @@ post("tasks", "task.act.accept-revision", `^\\/t\\/${TASK}\\/accept-revision$`,
257
338
  post("tasks", "task.act.reject-revision", `^\\/t\\/${TASK}\\/reject-revision$`, { scope: "step-up", project: "task", limited: "resource", sample: "/t/one/reject-revision" });
258
339
  post("tasks", "task.act.route", `^\\/t\\/${TASK}\\/route$`, { project: "task", limited: "resource", sample: "/t/one/route" });
259
340
  post("tasks", "task.act.retry-review", `^\\/t\\/${TASK}\\/retry-review$`, { project: "task", limited: "resource", sample: "/t/one/retry-review" });
260
- post("tasks", "task.act.complete", `^\\/t\\/${TASK}\\/complete$`, { project: "task", limited: "resource", sample: "/t/one/complete" });
261
- post("tasks", "task.act.merge", `^\\/t\\/${TASK}\\/merge$`, { project: "task", limited: "resource", sample: "/t/one/merge" });
262
- post("tasks", "task.act.confirm-stopped", `^\\/t\\/${TASK}\\/confirm-stopped$`, { scope: "step-up", project: "task", limited: "resource", sample: "/t/one/confirm-stopped" });
341
+ post("tasks", "task.act.complete", `^\\/t\\/${TASK}\\/complete$`, { project: "task", limited: "resource", sample: "/t/one/complete", ...approver("Only an approver can mark a result complete.") });
342
+ post("tasks", "task.act.merge", `^\\/t\\/${TASK}\\/merge$`, { project: "task", limited: "resource", sample: "/t/one/merge", ...approver("Only an approver can merge.") });
343
+ post("tasks", "task.act.confirm-stopped", `^\\/t\\/${TASK}\\/confirm-stopped$`, { scope: "step-up", project: "task", limited: "resource", sample: "/t/one/confirm-stopped", ...approver("Only an approver can confirm a build stopped.") });
263
344
  post("tasks", "task.act.stop", `^\\/t\\/${TASK}\\/stop$`, { project: "task", limited: "resource", sample: "/t/one/stop" });
264
345
  post("tasks", "task.act.resume-arm", `^\\/t\\/${TASK}\\/resume-arm$`, { project: "task", limited: "resource", sample: "/t/one/resume-arm" });
265
346
  post("tasks", "task.act.resume", `^\\/t\\/${TASK}\\/resume$`, { scope: "step-up", project: "task", limited: "resource", sample: "/t/one/resume" });
266
- post("tasks", "task.instance-act", `^\\/t\\/${TASK}\\/(block|unblock|repair-dependency|follow-up)$`, { project: "task", sample: "/t/one/block" });
347
+ for (const one of ["block", "unblock", "repair-dependency", "follow-up"])
348
+ post("tasks", `task.act.${one}`, `^\\/t\\/${TASK}\\/${one}$`, { project: "task", sample: `/t/one/${one}` });
267
349
  // Onboarding names a GitHub repository, not a local project. Its root-index/nonce ceremony proves the future destination.
268
350
  post("pages", "projects.onboard-preview", "/projects/onboard-preview", { project: "none" });
269
351
  post("pages", "projects.onboard-confirm", "/projects/onboard-confirm", { scope: "step-up", project: "none" });
270
352
  post("chat", "push.subscribe", "/push/subscribe", { scope: "step-up" });
271
353
  post("chat", "push.remove", "/push/remove");
272
- post("chat", "onboarding.phone-dismiss", "/onboarding/phone/dismiss");
354
+ post("chat", "onboarding.phone-dismiss", "/onboarding/phone/dismiss", approver("An approver puts this away.", "/chat"));
355
+ const LEAD_WORDS = { identity: "An approver names their lead.", about: "An approver edits what their lead knows about them.", "promise/cancel": "An approver manages the lead's promises.", on: "An approver turns the lead on." };
273
356
  for (const one of ["identity", "about", "promise/cancel", "on"])
274
- post("settings", `settings.lead-${one.replaceAll("/", "-")}`, `/settings/lead/${one}`);
357
+ post("settings", `settings.lead-${one.replaceAll("/", "-")}`, `/settings/lead/${one}`, approver(LEAD_WORDS[one], "/settings/lead"));
275
358
  post("chat", "chat.config", "/chat/config", { scope: "step-up" });
276
359
  for (const one of ["mint", "follow", "end", "stop"])
277
360
  post("chat", `chat.mate-${one}`, `/chat/mate/${one}`, { callers: ["cookie"], ...(one === "mint" ? { scope: "step-up" } : {}) });
278
- post("chat", "chat.proposal", `^\\/chat\\/proposal\\/${ID}\\/(confirm|dismiss)$`, { project: "proposal", limited: "proposal", sample: "/chat/proposal/1/confirm" });
279
- post("chat", "coordinator.proposal", `^\\/proposals\\/${ID}\\/(confirm|dismiss)$`, { sample: "/proposals/1/confirm" });
361
+ for (const one of ["confirm", "dismiss"])
362
+ post("chat", `chat.proposal.${one}`, `^\\/chat\\/proposal\\/${ID}\\/${one}$`, { project: "proposal", limited: "proposal", sample: `/chat/proposal/1/${one}` });
363
+ for (const one of ["confirm", "dismiss"])
364
+ post("chat", `coordinator.proposal.${one}`, `^\\/proposals\\/${ID}\\/${one}$`, { sample: `/proposals/1/${one}` });
280
365
  post("chat", "chat.send", "/chat", { scope: "step-up", project: "conversation" });
281
366
  post("chat", "chat.file", "^\\/chat\\/file\\/[0-9a-f]{32}$", { scope: "step-up", sample: `/chat/file/${"a".repeat(32)}` });
282
367
  post("chat", "chat.ack-send", `^\\/chat\\/ack\\/${ID}$`, { scope: "step-up", sample: "/chat/ack/1" });
283
368
  for (const one of ["prepare", "preview", "import", "save"])
284
369
  post("flows", `recipes.${one}-send`, `/recipes/${one}`, { limited: "collection" });
285
370
  post("flows", "recipes.launch-send", "/recipes/launch", { limited: "collection" });
286
- post("tasks", "run.act", `^\\/r\\/${ID}\\/(note|comment|revise|draft-repair|checks|add-tests)$`, { project: "run", limited: "resource", sample: "/r/1/note" });
371
+ for (const one of ["note", "comment", "revise", "draft-repair", "checks", "add-tests"])
372
+ post("tasks", `run.act.${one}`, `^\\/r\\/${ID}\\/${one}$`, { project: "run", limited: "resource", sample: `/r/1/${one}` });
287
373
  post("tasks", "session.editor-links", "/session/editor-links", { viewer: true });
288
374
  post("tasks", "incident.resolve", `^\\/i\\/${ID}\\/resolve$`, { project: "incident", sample: "/i/1/resolve" });
289
375
  /** The sole route table, in match order. */
@@ -321,6 +407,10 @@ export function assertRouteTable(routes = ROUTES) {
321
407
  throw new Error(`route table: ${route.id} cannot bypass console admission`);
322
408
  if (route.callers.length === 0)
323
409
  throw new Error(`route table: ${route.id} admits no caller`);
410
+ if ((route.role === "any") !== (route.roleRefusal === undefined))
411
+ throw new Error(`route table: ${route.id} role and its refusal go together`);
412
+ if (route.role !== "any" && route.stage !== "console")
413
+ throw new Error(`route table: ${route.id} edge routes prove roles in their protocol`);
324
414
  const test = new RegExp(route.pattern);
325
415
  if (!test.test(route.sample))
326
416
  throw new Error(`route table: ${route.id} does not match its sample ${route.sample}`);