toolroll 0.9.53 → 0.9.55

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 (254) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +5 -4
  3. package/dist/agent-onboard.js +4 -1
  4. package/dist/approval-rules-ui.d.ts +3 -2
  5. package/dist/approval-rules-ui.js +15 -20
  6. package/dist/assignment-ui.d.ts +3 -2
  7. package/dist/assignment-ui.js +4 -11
  8. package/dist/backup-ui.d.ts +2 -1
  9. package/dist/backup-ui.js +16 -27
  10. package/dist/brand-mark.d.ts +3 -2
  11. package/dist/brand-mark.js +4 -4
  12. package/dist/browser/workspace.css +1 -1
  13. package/dist/browser/workspace.js +25 -25
  14. package/dist/browser-shell.d.ts +2 -1
  15. package/dist/browser-shell.js +9 -9
  16. package/dist/browser-workspace.d.ts +2 -3
  17. package/dist/browser-workspace.js +0 -3
  18. package/dist/chat-ask.js +8 -8
  19. package/dist/chat-core.d.ts +171 -0
  20. package/dist/chat-core.js +288 -0
  21. package/dist/chat-decide.d.ts +10 -8
  22. package/dist/chat-decide.js +26 -21
  23. package/dist/chat-delivery-state.d.ts +51 -75
  24. package/dist/chat-delivery-state.js +140 -170
  25. package/dist/chat-delivery.d.ts +21 -2
  26. package/dist/chat-delivery.js +313 -194
  27. package/dist/chat-flow.d.ts +29 -1
  28. package/dist/chat-flow.js +24 -50
  29. package/dist/chat-messages.d.ts +319 -0
  30. package/dist/chat-messages.js +841 -0
  31. package/dist/chat-migration.d.ts +30 -0
  32. package/dist/chat-migration.js +802 -0
  33. package/dist/chat-polish.d.ts +5 -4
  34. package/dist/chat-polish.js +9 -10
  35. package/dist/chat-quiet.d.ts +3 -0
  36. package/dist/chat-quiet.js +5 -0
  37. package/dist/check-levels-ui.d.ts +2 -1
  38. package/dist/check-levels-ui.js +26 -53
  39. package/dist/cli.d.ts +3 -1
  40. package/dist/cli.js +9 -3
  41. package/dist/coding-shipping-ui.d.ts +2 -1
  42. package/dist/coding-shipping-ui.js +6 -6
  43. package/dist/coding-ui.d.ts +3 -1
  44. package/dist/coding-ui.js +30 -29
  45. package/dist/contracts/chat-content.d.ts +71 -2
  46. package/dist/contracts/chat-content.js +22 -3
  47. package/dist/contracts/chat-tables.d.ts +11 -4
  48. package/dist/contracts/chat-tables.js +165 -87
  49. package/dist/contracts/cli.d.ts +1 -3
  50. package/dist/contracts/console-api.d.ts +1 -1
  51. package/dist/contracts/console-api.js +6 -5
  52. package/dist/contracts/discord-callback.js +2 -1
  53. package/dist/contracts/health.d.ts +1 -3
  54. package/dist/contracts/slack-callback.js +3 -2
  55. package/dist/contracts/store-json.d.ts +1 -5
  56. package/dist/contracts/store-json.js +1 -4
  57. package/dist/contracts/teams-callback.js +2 -1
  58. package/dist/control-ui.d.ts +5 -4
  59. package/dist/control-ui.js +9 -34
  60. package/dist/credentials-ui.d.ts +5 -4
  61. package/dist/credentials-ui.js +23 -24
  62. package/dist/demo-chat.d.ts +3 -3
  63. package/dist/demo-chat.js +29 -76
  64. package/dist/discord-chat.js +19 -38
  65. package/dist/discord-settings.d.ts +3 -2
  66. package/dist/discord-settings.js +21 -55
  67. package/dist/discord.js +4 -14
  68. package/dist/envelope.js +10 -0
  69. package/dist/evidence-pack.d.ts +2 -1
  70. package/dist/evidence-pack.js +45 -49
  71. package/dist/export-ui.d.ts +7 -2
  72. package/dist/export-ui.js +4 -10
  73. package/dist/export.js +4 -4
  74. package/dist/flow-gallery-ui.d.ts +4 -3
  75. package/dist/flow-gallery-ui.js +28 -43
  76. package/dist/flow-starters-ui.d.ts +2 -1
  77. package/dist/flow-starters-ui.js +12 -15
  78. package/dist/flow-triggers.d.ts +1 -0
  79. package/dist/flow-triggers.js +16 -14
  80. package/dist/flows-ui.d.ts +4 -3
  81. package/dist/flows-ui.js +19 -30
  82. package/dist/flows.js +2 -1
  83. package/dist/guides.js +108 -3
  84. package/dist/html.d.ts +76 -0
  85. package/dist/html.js +173 -0
  86. package/dist/integrations-ui.d.ts +2 -1
  87. package/dist/integrations-ui.js +19 -23
  88. package/dist/kits-ui.d.ts +5 -4
  89. package/dist/kits-ui.js +18 -20
  90. package/dist/knowledge-ui.d.ts +6 -5
  91. package/dist/knowledge-ui.js +26 -25
  92. package/dist/lead-context.d.ts +11 -1
  93. package/dist/lead-context.js +6 -8
  94. package/dist/ledger-view.d.ts +2 -1
  95. package/dist/ledger-view.js +27 -29
  96. package/dist/limits-ui.d.ts +2 -1
  97. package/dist/limits-ui.js +7 -7
  98. package/dist/live-bus.d.ts +50 -8
  99. package/dist/live-bus.js +133 -50
  100. package/dist/mate-tools.js +3 -2
  101. package/dist/mcp-oauth.js +35 -36
  102. package/dist/models-ui.d.ts +2 -1
  103. package/dist/models-ui.js +25 -35
  104. package/dist/modes.d.ts +13 -1
  105. package/dist/modes.js +17 -1
  106. package/dist/monitoring-ui.d.ts +4 -3
  107. package/dist/monitoring-ui.js +13 -26
  108. package/dist/notification-rows.d.ts +30 -0
  109. package/dist/notification-rows.js +102 -0
  110. package/dist/openrouter-models.d.ts +2 -1
  111. package/dist/openrouter-models.js +5 -9
  112. package/dist/operate.js +16 -9
  113. package/dist/people-audit-ui.d.ts +2 -1
  114. package/dist/people-audit-ui.js +13 -13
  115. package/dist/plane-review.js +5 -11
  116. package/dist/policy-ui.d.ts +3 -2
  117. package/dist/policy-ui.js +17 -23
  118. package/dist/project-delete-ui.d.ts +4 -3
  119. package/dist/project-delete-ui.js +15 -27
  120. package/dist/project-delete.js +21 -26
  121. package/dist/pull-request-ui.d.ts +2 -1
  122. package/dist/pull-request-ui.js +17 -32
  123. package/dist/recipe-ui.d.ts +8 -7
  124. package/dist/recipe-ui.js +39 -32
  125. package/dist/release-adapters.d.ts +60 -0
  126. package/dist/release-adapters.js +435 -0
  127. package/dist/release-cli.d.ts +24 -0
  128. package/dist/release-cli.js +181 -0
  129. package/dist/release-gate.d.ts +80 -0
  130. package/dist/release-gate.js +150 -0
  131. package/dist/release-links.d.ts +29 -0
  132. package/dist/release-links.js +102 -0
  133. package/dist/release.d.ts +331 -0
  134. package/dist/release.js +550 -0
  135. package/dist/reply-shape.d.ts +2 -1
  136. package/dist/reply-shape.js +5 -5
  137. package/dist/repository-context-ui.d.ts +2 -1
  138. package/dist/repository-context-ui.js +2 -7
  139. package/dist/request-budget-ui.d.ts +2 -1
  140. package/dist/request-budget-ui.js +16 -16
  141. package/dist/result-review.d.ts +2 -3
  142. package/dist/result-review.js +3 -9
  143. package/dist/result-shots.js +2 -2
  144. package/dist/retention-ui.d.ts +7 -1
  145. package/dist/retention-ui.js +7 -11
  146. package/dist/retention.js +1 -1
  147. package/dist/serve.d.ts +11 -2
  148. package/dist/serve.js +69 -58
  149. package/dist/server/chat.d.ts +3 -6
  150. package/dist/server/chat.js +969 -1006
  151. package/dist/server/chrome.d.ts +291 -0
  152. package/dist/server/chrome.js +747 -0
  153. package/dist/server/console.css +2819 -0
  154. package/dist/server/flows.d.ts +2 -6
  155. package/dist/server/flows.js +840 -766
  156. package/dist/server/guards.d.ts +1 -1
  157. package/dist/server/guards.js +5 -2
  158. package/dist/server/handler-context.d.ts +1 -1
  159. package/dist/server/handler-context.js +1 -1
  160. package/dist/server/handler-registry.d.ts +8 -6
  161. package/dist/server/handler-registry.js +14 -2
  162. package/dist/server/http.d.ts +274 -0
  163. package/dist/server/http.js +239 -0
  164. package/dist/server/live.d.ts +34 -0
  165. package/dist/server/live.js +309 -0
  166. package/dist/server/pages.d.ts +2 -6
  167. package/dist/server/pages.js +808 -789
  168. package/dist/server/people-tokens.d.ts +1 -6
  169. package/dist/server/people-tokens.js +580 -572
  170. package/dist/server/remote.d.ts +4 -7
  171. package/dist/server/remote.js +37 -70
  172. package/dist/server/render-chat.d.ts +402 -0
  173. package/dist/server/render-chat.js +927 -0
  174. package/dist/server/render-pages.d.ts +535 -0
  175. package/dist/server/render-pages.js +1277 -0
  176. package/dist/server/render-people.d.ts +32 -0
  177. package/dist/server/render-people.js +121 -0
  178. package/dist/server/render-results.d.ts +744 -0
  179. package/dist/server/render-results.js +2237 -0
  180. package/dist/server/render-settings.d.ts +147 -0
  181. package/dist/server/render-settings.js +577 -0
  182. package/dist/server/render-tasks.d.ts +632 -0
  183. package/dist/server/render-tasks.js +2345 -0
  184. package/dist/server/request-context.d.ts +46 -0
  185. package/dist/server/request-context.js +17 -0
  186. package/dist/server/route-table.d.ts +25 -3
  187. package/dist/server/route-table.js +163 -75
  188. package/dist/server/runtime.d.ts +15 -7
  189. package/dist/server/runtime.js +7 -1
  190. package/dist/server/session.d.ts +138 -0
  191. package/dist/server/session.js +94 -0
  192. package/dist/server/settings.d.ts +1 -5
  193. package/dist/server/settings.js +2182 -2117
  194. package/dist/server/task-views.d.ts +11 -4
  195. package/dist/server/task-views.js +26 -22
  196. package/dist/server/tasks.d.ts +9 -9
  197. package/dist/server/tasks.js +1486 -1418
  198. package/dist/server-telemetry.d.ts +4 -3
  199. package/dist/server-telemetry.js +4 -8
  200. package/dist/skills-ui.d.ts +4 -3
  201. package/dist/skills-ui.js +14 -17
  202. package/dist/skills.d.ts +1 -1
  203. package/dist/skills.js +2 -1
  204. package/dist/slack-api.d.ts +5 -1
  205. package/dist/slack-api.js +2 -2
  206. package/dist/slack-chat.d.ts +9 -4
  207. package/dist/slack-chat.js +29 -52
  208. package/dist/slack-settings.d.ts +3 -2
  209. package/dist/slack-settings.js +23 -57
  210. package/dist/slack.js +6 -16
  211. package/dist/spend-ui.d.ts +2 -1
  212. package/dist/spend-ui.js +9 -32
  213. package/dist/sso-ui.d.ts +3 -2
  214. package/dist/sso-ui.js +26 -27
  215. package/dist/storage-ui.d.ts +2 -1
  216. package/dist/storage-ui.js +14 -33
  217. package/dist/store.d.ts +97 -364
  218. package/dist/store.js +207 -1531
  219. package/dist/surface.js +14 -3
  220. package/dist/task-composer.d.ts +2 -1
  221. package/dist/task-composer.js +7 -7
  222. package/dist/task-status.d.ts +4 -3
  223. package/dist/task-status.js +15 -16
  224. package/dist/team-http.d.ts +5 -9
  225. package/dist/team-http.js +5 -63
  226. package/dist/team-ui.d.ts +2 -1
  227. package/dist/team-ui.js +2 -5
  228. package/dist/teammate-question.js +12 -12
  229. package/dist/teammates-ui.d.ts +5 -4
  230. package/dist/teammates-ui.js +61 -90
  231. package/dist/teams-chat.js +12 -9
  232. package/dist/teams-settings.d.ts +3 -2
  233. package/dist/teams-settings.js +22 -33
  234. package/dist/teams.js +4 -5
  235. package/dist/telegram-flow.js +5 -5
  236. package/dist/telegram-settings.d.ts +3 -2
  237. package/dist/telegram-settings.js +14 -25
  238. package/dist/telegram.js +4 -4
  239. package/dist/toolroll-update-ui.d.ts +3 -2
  240. package/dist/toolroll-update-ui.js +35 -39
  241. package/dist/toolroll-update.d.ts +10 -0
  242. package/dist/toolroll-update.js +50 -3
  243. package/dist/tools-ui.d.ts +5 -2
  244. package/dist/tools-ui.js +31 -31
  245. package/dist/when-html.d.ts +11 -3
  246. package/dist/when-html.js +5 -6
  247. package/dist/workspace-revision.js +3 -1
  248. package/dist/workspace-ui.d.ts +4 -2
  249. package/dist/workspace-ui.js +10 -10
  250. package/package.json +1 -1
  251. package/dist/server/shared.d.ts +0 -3151
  252. package/dist/server/shared.js +0 -12001
  253. package/dist/slack-state.d.ts +0 -13
  254. package/dist/slack-state.js +0 -10
@@ -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 });
@@ -127,11 +149,11 @@ 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
151
  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" });
152
+ get("flows", "teammate.page", `^\\/teammates\\/[1-9][0-9]{0,9}$`, { ...free, sample: "/teammates/1" });
153
+ get("flows", "teammate.soul-file", `^\\/teammates\\/[1-9][0-9]{0,9}\\/soul\\.md$`, { ...free, sample: "/teammates/1/soul.md" });
154
+ get("flows", "flow.insights", `^\\/flows\\/${FLOW}\\/insights$`, { project: "flow", limited: "resource", ...free, sample: "/flows/1/insights" });
155
+ get("flows", "flow.run", `^\\/flows\\/${FLOW}\\/runs\\/${FLOW}\\/${FLOW}$`, { project: "flow", limited: "resource", ...free, sample: "/flows/1/runs/2/3" });
132
156
  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
157
  get("flows", "flow.page", `^\\/flows\\/${FLOW}$`, { project: "flow", limited: "resource", ...free, sample: "/flows/1" });
136
158
  get("flows", "recipes", "/recipes", { limited: "collection", ...free });
137
159
  for (const one of ["run", "start", "new", "edit", "from-task", "preview", "export"])
@@ -140,26 +162,42 @@ for (const one of ["run", "start", "new", "edit", "from-task", "preview", "expor
140
162
  get("flows", "recipes.other", "^\\/recipes\\/.*$", { sample: "/recipes/unknown" });
141
163
  // v115: routines became scheduled flows; old links to them land on the flows page.
142
164
  get("tasks", "routines", "/routines", { limited: "collection" });
143
- get("chat", "chat.stream", "/chat/stream", free);
144
- get("chat", "chat.mate-status", "/chat/mate/status", free);
165
+ // 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.
166
+ edge("live", "live", "GET", "/live", { callers: ["cookie", "bearer"], scope: "read", project: "adapter", proof: "adapter" });
167
+ get("chat", "chat.mate-status", "/chat/mate/status", { ...free, role: "approver", roleBrowser: true, roleRefusal: SESSION_JSON });
145
168
  get("chat", "chat.demo-live", "/chat/demo/live", free);
146
169
  get("chat", "chat.page", "/chat", { project: "conversation", limited: "conversation", ...free });
147
170
  get("chat", "chat.ack", `^\\/chat\\/ack\\/${ID}$`, { sample: "/chat/ack/1" });
148
171
  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" });
172
+ get("settings", "control", "/control", anyApprover("Project setup requires an approver."));
173
+ get("settings", "control.connection", "/control/connection", anyApprover("Project setup requires an approver."));
174
+ get("chat", "chat.action", `^\\/chat\\/action\\/${ID}$`, { project: "proposal", limited: "proposal", ...free, sample: "/chat/action/1", ...approver("Sign in to review this action.", "/projects") });
175
+ const SETTINGS_PAGE_ROLES = {
176
+ integrations: approver("An installation approver sees integrations.", "/settings"),
177
+ monitoring: operator("An instance operator sets up monitoring.", "/settings"),
178
+ updates: operator("An instance operator updates Toolroll.", "/settings"),
179
+ retention: operator("An instance operator sets retention.", "/settings"),
180
+ storage: operator("An instance operator looks after storage.", "/settings"),
181
+ backups: operator("An instance operator looks after backups.", "/settings"),
182
+ data: operator("An instance operator exports data.", "/settings"),
183
+ "sign-in": operator("An instance operator sets up sign-in.", "/settings"),
184
+ lead: approver("An approver sets up the lead.", "/settings"),
185
+ teams: approver("An installation approver can connect Teams.", "/settings"),
186
+ discord: approver("An installation approver can connect Discord.", "/settings"),
187
+ slack: approver("An installation approver can connect Slack.", "/settings"),
188
+ "slack/manifest": approver("An installation approver can connect Slack.", "/settings"),
189
+ };
152
190
  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);
191
+ get(one === "sessions" ? "people" : "settings", `settings.${one}`, `/settings/${one}`, { ...free, ...SETTINGS_PAGE_ROLES[one] });
154
192
  }
155
193
  get("settings", "settings.skills", "/settings/skills", { limited: "unscoped", ...free });
156
194
  get("settings", "settings.flows", "/settings/flows", { limited: "collection", ...free });
157
195
  get("settings", "settings.knowledge", "/settings/knowledge", { limited: "unscoped", ...free });
158
196
  get("settings", "settings.learning", "/settings/learning", { limited: "unscoped", ...free });
159
- get("settings", "settings.telegram", "/settings/telegram", { limited: "self", ...free });
197
+ get("settings", "settings.telegram", "/settings/telegram", { limited: "self", ...free, ...approver("An approver can pair their own phone.", "/settings") });
160
198
  get("settings", "settings.page", "/settings", { limited: "unscoped", ...free });
161
- get("pages", "health", "/health", free);
162
- get("pages", "metrics", "/metrics", free);
199
+ get("pages", "health", "/health", { ...free, ...anyOperator("An instance operator reads server health.", "/") });
200
+ get("pages", "metrics", "/metrics", { ...free, ...anyOperator("An instance operator reads metrics.", "/") });
163
201
  get("chat", "push.key", "/push/key");
164
202
  get("tasks", "decision.page", `^\\/d\\/${ID}$`, { project: "decision", limited: "resource", ...free, sample: "/d/1" });
165
203
  get("chat", "lead.status", "/lead/status", free);
@@ -167,74 +205,115 @@ get("chat", "chat.task-status", "/chat/task-status", free);
167
205
  get("tasks", "decision.evidence", `^\\/d\\/${ID}\\/evidence\\/${ID}$`, { project: "decision", limited: "resource", ...free, sample: "/d/1/evidence/2" });
168
206
  // ---- console actions -------------------------------------------------------------------------------------------
169
207
  // 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" });
208
+ post("chat", "chat.demo.ask", "/chat/demo/ask");
209
+ for (const one of ["approve", "change", "revise", "complete"])
210
+ post("chat", `chat.demo.${one}`, `^\\/chat\\/demo\\/[0-9]{1,9}\\/${one}$`, { sample: `/chat/demo/1/${one}` });
171
211
  post("settings", "provider.resume", "^\\/providers\\/[a-z]+\\/resume$", { sample: "/providers/codex/resume" });
172
212
  post("settings", "settings.updates-dismiss", "/settings/updates/dismiss");
173
213
  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" });
214
+ const CODING_ACT = { scope: "step-up", project: "coding", limited: "self" };
215
+ post("tasks", "code.start", "/code/start", CODING_ACT);
216
+ for (const one of ["send", "stop", "resume", "recover", "continue", "ship", "answer"])
217
+ post("tasks", `code.${one}-send`, `^\\/code\\/${SESSION}\\/${one}$`, { ...CODING_ACT, sample: `/code/${"a".repeat(32)}/${one}` });
218
+ // Any other coding action answers "not found" after the same access check.
219
+ post("tasks", "code.act-other", "^\\/code(\\/.*)?$", { ...CODING_ACT, sample: "/code/unknown" });
220
+ post("settings", "settings.skills-revise", "/settings/skills/revise", { project: "form", limited: "unscoped", ...approver("Sign in to revise a test.", "/settings/skills") });
221
+ 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") });
222
+ post("settings", "settings.skills-change", "/settings/skills/change", { project: "form", limited: "unscoped", ...approver("Sign in as an approver to manage skills.", "/settings/skills") });
223
+ for (const one of ["setup", "sample", "github"])
224
+ 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") });
225
+ const TEAMMATES = anyApprover("Sign in as an approver to look after teammates.", "/teammates");
226
+ post("flows", "teammate.new", "/teammates/new", { project: "form", ...TEAMMATES });
227
+ for (const one of ["soul", "state", "note", "settings", "summary", "tools", "memory", "routines", "week"])
228
+ post("flows", `teammate.${one}`, `^\\/teammates\\/[1-9][0-9]{0,9}\\/${one}$`, { sample: `/teammates/1/${one}`, ...TEAMMATES });
181
229
  post("flows", "teammate.answer", "^\\/teammates\\/questions\\/[1-9][0-9]{0,9}\\/answer$", { sample: "/teammates/questions/1/answer" });
182
230
  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" });
231
+ // Flow editing answers JSON: a caller without approver standing hears it in the editor's own format.
232
+ const CREATE_FLOWS = approver("Sign in as an approver to create flows.", "/flows");
233
+ 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." }) } };
234
+ post("flows", "flows.create", "/flows/new", { project: "form", limited: "resource", ...CREATE_FLOWS });
235
+ post("flows", "flows.example", "/flows/example", { project: "form", ...CREATE_FLOWS });
236
+ post("flows", "flows.import", "/flows/import", { project: "form", limited: "resource", bodyCap: LARGE_FORM, ...CREATE_FLOWS });
237
+ for (const one of ["save", "cards", "archive", "scripts"])
238
+ post("flows", `flow.${one}`, `^\\/flows\\/${FLOW}\\/${one}$`, { project: "flow", limited: "resource", sample: `/flows/1/${one}`, ...EDIT_FLOWS });
239
+ for (const one of ["linear-key", "hooks-address", "secrets"])
240
+ post("flows", `flow.${one}`, `^\\/flows\\/${FLOW}\\/${one}$`, { scope: "step-up", project: "flow", sample: `/flows/1/${one}`, ...EDIT_FLOWS });
241
+ post("flows", "flow.triggers", `^\\/flows\\/${FLOW}\\/triggers$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers", ...EDIT_FLOWS });
242
+ for (const one of ["move", "decide", "cancel", "comment", "assign", "watch"])
243
+ post("flows", `flow.card.${one}`, `^\\/flows\\/${FLOW}\\/cards\\/${FLOW}\\/${one}$`, { project: "flow", limited: "resource", sample: `/flows/1/cards/2/${one}`, ...EDIT_FLOWS });
244
+ post("flows", "flow.card.choose", `^\\/flows\\/${FLOW}\\/cards\\/${FLOW}\\/choose$`, { project: "flow", sample: "/flows/1/cards/2/choose", ...EDIT_FLOWS });
245
+ post("flows", "flow.trigger.pause", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/pause$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/pause", ...EDIT_FLOWS });
246
+ post("flows", "flow.trigger.resume", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/resume$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/resume", ...EDIT_FLOWS });
247
+ post("flows", "flow.trigger.remove", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/remove$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/remove", ...EDIT_FLOWS });
248
+ post("flows", "flow.trigger.check", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/check$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/check", ...EDIT_FLOWS });
249
+ post("flows", "flow.trigger.press", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/press$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/press", ...EDIT_FLOWS });
250
+ post("flows", "flow.trigger.renew", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/renew$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/renew", ...EDIT_FLOWS });
251
+ 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 });
252
+ post("flows", "flow.trigger.share", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/share$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/share", ...EDIT_FLOWS });
253
+ post("flows", "flow.trigger.unshare", `^\\/flows\\/${FLOW}\\/triggers\\/${FLOW}\\/unshare$`, { project: "flow", limited: "resource", sample: "/flows/1/triggers/2/unshare", ...EDIT_FLOWS });
254
+ post("settings", "spend.budget", "/spend/budget", { scope: "step-up", ...operator("An instance operator sets budgets.", "/spend") });
201
255
  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");
256
+ post("settings", "settings.project-delete", "/settings/project/delete", { scope: "step-up", ...operator("An instance operator deletes projects.", "/settings/project") });
257
+ post("pages", "ledger.checkpoint", "/ledger/checkpoint", operator("An instance operator makes ledger checkpoints.", "/ledger"));
258
+ const SETTINGS_ACT_ROLES = {
259
+ policy: operator("An instance operator sets the policy.", "/settings/policy"),
260
+ approval: operator("An instance operator sets approval rules.", "/settings/approval"),
261
+ "request-limits": operator("An instance operator sets request limits.", "/settings/sessions"),
262
+ "sign-in": operator("An instance operator sets up sign-in.", "/settings"),
263
+ updates: operator("An instance operator updates Toolroll.", "/settings"),
264
+ "updates/seen": operator("An instance operator updates Toolroll.", "/settings"),
265
+ "updates/cancel": operator("An instance operator updates Toolroll.", "/settings"),
266
+ retention: operator("An instance operator sets retention.", "/settings"),
267
+ storage: operator("An instance operator looks after storage.", "/settings"),
268
+ "storage/clean": operator("An instance operator looks after storage.", "/settings"),
269
+ "storage/discard": operator("An instance operator looks after storage.", "/settings"),
270
+ "backups/now": operator("An instance operator looks after backups.", "/settings"),
271
+ backups: operator("An instance operator looks after backups.", "/settings"),
272
+ data: operator("An instance operator exports data.", "/settings"),
273
+ "integrations/test": approver("An installation approver tests integrations.", "/settings"),
274
+ monitoring: operator("An instance operator sets up monitoring.", "/settings"),
275
+ "tools/connect": approver("Sign in as an approver to connect tools.", "/settings/tools"),
276
+ "tools/change": approver("Sign in as an approver to manage tools.", "/settings/tools"),
277
+ };
204
278
  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" } : {}) });
279
+ 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
280
  }
207
- post("settings", "settings.flows-on", "/settings/flows/on", { limited: "collection" });
281
+ post("settings", "settings.flows-on", "/settings/flows/on", { limited: "collection", ...approver("An approver switches starter flows on.", "/settings/flows") });
282
+ const MODELS = approver("Sign in as an approver to change models.", "/settings/models");
283
+ for (const one of ["check", "watch", "update", "agent"])
284
+ post("settings", `settings.models-${one}`, `/settings/models/${one}`, MODELS);
208
285
  // 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" });
286
+ post("settings", "settings.models-act", "^\\/settings\\/models\\/.*$", { sample: "/settings/models/unknown", ...MODELS });
287
+ post("settings", "settings.knowledge-refresh", "/settings/knowledge/refresh", { project: "form", limited: "unscoped", ...approver("Sign in as an approver to refresh project context.", "/settings/knowledge") });
288
+ post("settings", "settings.knowledge-proposal", "/settings/knowledge/proposal", { project: "form", ...approver("Sign in as an approver to decide memory proposals.", "/settings/knowledge") });
289
+ post("settings", "settings.knowledge-decision", "/settings/knowledge/decision", { project: "form", ...approver("Sign in as an approver to change project decisions.", "/settings/knowledge") });
290
+ 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
291
  post("settings", "settings.learning-change", "/settings/learning/change", { project: "form", limited: "unscoped" });
215
292
  for (const one of ["setup-preview", "setup-approve", "instructions-preview", "instructions-approve"])
216
293
  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" });
294
+ for (const [service, name] of [["slack", "Slack"], ["teams", "Teams"], ["discord", "Discord"]]) {
295
+ for (const one of ["connect", "pair", "unpair", "disconnect", "alerts"])
296
+ post("settings", `settings.${service}-${one}`, `/settings/${service}/${one}`, { scope: "step-up", ...approver(`An installation approver can connect ${name}.`, "/settings") });
297
+ }
298
+ post("settings", "settings.telegram-retry", "/settings/telegram/retry", { limited: "self", scope: "step-up", ...approver("An approver can retry their own replies.", "/settings") });
220
299
  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" });
300
+ 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") });
301
+ post("settings", "settings.telegram-pair", "/settings/telegram/pair", { limited: "self", scope: "step-up", ...approver("An approver can pair their own phone.", "/settings") });
302
+ post("settings", "settings.telegram-unpair", "/settings/telegram/unpair", { limited: "self", scope: "step-up", ...approver("An approver can pair their own phone.", "/settings") });
224
303
  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
304
  post("settings", `settings.${one.replaceAll("/", "-")}-send`, `/settings/${one}`);
226
305
  }
227
306
  post("pages", "projects.select", "/projects/select", { project: "form-path", limited: "unscoped", viewer: true });
228
- post("pages", "projects.remove", "/projects/remove", { project: "form" });
307
+ post("pages", "projects.remove", "/projects/remove", { project: "form", ...operator("An instance operator removes projects.", "/settings/project") });
229
308
  post("pages", "projects.open", "/projects/open", { project: "form-path" });
230
- post("tasks", "tasks.add", "/tasks/add", { project: "form", limited: "collection" });
309
+ post("tasks", "tasks.add", "/tasks/add", { project: "form", limited: "collection", bodyCap: LARGE_FORM });
231
310
  post("tasks", "queue.move", "/queue/move");
232
311
  post("tasks", "queue.note", "/queue/note");
233
312
  post("pages", "fleet.register", "/fleet/runner/register", { scope: "step-up" });
234
313
  post("settings", "mode.confirm", "/mode/confirm", { scope: "step-up" });
235
314
  post("settings", "mode.sign", "/mode/sign", { scope: "step-up" });
236
315
  post("settings", "mode.revoke", "/mode/revoke");
237
- post("people", "people.projects", "/people/projects", { scope: "step-up" });
316
+ post("people", "people.projects", "/people/projects", { scope: "step-up", ...operator("An instance operator manages project access.", "/people") });
238
317
  post("people", "people.invite", "/people/invite", { scope: "step-up" });
239
318
  post("people", "people.invite-revoke", "/people/invite-revoke", { scope: "step-up" });
240
319
  post("people", "people.revoke", "/people/revoke", { scope: "step-up" });
@@ -245,7 +324,7 @@ post("tasks", "task.act.hold", `^\\/t\\/${TASK}\\/hold$`, { project: "task", lim
245
324
  post("tasks", "task.act.unhold", `^\\/t\\/${TASK}\\/unhold$`, { project: "task", limited: "resource", sample: "/t/one/unhold" });
246
325
  post("tasks", "task.act.requeue", `^\\/t\\/${TASK}\\/requeue$`, { project: "task", limited: "resource", sample: "/t/one/requeue" });
247
326
  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" });
327
+ post("tasks", "task.act.scope", `^\\/t\\/${TASK}\\/scope$`, { project: "task", limited: "resource", sample: "/t/one/scope", bodyCap: LARGE_FORM });
249
328
  post("tasks", "task.act.approve", `^\\/t\\/${TASK}\\/approve$`, { scope: "step-up", project: "task", limited: "resource", sample: "/t/one/approve" });
250
329
  post("tasks", "task.act.plan", `^\\/t\\/${TASK}\\/plan$`, { project: "task", limited: "resource", sample: "/t/one/plan" });
251
330
  post("tasks", "task.act.plan-edit", `^\\/t\\/${TASK}\\/plan-edit$`, { project: "task", limited: "resource", sample: "/t/one/plan-edit" });
@@ -257,33 +336,38 @@ post("tasks", "task.act.accept-revision", `^\\/t\\/${TASK}\\/accept-revision$`,
257
336
  post("tasks", "task.act.reject-revision", `^\\/t\\/${TASK}\\/reject-revision$`, { scope: "step-up", project: "task", limited: "resource", sample: "/t/one/reject-revision" });
258
337
  post("tasks", "task.act.route", `^\\/t\\/${TASK}\\/route$`, { project: "task", limited: "resource", sample: "/t/one/route" });
259
338
  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" });
339
+ 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.") });
340
+ post("tasks", "task.act.merge", `^\\/t\\/${TASK}\\/merge$`, { project: "task", limited: "resource", sample: "/t/one/merge", ...approver("Only an approver can merge.") });
341
+ 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
342
  post("tasks", "task.act.stop", `^\\/t\\/${TASK}\\/stop$`, { project: "task", limited: "resource", sample: "/t/one/stop" });
264
343
  post("tasks", "task.act.resume-arm", `^\\/t\\/${TASK}\\/resume-arm$`, { project: "task", limited: "resource", sample: "/t/one/resume-arm" });
265
344
  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" });
345
+ for (const one of ["block", "unblock", "repair-dependency", "follow-up"])
346
+ post("tasks", `task.act.${one}`, `^\\/t\\/${TASK}\\/${one}$`, { project: "task", sample: `/t/one/${one}` });
267
347
  // Onboarding names a GitHub repository, not a local project. Its root-index/nonce ceremony proves the future destination.
268
348
  post("pages", "projects.onboard-preview", "/projects/onboard-preview", { project: "none" });
269
349
  post("pages", "projects.onboard-confirm", "/projects/onboard-confirm", { scope: "step-up", project: "none" });
270
350
  post("chat", "push.subscribe", "/push/subscribe", { scope: "step-up" });
271
351
  post("chat", "push.remove", "/push/remove");
272
- post("chat", "onboarding.phone-dismiss", "/onboarding/phone/dismiss");
352
+ post("chat", "onboarding.phone-dismiss", "/onboarding/phone/dismiss", approver("An approver puts this away.", "/chat"));
353
+ 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
354
  for (const one of ["identity", "about", "promise/cancel", "on"])
274
- post("settings", `settings.lead-${one.replaceAll("/", "-")}`, `/settings/lead/${one}`);
355
+ post("settings", `settings.lead-${one.replaceAll("/", "-")}`, `/settings/lead/${one}`, approver(LEAD_WORDS[one], "/settings/lead"));
275
356
  post("chat", "chat.config", "/chat/config", { scope: "step-up" });
276
357
  for (const one of ["mint", "follow", "end", "stop"])
277
358
  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" });
359
+ for (const one of ["confirm", "dismiss"])
360
+ post("chat", `chat.proposal.${one}`, `^\\/chat\\/proposal\\/${ID}\\/${one}$`, { project: "proposal", limited: "proposal", sample: `/chat/proposal/1/${one}` });
361
+ for (const one of ["confirm", "dismiss"])
362
+ post("chat", `coordinator.proposal.${one}`, `^\\/proposals\\/${ID}\\/${one}$`, { sample: `/proposals/1/${one}` });
280
363
  post("chat", "chat.send", "/chat", { scope: "step-up", project: "conversation" });
281
364
  post("chat", "chat.file", "^\\/chat\\/file\\/[0-9a-f]{32}$", { scope: "step-up", sample: `/chat/file/${"a".repeat(32)}` });
282
365
  post("chat", "chat.ack-send", `^\\/chat\\/ack\\/${ID}$`, { scope: "step-up", sample: "/chat/ack/1" });
283
366
  for (const one of ["prepare", "preview", "import", "save"])
284
367
  post("flows", `recipes.${one}-send`, `/recipes/${one}`, { limited: "collection" });
285
368
  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" });
369
+ for (const one of ["note", "comment", "revise", "draft-repair", "checks", "add-tests"])
370
+ post("tasks", `run.act.${one}`, `^\\/r\\/${ID}\\/${one}$`, { project: "run", limited: "resource", sample: `/r/1/${one}` });
287
371
  post("tasks", "session.editor-links", "/session/editor-links", { viewer: true });
288
372
  post("tasks", "incident.resolve", `^\\/i\\/${ID}\\/resolve$`, { project: "incident", sample: "/i/1/resolve" });
289
373
  /** The sole route table, in match order. */
@@ -321,6 +405,10 @@ export function assertRouteTable(routes = ROUTES) {
321
405
  throw new Error(`route table: ${route.id} cannot bypass console admission`);
322
406
  if (route.callers.length === 0)
323
407
  throw new Error(`route table: ${route.id} admits no caller`);
408
+ if ((route.role === "any") !== (route.roleRefusal === undefined))
409
+ throw new Error(`route table: ${route.id} role and its refusal go together`);
410
+ if (route.role !== "any" && route.stage !== "console")
411
+ throw new Error(`route table: ${route.id} edge routes prove roles in their protocol`);
324
412
  const test = new RegExp(route.pattern);
325
413
  if (!test.test(route.sample))
326
414
  throw new Error(`route table: ${route.id} does not match its sample ${route.sample}`);