toolroll 0.9.85 → 0.9.87

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 (220) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/dist/agent-fence.d.ts +6 -2
  3. package/dist/agent-fence.js +3 -2
  4. package/dist/agents-board.d.ts +12 -0
  5. package/dist/agents-board.js +26 -0
  6. package/dist/approval-rules-ui.d.ts +4 -1
  7. package/dist/approval-rules-ui.js +31 -15
  8. package/dist/assignment-ui.js +1 -1
  9. package/dist/assignment.js +4 -3
  10. package/dist/backup-ui.d.ts +5 -4
  11. package/dist/backup-ui.js +65 -32
  12. package/dist/browser/terminal.css +1 -1
  13. package/dist/browser/terminal.js +57 -54
  14. package/dist/browser/workspace.css +2 -2
  15. package/dist/browser/workspace.js +35 -68
  16. package/dist/browser-crew.js +1 -1
  17. package/dist/browser-workspace.d.ts +363 -177
  18. package/dist/browser-workspace.js +10 -9
  19. package/dist/chat-actions.d.ts +11 -1
  20. package/dist/chat-actions.js +53 -1
  21. package/dist/chat-channel.js +1 -0
  22. package/dist/chat-controls.d.ts +1 -1
  23. package/dist/chat-controls.js +1 -1
  24. package/dist/chat-quiet.js +3 -2
  25. package/dist/chat-settings-page.d.ts +29 -9
  26. package/dist/chat-settings-page.js +59 -28
  27. package/dist/check-levels-ui.d.ts +2 -1
  28. package/dist/check-levels-ui.js +67 -58
  29. package/dist/claim.d.ts +3 -0
  30. package/dist/claim.js +4 -1
  31. package/dist/contracts/chat-actions.d.ts +9 -0
  32. package/dist/contracts/chat-actions.js +3 -0
  33. package/dist/contracts/cli.d.ts +43 -0
  34. package/dist/contracts/cli.js +4 -0
  35. package/dist/contracts/console-api.d.ts +4 -1
  36. package/dist/contracts/console-api.js +4 -1
  37. package/dist/contracts/flow-propose.d.ts +4 -1
  38. package/dist/contracts/flow-propose.js +1 -1
  39. package/dist/contracts/flow.d.ts +41 -0
  40. package/dist/contracts/flow.js +4 -2
  41. package/dist/contracts/lead-tools.d.ts +21 -0
  42. package/dist/contracts/lead-tools.js +2 -0
  43. package/dist/contracts/store-json.d.ts +2 -0
  44. package/dist/contracts/trigger-payloads.d.ts +20 -0
  45. package/dist/contracts/trigger-payloads.js +4 -0
  46. package/dist/coordinator.js +1 -0
  47. package/dist/credentials-ui.d.ts +5 -3
  48. package/dist/credentials-ui.js +58 -31
  49. package/dist/deploy-gate.js +4 -0
  50. package/dist/design/kit.css +685 -0
  51. package/dist/design/kit.d.ts +208 -0
  52. package/dist/design/kit.js +177 -0
  53. package/dist/design/tokens.css +19 -1
  54. package/dist/discord-settings.js +8 -4
  55. package/dist/dispatch.d.ts +3 -0
  56. package/dist/dispatch.js +4 -1
  57. package/dist/export-ui.d.ts +6 -4
  58. package/dist/export-ui.js +18 -8
  59. package/dist/flow-browser.d.ts +103 -0
  60. package/dist/flow-browser.js +204 -0
  61. package/dist/flow-changed.d.ts +14 -0
  62. package/dist/flow-changed.js +53 -0
  63. package/dist/flow-code.d.ts +5 -1
  64. package/dist/flow-code.js +4 -1
  65. package/dist/flow-engine.js +16 -0
  66. package/dist/flow-gallery-ui.d.ts +41 -4
  67. package/dist/flow-gallery-ui.js +95 -63
  68. package/dist/flow-live.js +9 -1
  69. package/dist/flow-pull-request.js +15 -7
  70. package/dist/flow-share.js +3 -1
  71. package/dist/flow-starters-ui.d.ts +3 -9
  72. package/dist/flow-starters-ui.js +12 -23
  73. package/dist/flow-steps.d.ts +9 -1
  74. package/dist/flow-steps.js +55 -5
  75. package/dist/flow-triggers.js +55 -0
  76. package/dist/flow-unattended-words.d.ts +20 -0
  77. package/dist/flow-unattended-words.js +24 -0
  78. package/dist/flow-unattended.d.ts +9 -5
  79. package/dist/flow-unattended.js +45 -23
  80. package/dist/flows-cli.d.ts +19 -0
  81. package/dist/flows-cli.js +54 -5
  82. package/dist/flows-ui.d.ts +12 -6
  83. package/dist/flows-ui.js +152 -32
  84. package/dist/flows.js +5 -2
  85. package/dist/integrations-ui.d.ts +6 -4
  86. package/dist/integrations-ui.js +34 -40
  87. package/dist/integrations.d.ts +2 -2
  88. package/dist/integrations.js +4 -4
  89. package/dist/kits-ui.d.ts +8 -10
  90. package/dist/kits-ui.js +29 -33
  91. package/dist/kits.js +1 -1
  92. package/dist/knowledge-ui.js +76 -19
  93. package/dist/lead-tools.js +35 -6
  94. package/dist/lead-voice.js +16 -5
  95. package/dist/ledger-view.d.ts +2 -0
  96. package/dist/ledger-view.js +41 -38
  97. package/dist/models-ui.d.ts +8 -3
  98. package/dist/models-ui.js +82 -59
  99. package/dist/monitoring-ui.d.ts +4 -2
  100. package/dist/monitoring-ui.js +44 -13
  101. package/dist/needs-you.d.ts +1 -1
  102. package/dist/needs-you.js +8 -0
  103. package/dist/operate-remote-arguments.js +1 -0
  104. package/dist/operate-remote.js +1 -1
  105. package/dist/operate.d.ts +1 -1
  106. package/dist/operate.js +249 -6
  107. package/dist/passwordless.d.ts +1 -1
  108. package/dist/passwordless.js +1 -1
  109. package/dist/people-audit-ui.d.ts +2 -1
  110. package/dist/people-audit-ui.js +25 -23
  111. package/dist/policy-ui.d.ts +3 -2
  112. package/dist/policy-ui.js +27 -21
  113. package/dist/policy.d.ts +3 -0
  114. package/dist/policy.js +14 -1
  115. package/dist/project-delete-ui.d.ts +4 -2
  116. package/dist/project-delete-ui.js +135 -126
  117. package/dist/project-delete.js +7 -3
  118. package/dist/project-knowledge.d.ts +10 -10
  119. package/dist/publish.d.ts +11 -0
  120. package/dist/publish.js +16 -3
  121. package/dist/pull-request-flow.js +35 -4
  122. package/dist/pull-request-ui.d.ts +2 -1
  123. package/dist/pull-request-ui.js +39 -28
  124. package/dist/recipe-ui.d.ts +4 -2
  125. package/dist/recipe-ui.js +14 -4
  126. package/dist/release-adapters.d.ts +3 -0
  127. package/dist/release-adapters.js +22 -2
  128. package/dist/release-cli.js +81 -9
  129. package/dist/release.d.ts +21 -0
  130. package/dist/release.js +40 -4
  131. package/dist/releases.js +1 -1
  132. package/dist/request-budget-ui.d.ts +4 -2
  133. package/dist/request-budget-ui.js +30 -19
  134. package/dist/result-follow-ups.js +4 -0
  135. package/dist/retention-ui.d.ts +4 -3
  136. package/dist/retention-ui.js +15 -12
  137. package/dist/scope.d.ts +8 -0
  138. package/dist/scope.js +13 -0
  139. package/dist/serve.d.ts +0 -1
  140. package/dist/serve.js +51 -7
  141. package/dist/server/agents-computers.d.ts +9 -0
  142. package/dist/server/agents-computers.js +67 -0
  143. package/dist/server/chat.js +20 -18
  144. package/dist/server/chrome.d.ts +1 -1
  145. package/dist/server/chrome.js +19 -9
  146. package/dist/server/console.css +31 -2
  147. package/dist/server/flows.js +79 -36
  148. package/dist/server/home.d.ts +35 -0
  149. package/dist/server/home.js +104 -0
  150. package/dist/server/http.d.ts +3 -0
  151. package/dist/server/http.js +5 -0
  152. package/dist/server/kit-standalone.d.ts +22 -0
  153. package/dist/server/kit-standalone.js +69 -0
  154. package/dist/server/kit.d.ts +405 -0
  155. package/dist/server/kit.js +361 -0
  156. package/dist/server/pages.d.ts +2 -1
  157. package/dist/server/pages.js +213 -174
  158. package/dist/server/people-tokens.js +26 -85
  159. package/dist/server/render-history.d.ts +51 -0
  160. package/dist/server/render-history.js +228 -0
  161. package/dist/server/render-new-task.d.ts +29 -0
  162. package/dist/server/render-new-task.js +96 -0
  163. package/dist/server/render-next.d.ts +63 -0
  164. package/dist/server/render-next.js +109 -0
  165. package/dist/server/render-pages.d.ts +35 -190
  166. package/dist/server/render-pages.js +154 -563
  167. package/dist/server/render-people.d.ts +24 -3
  168. package/dist/server/render-people.js +93 -72
  169. package/dist/server/render-projects.d.ts +103 -0
  170. package/dist/server/render-projects.js +247 -0
  171. package/dist/server/render-results.d.ts +5 -24
  172. package/dist/server/render-results.js +48 -185
  173. package/dist/server/render-settings.d.ts +5 -74
  174. package/dist/server/render-settings.js +102 -493
  175. package/dist/server/render-tasks.d.ts +43 -5
  176. package/dist/server/render-tasks.js +131 -91
  177. package/dist/server/route-table.js +31 -5
  178. package/dist/server/settings.js +276 -255
  179. package/dist/server/task-views.d.ts +8 -0
  180. package/dist/server/task-views.js +90 -2
  181. package/dist/server/tasks.d.ts +18 -0
  182. package/dist/server/tasks.js +148 -41
  183. package/dist/server/terminals.js +1 -1
  184. package/dist/server/welcome.d.ts +22 -0
  185. package/dist/server/welcome.js +69 -0
  186. package/dist/settings-groups-ui.d.ts +279 -0
  187. package/dist/settings-groups-ui.js +615 -0
  188. package/dist/settings-kit.d.ts +42 -6
  189. package/dist/settings-kit.js +69 -7
  190. package/dist/settings-proofs.js +1 -0
  191. package/dist/setup-guide.d.ts +2 -2
  192. package/dist/skills-ui.js +59 -7
  193. package/dist/slack-settings.js +12 -5
  194. package/dist/spend-ui.d.ts +45 -26
  195. package/dist/spend-ui.js +124 -56
  196. package/dist/sso-ui.d.ts +5 -2
  197. package/dist/sso-ui.js +63 -31
  198. package/dist/storage-ui.d.ts +3 -2
  199. package/dist/storage-ui.js +56 -50
  200. package/dist/store.d.ts +182 -5
  201. package/dist/store.js +512 -38
  202. package/dist/surface.js +23 -3
  203. package/dist/task-archive.d.ts +10 -0
  204. package/dist/task-archive.js +16 -0
  205. package/dist/task-control.d.ts +1 -1
  206. package/dist/teams-settings.d.ts +2 -1
  207. package/dist/teams-settings.js +24 -41
  208. package/dist/telegram-settings.d.ts +0 -4
  209. package/dist/telegram-settings.js +52 -25
  210. package/dist/tidy.d.ts +130 -0
  211. package/dist/tidy.js +503 -0
  212. package/dist/toolroll-update-ui.d.ts +8 -4
  213. package/dist/toolroll-update-ui.js +55 -45
  214. package/dist/tools-ui.d.ts +5 -3
  215. package/dist/tools-ui.js +107 -42
  216. package/dist/work-index.d.ts +24 -0
  217. package/dist/work-index.js +56 -30
  218. package/dist/workspace-ui.d.ts +1 -1
  219. package/dist/workspace-ui.js +8 -4
  220. package/package.json +1 -1
@@ -0,0 +1,103 @@
1
+ import { type FlowDefinition } from "./flows.js";
2
+ import type { FlowRow, FlowScriptRow, Store } from "./store.js";
3
+ /** v144: each approval of browser use, live or ended. `digest` is browserApprovalDigest of what was approved. Additive only. */
4
+ export declare const FLOW_BROWSER_SCHEMA = "\nCREATE TABLE IF NOT EXISTS flow_browser_grant (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n flow INTEGER NOT NULL,\n repo TEXT NOT NULL,\n flow_revision INTEGER NOT NULL,\n flow_digest TEXT NOT NULL,\n digest TEXT NOT NULL,\n zones_json TEXT NOT NULL,\n signed_by TEXT NOT NULL,\n signed_at TEXT NOT NULL,\n proof TEXT NOT NULL,\n revoked_at TEXT,\n revoked_by TEXT\n);\nCREATE UNIQUE INDEX IF NOT EXISTS flow_browser_live ON flow_browser_grant (flow) WHERE revoked_at IS NULL;\n";
5
+ /** One zone an approval covers: the zone, and the script it runs (name, version and content digest). */
6
+ export type BrowserZone = {
7
+ zone: string;
8
+ script: string;
9
+ version: number;
10
+ digest: string;
11
+ };
12
+ /** Everything an approval is about: the flow, its project, the drawing (revision and digest) and every browser zone. */
13
+ export type BrowserTerms = {
14
+ flow: number;
15
+ repo: string;
16
+ revision: number;
17
+ flowDigest: string;
18
+ zones: BrowserZone[];
19
+ };
20
+ export type FlowBrowserGrant = BrowserTerms & {
21
+ id: number;
22
+ digest: string;
23
+ signedBy: string;
24
+ signedAt: string;
25
+ proof: string;
26
+ };
27
+ /** The approval's digest: domain-separated over the whole of its terms, so a changed flow, drawing, revision or any
28
+ * covered script (a restored one is a new version) is a different approval. */
29
+ export declare function browserApprovalDigest(terms: BrowserTerms): string;
30
+ /** The flow's script zones that say they start a browser, each with the script it runs now; null when one's script is
31
+ * missing, waiting for approval, or runs a project file (an approval seals the script's own text, never a file the
32
+ * project can change). */
33
+ export declare function browserZonesOf(store: Store, flow: FlowRow, definition: FlowDefinition): BrowserZone[] | null;
34
+ /** What an approval of this flow would be about now, read from the store; null when it can't be approved. */
35
+ export declare function browserTermsOf(store: Store, flow: FlowRow): BrowserTerms | null;
36
+ /** The flow's live approval, as recorded (whether it still stands is browserStanding's question). */
37
+ export declare function liveBrowserGrant(store: Store, flowId: number): FlowBrowserGrant | null;
38
+ /**
39
+ * Whether `zone` of this flow may start a browser now. `run`, when given, is what the caller is about to run — the
40
+ * drawing it read and the very script object it hands the launch — and the answer is about exactly that. The live
41
+ * approval stands only when its digest is the approval this flow would be now (same flow, project, drawing revision and
42
+ * digest, and every browser zone's script name, version and digest), it was a person's, and its signer is still an
43
+ * approver who reaches the project; `run`'s drawing must be that drawing and its script that zone's script. Anything
44
+ * unreadable, changed or mismatched is a no, with the plain words a card waits with.
45
+ */
46
+ export declare function browserStanding(store: Store, flowId: number, zone: string, run?: {
47
+ definition: FlowDefinition;
48
+ script: FlowScriptRow;
49
+ }): {
50
+ allowed: true;
51
+ } | {
52
+ allowed: false;
53
+ said: string;
54
+ };
55
+ /** What approving means, in words the person reads before they give their password. */
56
+ export declare function browserTerms(zones: readonly BrowserZone[]): string[];
57
+ /**
58
+ * Approve browser use for the flow as it stands. `proof` is the signer's credential method, proved by the caller just
59
+ * now; only a person's counts (a lead's or remote run's token is refused), from an approver who reaches the project.
60
+ * Re-reads the flow, its zones and scripts in the one transaction that writes; `sawDigest` (required) is the approval
61
+ * (browserApprovalDigest: drawing and scripts) the person read, and anything else is refused. Replaces any live one.
62
+ */
63
+ export declare function signFlowBrowser(store: Store, input: {
64
+ flow: number;
65
+ signer: string;
66
+ proof: string | null;
67
+ sawDigest: string;
68
+ /** The console and chat: the signer's account must still be this generation (the session that proved it) and reach every project. */
69
+ console?: {
70
+ generation: number;
71
+ };
72
+ }, now: Date): {
73
+ ok: true;
74
+ grant: FlowBrowserGrant;
75
+ } | {
76
+ ok: false;
77
+ message: string;
78
+ };
79
+ /** Any approver on the project ends it, with no password: lowering authority. False when there was none. */
80
+ export declare function revokeFlowBrowser(store: Store, flowId: number, by: string, now: Date): boolean;
81
+ /** What the flow page, `flows show` and chat say about browser use: null when no zone says it starts a browser. */
82
+ export type BrowserView = {
83
+ /** allowed: the approval stands now; ended: there is one, but it no longer stands; none: no one has approved. */
84
+ status: "allowed" | "ended" | "none";
85
+ /** The one line every surface shows. */
86
+ words: string;
87
+ signedBy: string | null;
88
+ signedAt: string | null;
89
+ /** The browser zones as they stand now, each with its title. Empty while one can't be approved (see `problem`). */
90
+ zones: (BrowserZone & {
91
+ title: string;
92
+ })[];
93
+ /** What approving means (browserTerms), and the approval digest the step-up must carry back. */
94
+ terms: string[];
95
+ digest: string | null;
96
+ /** Why it can't be approved as it stands, or null. */
97
+ problem: string | null;
98
+ };
99
+ /**
100
+ * Browser use for one flow, read from the store (no writes: page visits call it). Whether it is allowed is exactly
101
+ * browserStanding's answer for the flow's browser zones; the rest only says why in plain words.
102
+ */
103
+ export declare function browserViewOf(store: Store, flow: FlowRow): BrowserView | null;
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Browser use in a flow's script zone: a per-flow approval a person gives with their own proof, so that zone's launch
3
+ * gets the one sandbox rule a project check gets for Chromium (sandbox-runtime.ts SEATBELT_CHROMIUM) and nothing more.
4
+ *
5
+ * Two halves, both needed. The flow's drawing says which "Run a script" zones start a browser (`browser: true`, part of
6
+ * the flow's digest and said when the flow is confirmed); anyone who may change the flow can say it. Then a person, with
7
+ * their password or sign-in (never a lead's or remote run's token: person-proof.ts), approves browser use for the flow
8
+ * as it stands. The approval seals the flow's digest and, for each browser zone, its script's name and content digest.
9
+ * At launch (flow-steps.ts) the zone gets the rule only while all of that still holds: the same drawing, the same
10
+ * scripts, a person's proof, a signer who still reaches the project. Anything else gets nothing, and the card waits with
11
+ * a plain line naming the missing approval instead of Chromium crashing in the sandbox. Builds, checks, tool servers and
12
+ * every other flow launch (setup, triggers, zones that don't say browser) are untouched.
13
+ */
14
+ import { createHash } from "node:crypto";
15
+ import { canonicalJson } from "./canonical-json.js";
16
+ import { personProof } from "./person-proof.js";
17
+ import { flowDigest, validateFlowDefinition } from "./flows.js";
18
+ /** v144: each approval of browser use, live or ended. `digest` is browserApprovalDigest of what was approved. Additive only. */
19
+ export const FLOW_BROWSER_SCHEMA = `
20
+ CREATE TABLE IF NOT EXISTS flow_browser_grant (
21
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
22
+ flow INTEGER NOT NULL,
23
+ repo TEXT NOT NULL,
24
+ flow_revision INTEGER NOT NULL,
25
+ flow_digest TEXT NOT NULL,
26
+ digest TEXT NOT NULL,
27
+ zones_json TEXT NOT NULL,
28
+ signed_by TEXT NOT NULL,
29
+ signed_at TEXT NOT NULL,
30
+ proof TEXT NOT NULL,
31
+ revoked_at TEXT,
32
+ revoked_by TEXT
33
+ );
34
+ CREATE UNIQUE INDEX IF NOT EXISTS flow_browser_live ON flow_browser_grant (flow) WHERE revoked_at IS NULL;
35
+ `;
36
+ function definitionOf(flow) {
37
+ try {
38
+ return validateFlowDefinition(JSON.parse(flow.definitionJson), { stored: true });
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ }
44
+ /** The approval's digest: domain-separated over the whole of its terms, so a changed flow, drawing, revision or any
45
+ * covered script (a restored one is a new version) is a different approval. */
46
+ export function browserApprovalDigest(terms) {
47
+ return createHash("sha256").update(`toolroll:flow-browser:${canonicalJson(terms)}`, "utf8").digest("hex").slice(0, 32);
48
+ }
49
+ /** The flow's script zones that say they start a browser, each with the script it runs now; null when one's script is
50
+ * missing, waiting for approval, or runs a project file (an approval seals the script's own text, never a file the
51
+ * project can change). */
52
+ export function browserZonesOf(store, flow, definition) {
53
+ const out = [];
54
+ for (const stage of definition.stages) {
55
+ if (stage.kind !== "check" || stage.browser !== true)
56
+ continue;
57
+ const script = stage.script === null ? null : store.flowScript(flow.repo, stage.script);
58
+ if (script === null || script.held !== null || script.file !== null)
59
+ return null;
60
+ out.push({ zone: stage.id, script: script.name, version: script.version, digest: script.digest });
61
+ }
62
+ return out;
63
+ }
64
+ /** What an approval of this flow would be about now, read from the store; null when it can't be approved. */
65
+ export function browserTermsOf(store, flow) {
66
+ const definition = definitionOf(flow);
67
+ const zones = definition === null ? null : browserZonesOf(store, flow, definition);
68
+ return definition === null || zones === null ? null : { flow: flow.id, repo: flow.repo, revision: flow.revision, flowDigest: flowDigest(definition), zones };
69
+ }
70
+ function readGrant(row) {
71
+ if (row === undefined)
72
+ return null;
73
+ let zones;
74
+ try {
75
+ zones = JSON.parse(String(row["zones_json"]));
76
+ }
77
+ catch {
78
+ return null;
79
+ }
80
+ if (!Array.isArray(zones) || !zones.every(one => one !== null && typeof one === "object" && typeof one.zone === "string" && typeof one.script === "string" && Number.isInteger(one.version) && typeof one.digest === "string"))
81
+ return null;
82
+ return { id: Number(row["id"]), flow: Number(row["flow"]), repo: String(row["repo"]), revision: Number(row["flow_revision"]), flowDigest: String(row["flow_digest"]), digest: String(row["digest"]),
83
+ zones: zones, signedBy: String(row["signed_by"]), signedAt: String(row["signed_at"]), proof: String(row["proof"]) };
84
+ }
85
+ /** The flow's live approval, as recorded (whether it still stands is browserStanding's question). */
86
+ export function liveBrowserGrant(store, flowId) {
87
+ return readGrant(store.handle.prepare("SELECT * FROM flow_browser_grant WHERE flow = ? AND revoked_at IS NULL").get(flowId));
88
+ }
89
+ /** Whether `name` may approve on `repo` now: a live approver account that reaches the project. */
90
+ function approverNow(store, name, repo) {
91
+ const account = store.accountOf(name);
92
+ return account !== null && account.revokedAt === null && account.role === "approver" && store.accountCanAccess(name, repo);
93
+ }
94
+ /**
95
+ * Whether `zone` of this flow may start a browser now. `run`, when given, is what the caller is about to run — the
96
+ * drawing it read and the very script object it hands the launch — and the answer is about exactly that. The live
97
+ * approval stands only when its digest is the approval this flow would be now (same flow, project, drawing revision and
98
+ * digest, and every browser zone's script name, version and digest), it was a person's, and its signer is still an
99
+ * approver who reaches the project; `run`'s drawing must be that drawing and its script that zone's script. Anything
100
+ * unreadable, changed or mismatched is a no, with the plain words a card waits with.
101
+ */
102
+ export function browserStanding(store, flowId, zone, run) {
103
+ const flow = store.getFlow(flowId);
104
+ const definition = flow === null ? null : definitionOf(flow);
105
+ const stage = definition?.stages.find(one => one.id === zone);
106
+ if (flow === null || definition === null || stage === undefined || stage.kind !== "check" || stage.browser !== true)
107
+ return { allowed: false, said: "This zone doesn't say it starts a browser." };
108
+ const approve = `Approve it with: toolroll flows browser ${flow.id} on`;
109
+ const grant = liveBrowserGrant(store, flow.id);
110
+ if (grant === null)
111
+ return { allowed: false, said: `${stage.title} starts a browser, and no one has approved browser use for the ${flow.name} flow, so it didn't run. ${approve}` };
112
+ const terms = browserTermsOf(store, flow);
113
+ const now = terms?.zones.find(one => one.zone === zone);
114
+ const runStage = run?.definition.stages.find(one => one.id === zone);
115
+ const runs = run === undefined || (terms !== null && now !== undefined && flowDigest(run.definition) === terms.flowDigest && runStage?.kind === "check" && runStage.browser === true
116
+ && runStage.script === run.script.name && run.script.repo === flow.repo && run.script.held === null && run.script.file === null
117
+ && run.script.name === now.script && run.script.version === now.version && run.script.digest === now.digest);
118
+ const stands = runs && terms !== null && now !== undefined && flow.state === "active" && grant.digest === browserApprovalDigest(terms)
119
+ && browserApprovalDigest({ flow: grant.flow, repo: grant.repo, revision: grant.revision, flowDigest: grant.flowDigest, zones: grant.zones }) === grant.digest
120
+ && personProof(grant.proof) && approverNow(store, grant.signedBy, flow.repo);
121
+ return stands ? { allowed: true } : { allowed: false, said: `${stage.title} starts a browser, but browser use was approved for an earlier version of the ${flow.name} flow or its scripts, so it didn't run. ${approve}` };
122
+ }
123
+ /** What approving means, in words the person reads before they give their password. */
124
+ export function browserTerms(zones) {
125
+ return [
126
+ `Lets ${zones.map(one => `the ${one.script} script (zone ${one.zone})`).join(", ")} start Playwright's Chromium headless shell inside the flow's sandbox: the same one macOS service a project check gets for it, and nothing else.`,
127
+ "Changing the flow or one of these scripts (even back to how it was) ends it; nothing else gets it.",
128
+ ];
129
+ }
130
+ /**
131
+ * Approve browser use for the flow as it stands. `proof` is the signer's credential method, proved by the caller just
132
+ * now; only a person's counts (a lead's or remote run's token is refused), from an approver who reaches the project.
133
+ * Re-reads the flow, its zones and scripts in the one transaction that writes; `sawDigest` (required) is the approval
134
+ * (browserApprovalDigest: drawing and scripts) the person read, and anything else is refused. Replaces any live one.
135
+ */
136
+ export function signFlowBrowser(store, input, now) {
137
+ if (!personProof(input.proof))
138
+ return { ok: false, message: "Browser use needs a person's own approval: sign in with your password (a lead's or remote run's token can't give it)." };
139
+ return store.transact(() => {
140
+ const flow = store.getFlow(input.flow);
141
+ if (flow === null || flow.state !== "active")
142
+ return { ok: false, message: "No such flow." };
143
+ if (!approverNow(store, input.signer, flow.repo))
144
+ return { ok: false, message: "Only an approver on this flow's project can approve browser use." };
145
+ if (input.console !== undefined) {
146
+ const account = store.accountOf(input.signer);
147
+ if (account === null || account.generation !== input.console.generation)
148
+ return { ok: false, message: "Your access changed. Sign in again." };
149
+ if (account.projects !== null)
150
+ return { ok: false, message: "Allowing browser use needs an account with access to every project." };
151
+ }
152
+ if (definitionOf(flow) === null)
153
+ return { ok: false, message: "This flow's drawing can't be read." };
154
+ const terms = browserTermsOf(store, flow);
155
+ if (terms === null)
156
+ return { ok: false, message: "A zone that starts a browser has no script it can approve: missing, waiting for approval, or one that runs a project file (write the script on the zone instead)." };
157
+ if (terms.zones.length === 0)
158
+ return { ok: false, message: `No zone in ${flow.name} says it starts a browser. Mark the script zone first (browser: true), then approve.` };
159
+ const digest = browserApprovalDigest(terms);
160
+ if (input.sawDigest !== digest)
161
+ return { ok: false, message: "This flow or its scripts changed while you were reading. Read the terms again." };
162
+ store.handle.prepare("UPDATE flow_browser_grant SET revoked_at = ?, revoked_by = ? WHERE flow = ? AND revoked_at IS NULL").run(now.toISOString(), input.signer, flow.id);
163
+ const id = Number(store.handle.prepare("INSERT INTO flow_browser_grant (flow, repo, flow_revision, flow_digest, digest, zones_json, signed_by, signed_at, proof) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
164
+ .run(flow.id, flow.repo, terms.revision, terms.flowDigest, digest, JSON.stringify(terms.zones), input.signer, now.toISOString(), input.proof).lastInsertRowid);
165
+ store.recordAction({ at: now.toISOString(), actor: input.signer, repo: flow.repo, taskId: null, runId: null, source: "policy", action: "flow browser use: approved", outcome: "signed",
166
+ detail: `${flow.name} (flow #${flow.id}) · ${terms.zones.map(one => `${one.zone}: ${one.script} v${one.version}`).join(", ")}` });
167
+ return { ok: true, grant: { ...terms, id, digest, signedBy: input.signer, signedAt: now.toISOString(), proof: input.proof } };
168
+ });
169
+ }
170
+ /** Any approver on the project ends it, with no password: lowering authority. False when there was none. */
171
+ export function revokeFlowBrowser(store, flowId, by, now) {
172
+ return store.transact(() => {
173
+ const flow = store.getFlow(flowId);
174
+ const ended = store.handle.prepare("UPDATE flow_browser_grant SET revoked_at = ?, revoked_by = ? WHERE flow = ? AND revoked_at IS NULL").run(now.toISOString(), by, flowId).changes > 0;
175
+ if (ended)
176
+ store.recordAction({ at: now.toISOString(), actor: by, repo: flow?.repo ?? null, taskId: null, runId: null, source: "policy", action: "flow browser use: revoked", outcome: "revoked", detail: `flow #${flowId}` });
177
+ return ended;
178
+ });
179
+ }
180
+ const dayWords = (at) => new Date(at).toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" });
181
+ /**
182
+ * Browser use for one flow, read from the store (no writes: page visits call it). Whether it is allowed is exactly
183
+ * browserStanding's answer for the flow's browser zones; the rest only says why in plain words.
184
+ */
185
+ export function browserViewOf(store, flow) {
186
+ const definition = definitionOf(flow);
187
+ const stages = definition?.stages.filter(stage => stage.kind === "check" && stage.browser === true) ?? [];
188
+ if (definition === null || stages.length === 0)
189
+ return null;
190
+ const terms = browserTermsOf(store, flow);
191
+ const grant = liveBrowserGrant(store, flow.id);
192
+ const allowed = grant !== null && stages.every(stage => browserStanding(store, flow.id, stage.id).allowed);
193
+ const zones = (terms?.zones ?? []).map(zone => ({ ...zone, title: definition.stages.find(one => one.id === zone.zone)?.title ?? zone.zone }));
194
+ const problem = terms === null ? "A zone that starts a browser has no script that can be approved: it's missing, waiting for approval, or runs a project file (write the script on the zone instead)." : null;
195
+ const why = grant === null ? "" : terms === null || canonicalJson(grant.zones) !== canonicalJson(terms.zones) ? "the script changed"
196
+ : grant.flowDigest !== terms.flowDigest || grant.revision !== terms.revision ? "the flow changed"
197
+ : !approverNow(store, grant.signedBy, flow.repo) ? `${grant.signedBy} can no longer approve on this project` : "it no longer matches this flow";
198
+ return {
199
+ status: allowed ? "allowed" : grant === null ? "none" : "ended",
200
+ words: allowed ? `Browser: allowed by ${grant.signedBy}, ${dayWords(grant.signedAt)}` : grant === null ? "Browser: not allowed — runs will wait" : `Browser approval ended: ${why}`,
201
+ signedBy: grant?.signedBy ?? null, signedAt: grant?.signedAt ?? null,
202
+ zones, terms: terms === null ? [] : browserTerms(terms.zones), digest: terms === null ? null : browserApprovalDigest(terms), problem,
203
+ };
204
+ }
@@ -0,0 +1,14 @@
1
+ import type { Store } from "./store.js";
2
+ export type FlowChange = {
3
+ flow: number;
4
+ } | {
5
+ script: {
6
+ repo: string;
7
+ name: string;
8
+ };
9
+ };
10
+ /**
11
+ * Run `write` (the change itself) and end what it ends, all in one transaction. `write` answers whether it changed
12
+ * anything; when it didn't, nothing else happens. Returns what `write` returned.
13
+ */
14
+ export declare function flowChanged<T>(store: Store, change: FlowChange, by: string, now: Date, write: () => T, changed?: (result: T) => boolean): T;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The one road for a change to what a flow runs: its drawing saved, or a script one of its zones runs saved or removed.
3
+ * Every approval a person gave for the flow as it was ends here, in the same transaction as the write, with its ledger
4
+ * line, so the flow page, `flows show` and chat all say it ended and why:
5
+ * - "Runs on its own" (flow-unattended.ts): its grants end ("flow changed" / "script changed"), and the work they
6
+ * pre-authorized falls back to a person.
7
+ * - Browser use (flow-browser.ts): its approval seals the drawing and each browser zone's script, so the change itself
8
+ * stops it standing (browserStanding re-proves at every launch). Here it is said once in the ledger, for each
9
+ * flow whose approval stood before the write and doesn't after; the page shows it as expired with the reason.
10
+ * Nothing here widens anything: it only ends approvals.
11
+ */
12
+ import { browserViewOf } from "./flow-browser.js";
13
+ import { endUnattendedGrants } from "./flow-unattended.js";
14
+ /** The flows a change reaches: the flow itself, or every flow in the script's project with a zone that runs it. */
15
+ function flowsOf(store, change) {
16
+ if ("flow" in change) {
17
+ const flow = store.getFlow(change.flow);
18
+ return flow === null ? [] : [flow];
19
+ }
20
+ return store.listFlows([change.script.repo]).filter(flow => {
21
+ try {
22
+ const drawn = JSON.parse(flow.definitionJson);
23
+ return (drawn.stages ?? []).some(stage => stage.kind === "check" && stage.script === change.script.name);
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ });
29
+ }
30
+ /**
31
+ * Run `write` (the change itself) and end what it ends, all in one transaction. `write` answers whether it changed
32
+ * anything; when it didn't, nothing else happens. Returns what `write` returned.
33
+ */
34
+ export function flowChanged(store, change, by, now, write, changed = result => result === true) {
35
+ return store.transact(() => {
36
+ const before = new Map(flowsOf(store, change).map(flow => [flow.id, browserViewOf(store, flow)?.status === "allowed"]));
37
+ const result = write();
38
+ if (!changed(result))
39
+ return result;
40
+ if ("flow" in change)
41
+ endUnattendedGrants(store, { flow: change.flow }, by, "flow changed", now);
42
+ else
43
+ endUnattendedGrants(store, { script: change.script }, by, "script changed", now);
44
+ for (const [id, stood] of before) {
45
+ const flow = store.getFlow(id);
46
+ if (!stood || flow === null || browserViewOf(store, flow)?.status === "allowed")
47
+ continue;
48
+ store.recordAction({ at: now.toISOString(), actor: by, repo: flow.repo, taskId: null, runId: null, source: "policy", action: "flow browser use: ended", outcome: "ended",
49
+ detail: `${flow.name} (flow #${flow.id}) · ${"flow" in change ? "the flow changed" : `the ${change.script.name} script changed`}` });
50
+ }
51
+ return result;
52
+ });
53
+ }
@@ -9,6 +9,8 @@ export { LANGUAGE_WORDS, SCRIPT_LANGUAGES, type ScriptLanguage } from "./flows.j
9
9
  export declare const CODE_OUTPUT_CHARS: 12000;
10
10
  export type CodeRun = {
11
11
  script: FlowScriptRow;
12
+ /** Asked right before every spawn attempt (retries too); false refuses it. A browser zone's approval rides here. */
13
+ beforeSpawn?: () => boolean;
12
14
  /** Where it runs, and the project folder a file script is found in. */
13
15
  cwd: string;
14
16
  root: string;
@@ -58,7 +60,8 @@ export declare function cardsFromOutput(stdout: string): {
58
60
  * of its own for its input, an inline script and its temp files; writes as the project's writes setting allows (its
59
61
  * folder, the copy's Git data, its project's caches); the scratch pool denied around its own folder; and its network
60
62
  * open, as flows' scripts have always had it (they reach the services their secrets are for; no approval seals a
61
- * network for them). The fence is required: where this machine can't confine it, it runs only when an instance operator
63
+ * network for them). `browser` (flow-browser.ts browserStanding, proved by the caller at launch) adds Chromium's one
64
+ * service, as a project check has it; nothing else asks for it. The fence is required: where this machine can't confine it, it runs only when an instance operator
62
65
  * allowed unconfined agents, and otherwise nothing runs and the card says why.
63
66
  */
64
67
  export type FlowLaunch = {
@@ -75,6 +78,7 @@ export declare function flowLaunch(store: Store, options: {
75
78
  available?: () => boolean;
76
79
  policy?: FencePolicy;
77
80
  home?: string;
81
+ browser?: true;
78
82
  }): FlowLaunch | null | {
79
83
  problem: string;
80
84
  };
package/dist/flow-code.js CHANGED
@@ -79,6 +79,7 @@ export async function runCode(run) {
79
79
  const ran = await run.shell("/bin/sh", ["-c", 'exec "$@" < "$FLOW_INPUT"', "flow-script", runtime.file, ...runtime.args], {
80
80
  cwd: run.cwd, timeoutMs: script.timeoutMinutes * 60_000, envAllowlist: SETUP_ENV_ALLOWLIST, omitEnv: SETUP_ENV_DENYLIST, processGroup: true,
81
81
  env: { ...run.env, ...run.secrets, ...(run.launch?.env ?? {}), FLOW_INPUT: inputFile }, ...(run.launch?.fence ?? {}),
82
+ ...(run.beforeSpawn === undefined ? {} : { beforeSpawn: run.beforeSpawn }),
82
83
  });
83
84
  const printed = codeOutput(ran.stdout, run.secrets);
84
85
  const all = scrubSecrets(ran.stdout.slice(0, 1_000_000), run.secrets);
@@ -192,7 +193,9 @@ export function flowLaunch(store, options) {
192
193
  // The project's read denies apply to flows as to builds (read live: a flow seals no list, and these only narrow).
193
194
  const readDeny = store.projectSandbox(options.repo).readDeny.map(one => readDenyPath(one, home));
194
195
  const fence = fenceFor("flow", { databaseFile, worktree: options.folder, home, boundary, readDeny, policy: options.policy ?? 1, scratchRoots: [options.scratch], binding, runFolders: runFolders(own), poolRoots, extra: allowed?.deny ?? [],
195
- writes: allowed === null ? null : { writes: allowed.writes, readOnly: allowed.readOnly }, network: { mode: "open" } });
196
+ writes: allowed === null ? null : { writes: allowed.writes, readOnly: allowed.readOnly }, network: { mode: "open" },
197
+ // Chromium's one service, only for a script zone whose browser use a person approved, proved by the caller now.
198
+ ...(options.browser === true ? { browser: true } : {}) });
196
199
  return { folder: own, env: { ...env, ...(allowed === null ? {} : buildCacheEnv(cache)) }, fence, dispose };
197
200
  }
198
201
  catch {
@@ -23,6 +23,7 @@ import { cardFollowers, notifyPeople } from "./flow-people.js";
23
23
  import { DRAFT_CHARS, keptDraft } from "./flow-draft.js";
24
24
  import { chooseStep, flowPersonOf, sendStep } from "./flow-send.js";
25
25
  import { parseSoul, subagentLabel } from "./subagents.js";
26
+ import { cardTaskArchived, taskArchived } from "./task-archive.js";
26
27
  import { withStageHandoff } from "./contracts/stage-output.js";
27
28
  /** The flow's drawing, or null when it can't be read (the card then says so and waits). */
28
29
  export function flowDefinitionOf(flow) {
@@ -90,6 +91,14 @@ function advanceCard(store, flow, definition, card, now, options, outcome) {
90
91
  return;
91
92
  }
92
93
  const stage = definition.stages.find(one => one.id === card.stage);
94
+ // A put-away task (v145, task-tidy.md) holds its card where it is, whatever the zone: nothing moves on, files, sends
95
+ // or notifies until the task is restored.
96
+ if (cardTaskArchived(store, card.id)) {
97
+ const waiting = "Its task is put away. Restore the task to continue.";
98
+ if (card.waiting !== waiting)
99
+ store.updateFlowCard(card.id, { waiting }, now);
100
+ return;
101
+ }
93
102
  if (stage === undefined) {
94
103
  // The zone was removed while the card sat in it: back to the start.
95
104
  if (store.moveFlowCard(card.id, { to: definition.start, outcome: "moved", actor: "flow", note: null, expectEntry: card.entry }, now))
@@ -311,6 +320,13 @@ function workStage(store, flow, definition, stage, card, now, options, outcome,
311
320
  store.updateFlowCard(card.id, { waiting: "Its task is gone. Move the card to try again." }, now);
312
321
  return;
313
322
  }
323
+ // A put-away task (v145, task-tidy.md) holds its card where it is: nothing moves on, files or sends until it's restored.
324
+ if (taskArchived(store, current) || taskArchived(store, card.task)) {
325
+ const waiting = "Its task is put away. Restore the task to continue.";
326
+ if (card.waiting !== waiting)
327
+ store.updateFlowCard(card.id, { waiting }, now);
328
+ return;
329
+ }
314
330
  if (task.state === "done") {
315
331
  // A build whose project checks failed is done but not good: it takes the failure path, like a failed build.
316
332
  if (stage.kind === "task") {
@@ -1,13 +1,23 @@
1
1
  import { type GalleryAnswers, type GalleryPreview, type GalleryTemplate, type GalleryTool } from "./flow-gallery.js";
2
2
  import { type FlowDefinition, type FlowStage } from "./flows.js";
3
- import { type Html } from "./html.js";
3
+ import { type Html, type HtmlValue } from "./html.js";
4
4
  export declare const GALLERY_CSS: string;
5
5
  /** A flow's main path, as a person reads it: from the start, each step's first way on (a sort's first answer, a choose's
6
6
  * first option that goes somewhere, else next; a check's failure when passing just ends), never back to a step shown. */
7
7
  export declare function mainPath(definition: FlowDefinition): FlowStage[];
8
- /** A flow's steps in order, joined by arrows; a person's decisions carry the accent. */
8
+ /** A flow's steps in order, joined by arrows. */
9
9
  export declare function stepStrip(definition: FlowDefinition): Html;
10
- /** The gallery: every template, grouped, each with one "Use this". `repo` carries a chosen project to the template's page. */
10
+ /** One row of New flow: name · promise · one line · one outline action (the same shape for templates, kits, recipes and starters). */
11
+ export declare function templateRow(options: {
12
+ name: string;
13
+ promise: string;
14
+ line?: HtmlValue;
15
+ action?: HtmlValue;
16
+ attrs?: Html;
17
+ }): Html;
18
+ /** The groups New flow filters by: the template groups, then kits, recipes and starter flows. */
19
+ export declare const NEW_FLOW_GROUPS: ReadonlyArray<readonly [key: string, label: string]>;
20
+ /** The templates as rows, grouped (or one group): each with one "Use this". `repo` carries a chosen project to the template's page. */
11
21
  export declare function galleryHtml(input: {
12
22
  repo: string | null;
13
23
  canUse: boolean;
@@ -15,8 +25,35 @@ export declare function galleryHtml(input: {
15
25
  id: string;
16
26
  state: "connected" | "open" | "taken";
17
27
  }[] | null;
28
+ group?: string;
29
+ }): Html;
30
+ /**
31
+ * New flow: describe it (the shared composer; the words go to the lead as a chat draft, the project on its bar), then
32
+ * start from a template, filtered by group. Kits, recipes and starter flows arrive as their own rows.
33
+ */
34
+ export declare function newFlowHtml(input: {
35
+ repo: string | null;
36
+ projects: readonly {
37
+ path: string;
38
+ name: string;
39
+ }[];
40
+ canUse: boolean;
41
+ group: string;
42
+ connections: readonly {
43
+ id: string;
44
+ state: "connected" | "open" | "taken";
45
+ }[] | null;
46
+ ask: {
47
+ value: string;
48
+ error: string | null;
49
+ };
50
+ kits: Html;
51
+ recipes: Html;
52
+ starters: Html;
53
+ said: string | null;
54
+ problem: string | null;
18
55
  }): Html;
19
- /** A template's page: its questions, then what it will do and never do, then Create. */
56
+ /** A template's page, a reading page: its steps and tools, its questions, what it will do and never do, then Create. */
20
57
  export declare function galleryUseHtml(input: {
21
58
  template: GalleryTemplate;
22
59
  projects: readonly {