toolroll 0.9.67 → 0.9.69

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 (250) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/dist/acp-client.js +1 -3
  3. package/dist/agent-board.d.ts +0 -1
  4. package/dist/agent-board.js +0 -1
  5. package/dist/agent-fence.d.ts +58 -0
  6. package/dist/agent-fence.js +214 -19
  7. package/dist/agentconfig.d.ts +1 -1
  8. package/dist/agentconfig.js +6 -2
  9. package/dist/batch-checks.js +14 -26
  10. package/dist/browser/workspace.css +1 -1
  11. package/dist/browser/workspace.js +31 -29
  12. package/dist/builder.d.ts +9 -0
  13. package/dist/builder.js +30 -27
  14. package/dist/canonical-json.d.ts +14 -0
  15. package/dist/canonical-json.js +36 -0
  16. package/dist/chat-actions.js +1 -2
  17. package/dist/chat-admission.d.ts +19 -0
  18. package/dist/chat-admission.js +22 -0
  19. package/dist/chat-channel.js +1 -1
  20. package/dist/chat-delivery.js +0 -1
  21. package/dist/chat-follow.d.ts +17 -0
  22. package/dist/chat-follow.js +32 -0
  23. package/dist/chat-messages.js +0 -1
  24. package/dist/chat-settings-page.d.ts +23 -0
  25. package/dist/chat-settings-page.js +41 -0
  26. package/dist/check-access.d.ts +54 -0
  27. package/dist/check-access.js +80 -0
  28. package/dist/claim.js +3 -6
  29. package/dist/claude-print.d.ts +12 -0
  30. package/dist/claude-print.js +34 -0
  31. package/dist/cli-http.d.ts +0 -7
  32. package/dist/cli-http.js +0 -21
  33. package/dist/cli.d.ts +3 -10
  34. package/dist/cli.js +15 -24
  35. package/dist/coding-provider.js +2 -1
  36. package/dist/coding-workspace.js +1 -1
  37. package/dist/{team-cli.d.ts → connect-cli.d.ts} +5 -9
  38. package/dist/connect-cli.js +331 -0
  39. package/dist/containment.d.ts +0 -2
  40. package/dist/containment.js +2 -6
  41. package/dist/contracts/chat-actions.d.ts +0 -1
  42. package/dist/contracts/chat-content.d.ts +0 -4
  43. package/dist/contracts/chat-content.js +0 -2
  44. package/dist/contracts/chat-tables.d.ts +0 -1
  45. package/dist/contracts/cli.d.ts +0 -62
  46. package/dist/contracts/cli.js +0 -4
  47. package/dist/contracts/coding-workspace.js +1 -1
  48. package/dist/contracts/console-api.d.ts +1 -0
  49. package/dist/contracts/console-api.js +1 -0
  50. package/dist/contracts/contract.js +1 -1
  51. package/dist/contracts/flow-card.d.ts +0 -1
  52. package/dist/contracts/flow-propose.d.ts +0 -1
  53. package/dist/contracts/flow-send.js +1 -1
  54. package/dist/contracts/gateway-tools.d.ts +0 -1
  55. package/dist/contracts/integration-metadata.js +1 -1
  56. package/dist/contracts/lead-tools.d.ts +0 -1
  57. package/dist/contracts/memory-pass.d.ts +0 -1
  58. package/dist/contracts/memory-pass.js +1 -1
  59. package/dist/contracts/plan.js +1 -1
  60. package/dist/contracts/project-knowledge.js +1 -1
  61. package/dist/contracts/project-skills.d.ts +0 -1
  62. package/dist/contracts/proof.js +1 -1
  63. package/dist/contracts/recipes.js +1 -1
  64. package/dist/contracts/scope.js +1 -1
  65. package/dist/contracts/scout-report.js +1 -1
  66. package/dist/contracts/teams-callback.d.ts +0 -1
  67. package/dist/converse.d.ts +0 -6
  68. package/dist/converse.js +0 -19
  69. package/dist/daemon.js +3 -2
  70. package/dist/decision.d.ts +1 -3
  71. package/dist/deprecations.d.ts +12 -4
  72. package/dist/deprecations.js +27 -4
  73. package/dist/desktop-update-recovery.js +1 -1
  74. package/dist/desktop-update.js +3 -2
  75. package/dist/discord-chat.js +4 -15
  76. package/dist/discord-settings.d.ts +2 -6
  77. package/dist/discord-settings.js +10 -38
  78. package/dist/discord.js +12 -23
  79. package/dist/evidence.d.ts +0 -1
  80. package/dist/evidence.js +0 -3
  81. package/dist/exec.d.ts +0 -9
  82. package/dist/exec.js +3 -81
  83. package/dist/flow-actions.js +0 -3
  84. package/dist/flow-draft.d.ts +1 -3
  85. package/dist/flow-draft.js +15 -28
  86. package/dist/flow-scripts.d.ts +1 -8
  87. package/dist/flow-scripts.js +0 -13
  88. package/dist/flow-send.js +1 -1
  89. package/dist/flow-share.d.ts +0 -2
  90. package/dist/flow-share.js +1 -6
  91. package/dist/flow-steps.js +1 -1
  92. package/dist/flow-triggers.d.ts +0 -1
  93. package/dist/flow-triggers.js +1 -2
  94. package/dist/flow-unattended.js +1 -12
  95. package/dist/flows.js +1 -1
  96. package/dist/health-cli.d.ts +2 -2
  97. package/dist/health-cli.js +1 -1
  98. package/dist/invoke.d.ts +39 -5
  99. package/dist/invoke.js +181 -24
  100. package/dist/is-record.d.ts +2 -0
  101. package/dist/is-record.js +2 -0
  102. package/dist/lead-doors.d.ts +1 -5
  103. package/dist/lead-doors.js +0 -4
  104. package/dist/lead-tools.js +3 -6
  105. package/dist/live-detail.d.ts +0 -1
  106. package/dist/live-detail.js +0 -6
  107. package/dist/live-terminal.js +2 -1
  108. package/dist/live.d.ts +0 -1
  109. package/dist/live.js +1 -2
  110. package/dist/mcp-core.js +0 -2
  111. package/dist/memory-pass.js +1 -1
  112. package/dist/model-catalog.js +1 -1
  113. package/dist/modes.js +1 -12
  114. package/dist/operate.d.ts +4 -0
  115. package/dist/operate.js +456 -750
  116. package/dist/password-bearer.d.ts +0 -2
  117. package/dist/password-bearer.js +0 -4
  118. package/dist/peek-cli.js +2 -1
  119. package/dist/peek.d.ts +2 -26
  120. package/dist/peek.js +2 -168
  121. package/dist/phase-routing.d.ts +0 -4
  122. package/dist/phase-routing.js +2 -21
  123. package/dist/planner-source.d.ts +2 -1
  124. package/dist/planner-source.js +2 -12
  125. package/dist/planner.js +2 -14
  126. package/dist/process-tree.d.ts +0 -2
  127. package/dist/process-tree.js +0 -10
  128. package/dist/project-delete-ui.d.ts +16 -0
  129. package/dist/project-delete-ui.js +60 -1
  130. package/dist/project-delete.d.ts +0 -2
  131. package/dist/project-delete.js +1 -3
  132. package/dist/project-learning.d.ts +0 -14
  133. package/dist/project-skills.d.ts +0 -1
  134. package/dist/project-skills.js +0 -14
  135. package/dist/project-tools.js +2 -10
  136. package/dist/provider.d.ts +0 -17
  137. package/dist/provider.js +0 -26
  138. package/dist/pulls.d.ts +0 -2
  139. package/dist/pulls.js +0 -9
  140. package/dist/release-adapters.d.ts +3 -0
  141. package/dist/release-adapters.js +15 -5
  142. package/dist/remote-exec.d.ts +3 -2
  143. package/dist/remote-exec.js +3 -2
  144. package/dist/repos.d.ts +0 -5
  145. package/dist/repos.js +2 -10
  146. package/dist/repository-context.d.ts +0 -2
  147. package/dist/repository-context.js +0 -4
  148. package/dist/restart-certification.d.ts +0 -2
  149. package/dist/restart-certification.js +1 -5
  150. package/dist/result-follow-ups.d.ts +1 -4
  151. package/dist/result-follow-ups.js +11 -21
  152. package/dist/result-review.d.ts +0 -2
  153. package/dist/result-review.js +0 -2
  154. package/dist/retention.d.ts +0 -1
  155. package/dist/retention.js +0 -1
  156. package/dist/review-context.d.ts +0 -1
  157. package/dist/review-context.js +1 -12
  158. package/dist/reviewer.d.ts +0 -16
  159. package/dist/reviewer.js +0 -18
  160. package/dist/run-closeout.js +2 -1
  161. package/dist/scope.d.ts +9 -0
  162. package/dist/scope.js +13 -18
  163. package/dist/scout-browser.js +2 -1
  164. package/dist/serve.js +11 -2
  165. package/dist/server/console.css +5 -133
  166. package/dist/server/flows.js +2 -2
  167. package/dist/server/http.d.ts +4 -0
  168. package/dist/server/http.js +5 -0
  169. package/dist/server/pages.js +2 -2
  170. package/dist/server/people-tokens.js +3 -4
  171. package/dist/server/remote.js +1 -6
  172. package/dist/server/render-chat.d.ts +9 -2
  173. package/dist/server/render-chat.js +53 -7
  174. package/dist/server/render-results.d.ts +3 -6
  175. package/dist/server/render-results.js +4 -9
  176. package/dist/server/render-tasks.js +6 -6
  177. package/dist/server/route-table.d.ts +0 -4
  178. package/dist/server/route-table.js +1 -6
  179. package/dist/server/settings.js +60 -23
  180. package/dist/server/task-views.js +1 -1
  181. package/dist/server/tasks.js +5 -3
  182. package/dist/setup-guide.d.ts +2 -2
  183. package/dist/skills.d.ts +0 -6
  184. package/dist/skills.js +0 -8
  185. package/dist/slack-chat.d.ts +1 -7
  186. package/dist/slack-chat.js +5 -18
  187. package/dist/slack-settings.d.ts +2 -6
  188. package/dist/slack-settings.js +10 -38
  189. package/dist/slack.js +12 -23
  190. package/dist/sleep.d.ts +2 -0
  191. package/dist/sleep.js +2 -0
  192. package/dist/store.d.ts +29 -75
  193. package/dist/store.js +89 -180
  194. package/dist/structured-output.d.ts +0 -1
  195. package/dist/structured-output.js +0 -1
  196. package/dist/subagent-work.d.ts +0 -2
  197. package/dist/subagent-work.js +0 -2
  198. package/dist/subagents.d.ts +1 -3
  199. package/dist/subagents.js +20 -33
  200. package/dist/subscription-chat.d.ts +1 -3
  201. package/dist/subscription-chat.js +2 -3
  202. package/dist/surface.js +2 -1
  203. package/dist/task-sizing.d.ts +1 -4
  204. package/dist/task-sizing.js +3 -10
  205. package/dist/teams-chat.js +4 -11
  206. package/dist/teams.js +12 -19
  207. package/dist/telegram-lead.js +2 -2
  208. package/dist/telegram.js +1 -1
  209. package/dist/token-notices.d.ts +0 -11
  210. package/dist/token-notices.js +1 -5
  211. package/dist/tool-secret-store.d.ts +0 -3
  212. package/dist/tool-secret-store.js +2 -11
  213. package/dist/toolroll-update.d.ts +0 -2
  214. package/dist/toolroll-update.js +2 -2
  215. package/dist/verification-evidence.d.ts +0 -1
  216. package/dist/verification-evidence.js +0 -1
  217. package/dist/webhooks.d.ts +0 -1
  218. package/dist/webhooks.js +0 -1
  219. package/dist/workspace-ui.d.ts +0 -8
  220. package/dist/workspace-ui.js +1 -6
  221. package/package.json +1 -1
  222. package/dist/contracts/native-census.d.ts +0 -42
  223. package/dist/contracts/native-census.js +0 -36
  224. package/dist/process-recovery-anchor.d.ts +0 -75
  225. package/dist/process-recovery-anchor.js +0 -188
  226. package/dist/process-recovery-coalition.d.ts +0 -25
  227. package/dist/process-recovery-coalition.js +0 -105
  228. package/dist/process-recovery-eligibility.d.ts +0 -179
  229. package/dist/process-recovery-eligibility.js +0 -174
  230. package/dist/process-recovery-native.d.ts +0 -161
  231. package/dist/process-recovery-native.js +0 -595
  232. package/dist/process-recovery-provenance.d.ts +0 -163
  233. package/dist/process-recovery-provenance.js +0 -292
  234. package/dist/process-recovery-services.d.ts +0 -56
  235. package/dist/process-recovery-services.js +0 -191
  236. package/dist/process-recovery-settlement.d.ts +0 -32
  237. package/dist/process-recovery-settlement.js +0 -133
  238. package/dist/process-recovery.d.ts +0 -41
  239. package/dist/process-recovery.js +0 -91
  240. package/dist/session-brief.d.ts +0 -8
  241. package/dist/session-brief.js +0 -24
  242. package/dist/session-cli.d.ts +0 -42
  243. package/dist/session-cli.js +0 -271
  244. package/dist/session-contract.d.ts +0 -183
  245. package/dist/session-contract.js +0 -122
  246. package/dist/session-remote.d.ts +0 -19
  247. package/dist/session-remote.js +0 -86
  248. package/dist/session-service.d.ts +0 -22
  249. package/dist/session-service.js +0 -157
  250. package/dist/team-cli.js +0 -692
@@ -7,9 +7,7 @@ import { CodingWorkspace } from '../coding-workspace.js';
7
7
  import { createMcpHttp } from "../mcp-http.js";
8
8
  import { createOAuthHttp, resourceMetadataUrl } from "../mcp-oauth.js";
9
9
  import { reproveRemote } from "../operate-remote.js";
10
- import { rowVisible } from "../project.js";
11
10
  import { RequestBudget, SourceAdmission } from "../request-budget.js";
12
- import { createSessionRunner } from '../session-remote.js';
13
11
  import {} from "../store.js";
14
12
  import { handleTeamHttp, TEAM_PASSWORD_REVERIFY_MS } from '../team-http.js';
15
13
  import { handleTeamsHttp } from "../teams.js";
@@ -17,7 +15,7 @@ import { flowHook, telegramHook } from "./remote-hooks.js";
17
15
  import { respond } from "./http.js";
18
16
  import {} from "./session.js";
19
17
  export function createRemoteHandlers(runtime) {
20
- const { identify, store, admitPasswordSource, admitBearer, allowedHost, team, teamBrowserReply, requestBudget, options, coding, codingProjects, liveCeiling, clock, evidenceRoot, consoleOrigin, managedRepos, joinSourceOf, ssoSettings, oauthTokenBudget, hookContext, teamsSourceBudget, teamsTenantBudget } = runtime;
18
+ const { identify, store, admitPasswordSource, admitBearer, allowedHost, team, teamBrowserReply, requestBudget, options, clock, evidenceRoot, consoleOrigin, managedRepos, joinSourceOf, ssoSettings, oauthTokenBudget, hookContext, teamsSourceBudget, teamsTenantBudget } = runtime;
21
19
  /**
22
20
  * The full principal a live `so_` API token stands for: its person and their generation, the token's own read/act
23
21
  * scope, its project limit narrowed by the person's current access, and its id. Every route that accepts an API token
@@ -97,15 +95,12 @@ export function createRemoteHandlers(runtime) {
97
95
  return who?.via === 'cookie' ? teamBrowserReply(reply, actor, who.session.csrf) : reply;
98
96
  },
99
97
  });
100
- // D7: native session operations are remote commands too (`session …` over /api/cli), run by the session owner.
101
- const sessionRunner = createSessionRunner({ store, workspace: coding, projects: codingProjects, projectAllowed: repo => rowVisible(liveCeiling(), repo), clock });
102
98
  // Remote CLI: one live API token names the person; the shared command boundary decides everything else.
103
99
  const cliEndpoint = (request, response) => handleCliHttp(request, response, {
104
100
  admit: principal => requestBudget.admit(principal.tokenId, "api"),
105
101
  authenticate: tokenPrincipalOf,
106
102
  run: async () => options.cliRunner ?? (await import("../operate.js")).runOperateAs ?? null,
107
103
  modeOf: options.cliModeOf,
108
- session: sessionRunner,
109
104
  store,
110
105
  });
111
106
  const mcpHttp = createMcpHttp({ store, clock, evidenceRoot, ...(options.requestBudgetClock === undefined ? {} : { requestBudgetClock: options.requestBudgetClock }), signedIn: request => identify(request, true, true) !== null, admit: person => requestBudget.admit(person.principal.tokenId, "mcp"),
@@ -142,9 +142,16 @@ export declare function leadPromptStarters(focus?: TaskChatFocus | null): Html;
142
142
  export declare function chatHeading(copy: string, projectCount: number, showProjectToggle?: boolean): Html;
143
143
  /** A deliberately small rich-text grammar for model copy. The reply is shaped first (the lead's voice, enforced:
144
144
  * no headers, at most three bold anchors, labelled links, no internal ids unless `asked` wanted them); input is
145
- * escaped before tags are introduced: bullets, numbered steps, bold, inline code and labelled http(s) links are
146
- * presentation only—never executable HTML. */
145
+ * escaped before tags are introduced: bullets, numbered steps, bold, inline code, fenced code blocks and labelled
146
+ * http(s) links are presentation only—never executable HTML. */
147
147
  export declare function renderChatText(raw: string, asked?: string): Html;
148
+ /** A line comment from the chat Workbench (src/browser/line-steer.tsx steerMessage): On "<file, as a JSON string>":<line>
149
+ * [ (removed line)] (run #<n>): <comment>, then a blank line and "> <the line>" (just ">" for a blank line, once the
150
+ * chat POST has trimmed the message), always last.
151
+ * The words the lead reads stay exactly as sent; the thread shows them in three parts: a small reference line, the quoted
152
+ * code as written, and the comment. Null for any other message, which keeps its own plain rendering. Everything is
153
+ * escaped by the html template. */
154
+ export declare function steerCommentHtml(text: string): Html | null;
148
155
  export declare function chatActivity(activity: string | null): Html;
149
156
  export declare const CHAT_UI_SCRIPT: string;
150
157
  /** Provider, model, and limits under ONE disclosure (UI polish
@@ -33,7 +33,6 @@ import { chatMoney, oneLineOf, relativeAge, safePrUrl, screen, sentenceCase, str
33
33
  import { chatResultHref, projectChatHref, taskChatHref, taskHref } from "./http.js";
34
34
  import {} from "./render-pages.js";
35
35
  import { completionReceiptCard } from "./render-results.js";
36
- import {} from "./render-settings.js";
37
36
  import { agentsAvailabilityHtml, agentsBadgesHtml, agentsStripHtml, agentsSummaryWords, agentsWhyHtml, approvalSheetHtml, checkProgressHtml, consentClosedHtml, consentDoorOf, decisionAnswerCard, dispatchHeadline, milestoneProgressHtml, planRevisionLedgerHtml, profileWords, sizeConsequence, taskControlDetailsHtml, taskStatusCard, taskViewSwitch } from "./render-tasks.js";
38
37
  /** The installation fact that the lead was turned on by default, once (its value: the provider). */
39
38
  export const LEAD_BY_DEFAULT_FACT = "lead-on-by-default";
@@ -260,8 +259,8 @@ export function chatHeading(copy, projectCount, showProjectToggle = true) {
260
259
  }
261
260
  /** A deliberately small rich-text grammar for model copy. The reply is shaped first (the lead's voice, enforced:
262
261
  * no headers, at most three bold anchors, labelled links, no internal ids unless `asked` wanted them); input is
263
- * escaped before tags are introduced: bullets, numbered steps, bold, inline code and labelled http(s) links are
264
- * presentation only—never executable HTML. */
262
+ * escaped before tags are introduced: bullets, numbered steps, bold, inline code, fenced code blocks and labelled
263
+ * http(s) links are presentation only—never executable HTML. */
265
264
  export function renderChatText(raw, asked) {
266
265
  const inline = replyHtmlInline;
267
266
  const text = shapeReply(raw, { appOrigin: requestContext.getStore()?.appOrigins ?? null, ...(asked === undefined ? {} : { asked }) });
@@ -281,7 +280,31 @@ export function renderChatText(raw, asked) {
281
280
  out.push(list.tag === "ul" ? html `<ul>${list.items}</ul>` : html `<ol>${list.items}</ol>`);
282
281
  list = null;
283
282
  };
283
+ // A fenced block (shaping leaves it exactly as written) is shown as written: escaped, never parsed.
284
+ let fence = null;
285
+ const closeFence = () => {
286
+ const open = fence;
287
+ if (open === null)
288
+ return;
289
+ out.push(html `<pre class="chat-code"${open.lang === "" ? "" : html ` data-lang="${open.lang}"`}><code>${open.lines.join("\n")}</code></pre>`);
290
+ fence = null;
291
+ };
284
292
  for (const line of lines) {
293
+ const open = fence;
294
+ if (open !== null) {
295
+ if (/^\s{0,3}```\s*$/.test(line))
296
+ closeFence();
297
+ else
298
+ open.lines.push(line);
299
+ continue;
300
+ }
301
+ const fenceMark = /^\s{0,3}```\s*([A-Za-z0-9_+.#-]{0,24})\s*$/.exec(line);
302
+ if (fenceMark !== null) {
303
+ flushParagraph();
304
+ closeList();
305
+ fence = { lang: fenceMark[1], lines: [] };
306
+ continue;
307
+ }
285
308
  const bullet = /^\s*[-*]\s+(.+)$/.exec(line);
286
309
  const numbered = /^\s*(\d{1,9})[.)]\s+(.+)$/.exec(line);
287
310
  if (bullet !== null || numbered !== null) {
@@ -304,10 +327,35 @@ export function renderChatText(raw, asked) {
304
327
  paragraph.push(line);
305
328
  }
306
329
  }
330
+ // An unclosed fence still shows its lines as code: the reply may have been cut.
307
331
  flushParagraph();
308
332
  closeList();
333
+ closeFence();
309
334
  return html `<div class="chat-copy">${out}</div>`;
310
335
  }
336
+ /** A line comment from the chat Workbench (src/browser/line-steer.tsx steerMessage): On "<file, as a JSON string>":<line>
337
+ * [ (removed line)] (run #<n>): <comment>, then a blank line and "> <the line>" (just ">" for a blank line, once the
338
+ * chat POST has trimmed the message), always last.
339
+ * The words the lead reads stay exactly as sent; the thread shows them in three parts: a small reference line, the quoted
340
+ * code as written, and the comment. Null for any other message, which keeps its own plain rendering. Everything is
341
+ * escaped by the html template. */
342
+ export function steerCommentHtml(text) {
343
+ const steer = /^On ("(?:[^"\\\n]|\\.){1,1200}"):(\d{1,9})( \(removed line\))? \(run #(\d{1,12})\): ([\s\S]+)\n\n>(?: ([^\n]*))?$/.exec(text);
344
+ if (steer === null)
345
+ return null;
346
+ // A blank line's quote ("> ") reaches the store as ">": the chat POST trims the message (src/server/chat.ts).
347
+ const [, quoted, line, removed, run, comment, code = ""] = steer;
348
+ let path;
349
+ try {
350
+ path = JSON.parse(quoted);
351
+ }
352
+ catch {
353
+ return null;
354
+ }
355
+ if (typeof path !== "string" || path === "" || /[\u0000-\u001f]/.test(path) || comment.trim() === "")
356
+ return null;
357
+ return html `<div class="chat-steer" data-steer-comment><p class="chat-steer-ref">${path}:${line}${removed === undefined ? "" : " · removed line"} · run #${run}</p>${code === "" ? "" : html `<pre class="chat-steer-code"><code>${code}</code></pre>`}<p class="chat-steer-note" style="white-space:pre-wrap">${comment}</p></div>`;
358
+ }
311
359
  export function chatActivity(activity) {
312
360
  return chatActivityDetailsHtml(activity);
313
361
  }
@@ -351,8 +399,6 @@ export function chatLimitsHtml(facts) {
351
399
  return html `<details class="chat-limits"><summary>Model &amp; limits<span class="meta">${facts.subscription ? "membership" : facts.provider}</span></summary><div class="chat-budget">${rows}</div></details>`;
352
400
  }
353
401
  export function chatPage(chrome, data) {
354
- const formFacts = { keyFacts: data.keyFacts, openrouterModels: data.openrouterModels, ...(data.liveModels === undefined ? {} : { liveModels: data.liveModels }), csrf: data.csrf,
355
- returnTo: data.focusTask === null ? "/chat" : taskChatHref(data.focusTask.id) };
356
402
  const parts = [
357
403
  data.focusTask === null
358
404
  ? chatHeading("", data.projects.length, data.enabled.ok)
@@ -803,7 +849,7 @@ export function leadBrowserMessages(rows, back, target = { task: null, project:
803
849
  return rows.messages.map(message => ({
804
850
  id: message.id, role: message.role, text: message.text,
805
851
  html: htmlString(message.role === 'operator'
806
- ? (() => { const queued = rows.queue?.get(message.id); return html `${message.text === "" ? "" : html `<p>${message.text}</p>`}${queued === undefined ? "" : queuedStateHtml(queued, back)}`; })()
852
+ ? (() => { const queued = rows.queue?.get(message.id); return html `${message.text === "" ? "" : steerCommentHtml(message.text) ?? html `<p>${message.text}</p>`}${queued === undefined ? "" : queuedStateHtml(queued, back)}`; })()
807
853
  : html `${renderChatText(message.text, askedBy.get(message.id))}${asked(message.turn)}`),
808
854
  activity: message.activity, createdAt: message.createdAt,
809
855
  ...(() => {
@@ -882,7 +928,7 @@ export function leadThreadHtml(data) {
882
928
  asked = message.text;
883
929
  // A message that waits its turn (the lead queue) says so, with Remove while it still waits — as in the React thread.
884
930
  const queued = data.queue?.get(message.id);
885
- thread.push(html `<div class="msg op" data-message-role="operator" data-key="m${message.id}">${message.text === "" ? "" : html `<p style="white-space:pre-wrap">${message.text}</p>`}${queued === undefined ? "" : queuedStateHtml(queued, returnTo)}</div>`);
931
+ thread.push(html `<div class="msg op" data-message-role="operator" data-key="m${message.id}">${message.text === "" ? "" : steerCommentHtml(message.text) ?? html `<p style="white-space:pre-wrap">${message.text}</p>`}${queued === undefined ? "" : queuedStateHtml(queued, returnTo)}</div>`);
886
932
  continue;
887
933
  }
888
934
  const cards = message.turn === null ? [] : (byTurn.get(message.turn) ?? []);
@@ -1,4 +1,5 @@
1
1
  /** Results and runs: the review cockpit, evidence, receipts, diffs and the result panel. */
2
+ import type { CheckAccess } from "../check-access.js";
2
3
  import type { RunNetworkRecord } from "../egress.js";
3
4
  import { type AssignmentSnapshot } from "../assignment.js";
4
5
  import { type BrowserCheckItem, type BrowserNeedAction, type BrowserResultPanel, type BrowserResultView } from "../browser-workspace.js";
@@ -95,9 +96,6 @@ export declare function withRefusal(href: string, code: keyof typeof RESULT_REFU
95
96
  /** The queue reads at most this many completed tasks — the store's own
96
97
  * page ceiling, admission bound before it. */
97
98
  export declare const REVIEW_QUEUE_CAP = 100;
98
- /** The one return road the comment endpoint honors besides its own run
99
- * page: the cockpit's selected-result link, exactly. */
100
- export declare const REVIEW_RETURN: RegExp;
101
99
  /** Review priority: a deterministic, LABELED presentation aid over facts
102
100
  * the done list already carries. Band 0 goes first; the words name why.
103
101
  * It is never a verdict — the stored proof verdict stays the authority,
@@ -187,6 +185,8 @@ export type ReviewCockpitView = {
187
185
  efficiency?: RunEfficiency | null;
188
186
  /** What the run's network was and what its proxy saw (egress.ts): display only. */
189
187
  network?: RunNetworkRecord | null;
188
+ /** How far its checks reached (check-access.ts): shown only when wider than sandboxed. */
189
+ checkAccess?: CheckAccess;
190
190
  taskId: string;
191
191
  title: string;
192
192
  repo: string | null;
@@ -287,9 +287,6 @@ export declare function reviewCockpitPage(chrome: Chrome, data: {
287
287
  user: string;
288
288
  now: Date;
289
289
  }): Screen;
290
- /** The selected result: intent → proof → changes → publication → acts,
291
- * one scan path, every fact labeled by its source. */
292
- export declare function reviewCockpitDetail(view: ReviewCockpitView, csrf: string, noted: boolean, canRetryReview?: boolean, tab?: ResultTab, user?: string): Html;
293
290
  /** The selected result's HTML and, for the rebuilt page, the same result
294
291
  * as data: every form and panel body is the one the HTML carries. */
295
292
  /** Where a result is shown: its own page (/review), or the Result tab of its task's page (docs/plans/one-page-task-and-agent.md),
@@ -238,9 +238,6 @@ export function withRefusal(href, code) {
238
238
  /** The queue reads at most this many completed tasks — the store's own
239
239
  * page ceiling, admission bound before it. */
240
240
  export const REVIEW_QUEUE_CAP = 100;
241
- /** The one return road the comment endpoint honors besides its own run
242
- * page: the cockpit's selected-result link, exactly. */
243
- export const REVIEW_RETURN = /^\/review\?result=[A-Za-z0-9._~%-]{1,200}$/;
244
241
  export const PRIORITY_LABELS = { 0: "needs action", 1: "review", 2: "no flags" };
245
242
  export function reviewPriorityOf(row) {
246
243
  if (row.assignment != null && (row.assignment.receipt?.runId ?? null) === row.runId) {
@@ -568,11 +565,6 @@ export function reviewCockpitPage(chrome, data) {
568
565
  functional: { script: reviewEvidenceScript() + (selected !== null && selected.detail !== null ? RESULT_REVIEW_SCRIPT : ""), fetches: false },
569
566
  });
570
567
  }
571
- /** The selected result: intent → proof → changes → publication → acts,
572
- * one scan path, every fact labeled by its source. */
573
- export function reviewCockpitDetail(view, csrf, noted, canRetryReview = false, tab = "summary", user = "") {
574
- return reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user).html;
575
- }
576
568
  export function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user, host = { kind: "review" }) {
577
569
  const parts = [];
578
570
  const run = view.run;
@@ -637,6 +629,7 @@ export function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab,
637
629
  record: run === null ? null : { build: run.id, href: `/r/${run.id}?record=1`, facts: [
638
630
  { label: "Agent", value: [run.provider, run.model].filter(Boolean).join(" · ") },
639
631
  { label: "Worker", value: run.runner },
632
+ ...(view.checkAccess === undefined || view.checkAccess === "sandboxed" ? [] : [{ label: "Checks", value: view.checkAccess === "unsandboxed" ? "Checks ran without a sandbox" : "Checks ran with model and GitHub sign-ins" }]),
640
633
  ...(run.branch === null ? [] : [{ label: "Branch", value: run.branch }]),
641
634
  ...(run.finishedAt === null ? [] : [{ label: "Finished", value: when(run.finishedAt) }]),
642
635
  ...(run.ranMinutes === null ? [] : [{ label: "Ran", value: `${run.ranMinutes} min` }]),
@@ -1268,7 +1261,9 @@ export function reviewDiffHtml(patch, stat, runId, commentable, anchors = new Ma
1268
1261
  ? html `<span class="diff-annotate-space"></span>`
1269
1262
  : html `<button type="button" class="diff-annotate pick-line" data-path="${one.path}" data-line="${lineNumber}" data-side="${side}" aria-label="Annotate ${one.path}, ${side} line ${lineNumber}" title="Annotate this line">${annotateIcon}</button>`;
1270
1263
  const marker = line.kind === "addition" ? "+" : line.kind === "deletion" ? "−" : line.kind === "context" ? " " : "·";
1271
- return html `<div class="diff-line diff-${line.kind}">${annotate}<span class="diff-gutter">${line.oldLine ?? ""}</span><span class="diff-gutter">${line.newLine ?? ""}</span><code><b aria-hidden="true">${marker}</b>${line.text}</code></div>`;
1264
+ // Each real line names its file and line, so a comment (or a test) can find the exact row.
1265
+ const named = lineNumber === null || line.kind === "meta" ? "" : html ` data-diff-file="${one.path}" data-diff-line="${lineNumber}"`;
1266
+ return html `<div class="diff-line diff-${line.kind}"${named}>${annotate}<span class="diff-gutter">${line.oldLine ?? ""}</span><span class="diff-gutter">${line.newLine ?? ""}</span><code><b aria-hidden="true">${marker}</b>${line.text}</code></div>`;
1272
1267
  })}</div></section>`;
1273
1268
  });
1274
1269
  const anchor = anchors.get(one.path);
@@ -1,6 +1,7 @@
1
1
  /** Task pages: approval, agents, plans, controls, decisions and the task body. */
2
2
  import { networkWords } from "../egress.js";
3
3
  import { writesWords } from "../writes-mode.js";
4
+ import { checkAccessWords } from "../check-access.js";
4
5
  import { ACP_AGENTS, isAcpProfile } from "../acp-agents.js";
5
6
  import { html, htmlString, joinHtml, postForm, replaceMarkup, reauth } from "../html.js";
6
7
  import { closedQuestionReason, closedQuestionWords } from "../decision.js";
@@ -22,17 +23,16 @@ import { starterForWork } from "../flow-starters.js";
22
23
  import {} from "../gaps.js";
23
24
  import { readAuthModeStrict } from "../keys.js";
24
25
  import { failedAttemptSentence, retryNoteOf } from "../needs-you.js";
25
- import { agentsSummary, chosenWords, makesNoPlan, postureWords, projectRoute, PHASES as ROUTE_PHASES, TASK_SIZES } from "../phase-routing.js";
26
+ import { chosenWords, makesNoPlan, PHASES as ROUTE_PHASES, TASK_SIZES } from "../phase-routing.js";
26
27
  import { parseExecutionPlanDocument } from "../plan.js";
27
28
  import {} from "../planner-source.js";
28
29
  import { projectName } from "../project.js";
29
30
  import { dispatchStatusToken, manualReviewOnly, plainReasonWords, reviewConflict } from "../proof.js";
30
31
  import { providerName } from "../provider-auth.js";
31
- import { PROVIDER_IDS } from "../provider.js";
32
+ import {} from "../provider.js";
32
33
  import { qualityModeTitle } from "../quality.js";
33
- import {} from "../result-evidence-readers.js";
34
34
  import { evidenceShortenedWords } from "../result-review.js";
35
- import { acceptanceToLines, approvalOf, chainFromJson, scopeAuthorityOf } from "../scope.js";
35
+ import { acceptanceToLines, approvalOf, scopeAuthorityOf } from "../scope.js";
36
36
  import {} from "../store.js";
37
37
  import { runCostWords } from "../summary.js";
38
38
  import {} from "../task-control.js";
@@ -44,7 +44,7 @@ import { isOverdue, oneLineOf, projectChip, regionScript, safePrUrl, screen, SEN
44
44
  import { steerDestinationWords, steerStandingParts } from "../steer-status.js";
45
45
  import { reviewHref, taskChatHref, taskHref } from "./http.js";
46
46
  import { coordinatorProposalsSection } from "./render-chat.js";
47
- import { completionReceiptCard, criterionMatrixHtml, evidenceWords, homePhaseWords, incidentWords, needsCheck, pullRequestCardHtml, reasonWords, receiptStatusOf, repairChainHtml, reportItemsHtml, reviewRetryPanel, runNoun, runOutcomeBadge, semanticCoverageHtml, terminalDiffCard, verificationExplanation, verificationRecovered } from "./render-results.js";
47
+ import { completionReceiptCard, criterionMatrixHtml, evidenceWords, homePhaseWords, incidentWords, needsCheck, pullRequestCardHtml, reasonWords, receiptStatusOf, repairChainHtml, reportItemsHtml, reviewRetryPanel, runNoun, runOutcomeBadge, semanticCoverageHtml, verificationExplanation, verificationRecovered } from "./render-results.js";
48
48
  import {} from "./session.js";
49
49
  import { linkWords, originNote } from "../task-source.js";
50
50
  export function contractChangeHtml(change) {
@@ -542,7 +542,7 @@ export function profileWords(scope) {
542
542
  ? html `<p class="meta">Runs on <span class="mono">${ACP_AGENTS[profile.provider].name} (via ACP) · ${profile.model}</span> — full access inside the OS fence; the agent runs any tool it chooses and nothing asks. No turn limit: stops after ${Math.round(profile.timeoutSeconds / 60)} min, or ${Math.round(profile.idleTimeoutSeconds / 60)} min silent; repairs on ${repair}, ${Math.round(profile.repairTimeoutSeconds / 60)} min, or ${Math.round(profile.repairIdleTimeoutSeconds / 60)} min silent.</p>`
543
543
  : html `<p class="meta">Runs on <span class="mono">${profile.provider} · ${profile.model}</span> — ${profile.sandboxMode === "danger-full-access" ? "FULL permissions via --dangerously-bypass-approvals-and-sandbox; nothing asks" : "workspace-write sandbox"}, no turn limit (${Math.round(profile.timeoutSeconds / 60)} min ${profile.timeoutKind === "idle" ? "without progress" : "per attempt"}); repairs on ${repair}, ${Math.round(profile.repairTimeoutSeconds / 60)} min</p>`;
544
544
  // What the build may reach is part of what is approved (egress.ts): said on every approval form.
545
- return html `${base}<p class="meta" data-scope-network>${networkWords(profile.network ?? null)}</p><p class="meta" data-scope-writes>Writes: ${writesWords(profile.writes)}.</p>`;
545
+ return html `${base}<p class="meta" data-scope-network>${networkWords(profile.network ?? null)}</p><p class="meta" data-scope-writes>Writes: ${writesWords(profile.writes)}.</p>${profile.checkAccess === undefined ? "" : html `<p class="meta" data-scope-checks>Checks: ${checkAccessWords(profile.checkAccess)}.</p>`}`;
546
546
  }
547
547
  export const ROLE_NOUN = { plan: "Planner", build: "Builder", repair: "Repair", review: "Reviewer" };
548
548
  export function agentsStandingWords(view) {
@@ -91,9 +91,5 @@ export interface RouteDeclaration {
91
91
  export declare const ROUTES: readonly RouteDeclaration[];
92
92
  /** The first row that declares this method and path, or null: an undeclared request never reaches a handler. */
93
93
  export declare function matchRoute(method: string, pathname: string, stage?: Stage): RouteDeclaration | null;
94
- /** True when some row declares this path for another method (a wrong-method request). */
95
- export declare function declaredForAnotherMethod(method: string, pathname: string, stage?: Stage): boolean;
96
94
  /** Refuses a malformed table at startup: duplicate ids, duplicate method+pattern rows, or a row shadowed by an earlier one for its own sample. */
97
95
  export declare function assertRouteTable(routes?: readonly RouteDeclaration[]): void;
98
- /** The effective policy of every row, as plain data — what the snapshot test compares. */
99
- export declare const routePolicySnapshot: () => Array<Omit<RouteDeclaration, "sample">>;
@@ -284,6 +284,7 @@ post("settings", "settings.project-concurrency", "/settings/project/concurrency"
284
284
  post("settings", "settings.project-live-view", "/settings/project/live-view", { scope: "step-up", callers: ["cookie"] });
285
285
  post("settings", "settings.project-network", "/settings/project/network", { scope: "step-up", callers: ["cookie"] });
286
286
  post("settings", "settings.project-writes", "/settings/project/writes", { scope: "step-up", callers: ["cookie"] });
287
+ post("settings", "settings.project-check-access", "/settings/project/check-access", { scope: "step-up", callers: ["cookie"] });
287
288
  post("settings", "settings.project-delete", "/settings/project/delete", { scope: "step-up", ...operator("An instance operator deletes projects.", "/settings/project") });
288
289
  // Lead sweeps (docs/plans/lead-fan-out.md): the table, and cancelling what isn't ready. The handler shows a sweep only in
289
290
  // a project the reader can see, and acts only for an approver.
@@ -431,10 +432,6 @@ export function matchRoute(method, pathname, stage) {
431
432
  }
432
433
  return null;
433
434
  }
434
- /** True when some row declares this path for another method (a wrong-method request). */
435
- export function declaredForAnotherMethod(method, pathname, stage) {
436
- return compiled.some(({ route, test }) => (stage === undefined || route.stage === stage) && !methodMatches(route.method, method) && test.test(pathname));
437
- }
438
435
  /** Refuses a malformed table at startup: duplicate ids, duplicate method+pattern rows, or a row shadowed by an earlier one for its own sample. */
439
436
  export function assertRouteTable(routes = ROUTES) {
440
437
  const ids = new Set(), keys = new Set();
@@ -466,5 +463,3 @@ export function assertRouteTable(routes = ROUTES) {
466
463
  }
467
464
  }
468
465
  assertRouteTable();
469
- /** The effective policy of every row, as plain data — what the snapshot test compares. */
470
- export const routePolicySnapshot = () => ROUTES.map(({ sample: _sample, ...rest }) => ({ ...rest }));
@@ -32,7 +32,7 @@ import { budgetLabel, monthOf } from "../spend.js";
32
32
  import { PACKAGE_VERSION } from "../version.js";
33
33
  import { handlersOf } from './handler-registry.js';
34
34
  import { refuse, screen } from "./chrome.js";
35
- import { HANDOFF_STYLE, redirect, respond, safeReturn, signInSpent, startedHere } from "./http.js";
35
+ import { HANDOFF_STYLE, redirect, respond, safeReturn, sayTo, signInSpent, startedHere } from "./http.js";
36
36
  import { html, htmlString, joinHtml, postForm, reauth } from "../html.js";
37
37
  import { browserCaller } from "./session.js";
38
38
  /** settings handlers, moved without changing their route bodies. */
@@ -81,6 +81,7 @@ import { checkPolicy, parseList, policyParts } from "../policy.js";
81
81
  import { parseProjectConcurrency, saveProjectConcurrency } from "../project-concurrency.js";
82
82
  import { MAX_NETWORK_HOSTS, normalizeHost } from "../egress.js";
83
83
  import { isWritesMode } from "../writes-mode.js";
84
+ import { isCheckAccess, signInsUnavailableWords, widerThan } from "../check-access.js";
84
85
  import { projectDeleteConfirmHtml, projectSettingsHtml } from "../project-delete-ui.js";
85
86
  import { deleteProject, holdingsWords } from "../project-delete.js";
86
87
  import { applySavedKnowledge, changeKnowledge, knowledgeVersion, knowledgeView } from "../project-knowledge.js";
@@ -127,7 +128,7 @@ import { personChip, ROLE_TITLES } from "./chrome.js";
127
128
  import { chatReturnWithSaid, goOutside } from "./http.js";
128
129
  import { latestReleaseFor, leadSettingsHtml, notificationProjects, SETTINGS_AUTOSAVE_SCRIPT, settingsPage, settingsWorkers, telegramDeliveryWords, telegramTrouble } from "./render-settings.js";
129
130
  export function createSettingsHandlers(runtime) {
130
- const { store, providerHome, managedRepos, consoleProjects, sendScreen, chromeFor, connectionCheck, clock, modelCatalog, modelSeams, toolsViewOf, codexServers, projectViewOf, sessions, restricted, options, integrations, consoleOrigin, evidenceRoot, storagePool, visible, ssoSettings, checkLocalAgents, chatKeyFor, chatCatalog, leadWords, agentSignInCommand, settingsUpdates, phoneSetup, authenticateApprover, authenticateCredential, deletedRepos, recordSignIn, projectOf, providerFor, admissionList, toolHome, connectVisits, mintApprovalNonce, consumeApprovalNonce, defaultProject, googleVisits } = runtime;
131
+ const { store, providerHome, managedRepos, consoleProjects, sendScreen, chromeFor, connectionCheck, clock, modelCatalog, modelSeams, toolsViewOf, codexServers, projectViewOf, sessions, restricted, options, integrations, consoleOrigin, evidenceRoot, storagePool, visible, ssoSettings, checkLocalAgents, chatKeyFor, chatCatalog, leadWords, agentSignInCommand, settingsUpdates, phoneSetup, authenticateApprover, authenticateCredential, deletedRepos, projectOf, providerFor, admissionList, toolHome, connectVisits, mintApprovalNonce, consumeApprovalNonce, defaultProject, googleVisits } = runtime;
131
132
  // v105: what agent work cost, by month, and the monthly budgets. An instance operator's page.
132
133
  async function spendPage(ctx) {
133
134
  const { url, who, response, now } = ctx;
@@ -451,7 +452,7 @@ ${connectionHtml(asked, connection, task)}${command === null ? "" : html `<p>On
451
452
  // v105: Settings → Data, where an instance operator downloads everything. The download is a POST with the password.
452
453
  async function dataPage(ctx) {
453
454
  const { url, response, project } = ctx;
454
- const who = browserCaller(ctx.who);
455
+ browserCaller(ctx.who);
455
456
  return sendScreen(response, 200, screen("Data", html `<p><a href="/settings">Settings</a></p><h1>Data</h1>${(dataExportHtml({ problem: url.searchParams.get("problem") }))}`, { chrome: chromeFor(project, "settings") }));
456
457
  }
457
458
  // v100: Settings → Sign-in, the identity provider people sign in with. An instance operator's page.
@@ -486,7 +487,7 @@ ${connectionHtml(asked, connection, task)}${command === null ? "" : html `<p>On
486
487
  const { response, now, posted } = ctx;
487
488
  const who = browserCaller(ctx.who);
488
489
  const body = readForm(posted, CONSOLE_FORMS.passwordless);
489
- const back = (key, words) => redirect(response, `/settings/passwordless?${key}=${encodeURIComponent(words)}`);
490
+ const back = sayTo(response, "/settings/passwordless");
490
491
  const action = body.get("action");
491
492
  if (action === "off") {
492
493
  setPasswordless(store, false, who.name, now);
@@ -812,7 +813,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
812
813
  async function spendBudget(ctx) {
813
814
  const { who, response, now, posted } = ctx;
814
815
  const body = readForm(posted, CONSOLE_FORMS.spendBudget);
815
- const back = (key, words) => redirect(response, `/spend?${key}=${encodeURIComponent(words)}`);
816
+ const back = sayTo(response, "/spend");
816
817
  if (!authenticateApprover(who, body.get("password") ?? "").ok)
817
818
  return back("problem", "Enter your Toolroll password to change a budget.");
818
819
  const target = /^(installation|project|person|subagent):(.+)$/.exec(body.get("target") ?? "");
@@ -848,7 +849,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
848
849
  return refuse(response, who, 403, "That project is outside your access.", "/settings/project");
849
850
  if (who.via !== "cookie" || who.role !== "approver" || !store.accountCanAccess(who.name, repo))
850
851
  return refuse(response, who, 403, "An approver for this project sets how many tasks build at once.", "/settings/project");
851
- const back = (key, words) => redirect(response, `/settings/project?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}`);
852
+ const back = sayTo(response, `/settings/project?repo=${encodeURIComponent(repo)}`);
852
853
  const n = parseProjectConcurrency(body.get("concurrency") ?? "");
853
854
  if (n === null)
854
855
  return back("problem", "Builds at once is a whole number from 1 to 64. Nothing changed.");
@@ -870,7 +871,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
870
871
  return refuse(response, who, 403, "That project is outside your access.", "/settings/project");
871
872
  if (who.via !== "cookie" || who.role !== "approver" || store.isDemo() || !store.accountCanAccess(who.name, repo))
872
873
  return refuse(response, who, 403, "An approver for this project changes the live view.", "/settings/project");
873
- const back = (key, words) => redirect(response, `/settings/project?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}`);
874
+ const back = sayTo(response, `/settings/project?repo=${encodeURIComponent(repo)}`);
874
875
  const act = body.get("act");
875
876
  if (act === "off")
876
877
  return back("said", setLiveCommands(store, { repo, commands: false, by: who.name, via: "the console" }, now).said);
@@ -893,7 +894,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
893
894
  return refuse(response, who, 403, "That project is outside your access.", "/settings/project");
894
895
  if (who.via !== "cookie" || who.role !== "approver" || store.isDemo() || !store.accountCanAccess(who.name, repo))
895
896
  return refuse(response, who, 403, "An approver for this project changes what its builds may reach.", "/settings/project");
896
- const back = (key, words) => redirect(response, `/settings/project?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}#network`);
897
+ const back = sayTo(response, `/settings/project?repo=${encodeURIComponent(repo)}`, "#network");
897
898
  const hosts = (body.get("hosts") ?? "").split(/[\s,]+/).filter(Boolean).map(normalizeHost);
898
899
  if (hosts.some(one => one === null))
899
900
  return back("problem", "A host is a name like api.stripe.com or *.stripe.com: no address, port or path. Nothing changed.");
@@ -942,7 +943,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
942
943
  async function policySave(ctx) {
943
944
  const { who, response, now, posted } = ctx;
944
945
  const body = readForm(posted, CONSOLE_FORMS.policy);
945
- const back = (key, words) => redirect(response, `/settings/policy?${key}=${encodeURIComponent(words)}`);
946
+ const back = sayTo(response, "/settings/policy");
946
947
  if (!authenticateApprover(who, body.get("password") ?? "").ok)
947
948
  return back("problem", "That password didn't match. Nothing changed.");
948
949
  const chosen = body.getAll("provider");
@@ -962,7 +963,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
962
963
  const known = consoleProjects();
963
964
  if (!known.includes(repo))
964
965
  return refuse(response, who, 403, "That project is outside your access.", "/settings/approval");
965
- const back = (key, words) => redirect(response, `/settings/approval?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}`);
966
+ const back = sayTo(response, `/settings/approval?repo=${encodeURIComponent(repo)}`);
966
967
  if (!authenticateApprover(who, body.get("password") ?? "").ok)
967
968
  return back("problem", "That password didn't match. Nothing changed.");
968
969
  const protect = body.get("protect") ?? "none";
@@ -980,7 +981,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
980
981
  async function requestLimitsSave(ctx) {
981
982
  const { who, response, now, posted } = ctx;
982
983
  const body = readForm(posted, CONSOLE_FORMS.requestLimits);
983
- const back = (key, words) => redirect(response, `/settings/sessions?${key}=${encodeURIComponent(words)}`);
984
+ const back = sayTo(response, "/settings/sessions");
984
985
  const target = body.get("target") ?? "";
985
986
  const token = target === "*" ? null : store.apiTokens(null).find(one => one.id === target && one.revokedAt === null) ?? null;
986
987
  if (target !== "*" && token === null)
@@ -1004,7 +1005,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
1004
1005
  const body = readForm(posted, CONSOLE_FORMS.signIn);
1005
1006
  if (!options.configDir)
1006
1007
  return refuse(response, who, 403, "An instance operator sets up sign-in.", "/settings");
1007
- const back = (key, words) => redirect(response, `/settings/sign-in?${key}=${encodeURIComponent(words)}`);
1008
+ const back = sayTo(response, "/settings/sign-in");
1008
1009
  const action = body.get("action") ?? "";
1009
1010
  const before = ssoSettings();
1010
1011
  if (action === "test") {
@@ -1046,7 +1047,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
1046
1047
  if (databaseFile === null)
1047
1048
  return refuse(response, who, 403, "An instance operator updates Toolroll.", "/settings");
1048
1049
  const stateDir = dirname(databaseFile);
1049
- const back = (key, words) => redirect(response, `/settings/updates?${key}=${encodeURIComponent(words)}`);
1050
+ const back = sayTo(response, "/settings/updates");
1050
1051
  if (op === "seen") {
1051
1052
  markWhatsNewSeen(stateDir);
1052
1053
  return redirect(response, "/settings/updates");
@@ -1090,7 +1091,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
1090
1091
  async function retentionSave(ctx) {
1091
1092
  const { who, response, now, posted } = ctx;
1092
1093
  const body = readForm(posted, CONSOLE_FORMS.retention);
1093
- const back = (key, words) => redirect(response, `/settings/retention?${key}=${encodeURIComponent(words)}`);
1094
+ const back = sayTo(response, "/settings/retention");
1094
1095
  if (!authenticateApprover(who, body.get("password") ?? "").ok)
1095
1096
  return back("problem", "Enter your Toolroll password to change retention.");
1096
1097
  const before = store.retentionPeriods();
@@ -1112,7 +1113,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
1112
1113
  const databaseFile = store.databaseFile();
1113
1114
  if (databaseFile === null)
1114
1115
  return refuse(response, who, 403, "An instance operator looks after storage.", "/settings");
1115
- const back = (key, words) => redirect(response, `/settings/storage?${key}=${encodeURIComponent(words)}`);
1116
+ const back = sayTo(response, "/settings/storage");
1116
1117
  const what = op === "clean" ? "clean up" : op === "discard" ? "discard a checkout" : "change checkout cleanup";
1117
1118
  if (!authenticateApprover(who, body.get("password") ?? "").ok)
1118
1119
  return back("problem", `Enter your Toolroll password to ${what}.`);
@@ -1152,7 +1153,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
1152
1153
  return refuse(response, who, 404, "No such project.", "/projects");
1153
1154
  if (who.role !== "approver" || store.isDemo())
1154
1155
  return refuse(response, who, 403, "An approver sets up pull requests.", "/projects");
1155
- const back = (key, words) => redirect(response, `/settings/pull-requests?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}`);
1156
+ const back = sayTo(response, `/settings/pull-requests?repo=${encodeURIComponent(repo)}`);
1156
1157
  const act = body.get("act");
1157
1158
  if (!authenticateApprover(who, body.get("password") ?? "", repo).ok)
1158
1159
  return back("problem", "Enter your Toolroll password to change pull requests.");
@@ -1193,7 +1194,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
1193
1194
  return refuse(response, who, 404, "No such project.", "/projects");
1194
1195
  if (who.role !== "approver" || store.isDemo())
1195
1196
  return refuse(response, who, 403, "An approver sets a project's checks.", "/projects");
1196
- const back = (key, words) => redirect(response, `/settings/checks?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}`);
1197
+ const back = sayTo(response, `/settings/checks?repo=${encodeURIComponent(repo)}`);
1197
1198
  if (!authenticateApprover(who, body.get("password") ?? "", repo).ok)
1198
1199
  return back("problem", "Enter your Toolroll password to change checks.");
1199
1200
  const act = body.get("act");
@@ -1256,7 +1257,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
1256
1257
  const databaseFile = store.databaseFile();
1257
1258
  if (databaseFile === null)
1258
1259
  return refuse(response, who, 403, "An instance operator looks after backups.", "/settings");
1259
- const back = (key, words) => redirect(response, `/settings/backups?${key}=${encodeURIComponent(words)}`);
1260
+ const back = sayTo(response, "/settings/backups");
1260
1261
  if (!authenticateApprover(who, body.get("password") ?? "").ok)
1261
1262
  return back("problem", "Enter your Toolroll password to change backups.");
1262
1263
  const every = body.get("every") ?? "";
@@ -1296,7 +1297,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
1296
1297
  const { who, response, posted } = ctx;
1297
1298
  const body = readForm(posted, CONSOLE_FORMS.flowsOn);
1298
1299
  const repo = body.get("repo") ?? "";
1299
- const back = (key, words) => redirect(response, `/settings/flows?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}`);
1300
+ const back = sayTo(response, `/settings/flows?repo=${encodeURIComponent(repo)}`);
1300
1301
  const known = [...new Set([...(admissionList() ?? []), ...managedRepos(), ...store.knownRepos()])];
1301
1302
  if (store.isDemo())
1302
1303
  return refuse(response, who, 403, "An approver switches starter flows on.", "/settings/flows");
@@ -1315,7 +1316,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
1315
1316
  if (!options.configDir)
1316
1317
  return refuse(response, who, 403, "An installation approver tests integrations.", "/settings");
1317
1318
  const key = body.get("key") ?? "";
1318
- const back = (name, words) => redirect(response, `/settings/integrations?${name}=${encodeURIComponent(words)}`);
1319
+ const back = sayTo(response, "/settings/integrations");
1319
1320
  const checked = (await integrations.check([key])).find(one => one.key === key);
1320
1321
  if (checked === undefined)
1321
1322
  return back("problem", "That integration isn't here any more.");
@@ -1328,7 +1329,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
1328
1329
  const body = readForm(posted, CONSOLE_FORMS.monitoring);
1329
1330
  if (!options.configDir)
1330
1331
  return refuse(response, who, 403, "An instance operator sets up monitoring.", "/settings");
1331
- const back = (key, words) => redirect(response, `/settings/monitoring?${key}=${encodeURIComponent(words)}`);
1332
+ const back = sayTo(response, "/settings/monitoring");
1332
1333
  if (!authenticateApprover(who, body.get("password") ?? "").ok)
1333
1334
  return back("problem", "Enter your Toolroll password to change monitoring.");
1334
1335
  const before = readMonitoring(options.configDir);
@@ -1429,7 +1430,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
1429
1430
  const repo = body.get("repo") ?? "";
1430
1431
  if (!visible(repo) || !consoleProjects().includes(repo))
1431
1432
  return refuse(response, who, 403, "That project is outside your access.", "/projects");
1432
- const back = (key, words, anchor = "") => redirect(response, `/settings/tools?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}${anchor}`);
1433
+ const back = sayTo(response, `/settings/tools?repo=${encodeURIComponent(repo)}`);
1433
1434
  // As for Connect: a post from a page that showed another project is refused, never applied here.
1434
1435
  const shown = body.get("shown") ?? "";
1435
1436
  if (shown !== repo) {
@@ -2485,6 +2486,7 @@ ${modeWords(terms)
2485
2486
  "settings.project-live-view": projectLiveView,
2486
2487
  "settings.project-network": projectNetwork,
2487
2488
  "settings.project-writes": projectWrites,
2489
+ "settings.project-check-access": projectCheckAccess,
2488
2490
  "settings.project-delete": projectDelete,
2489
2491
  "settings.policy-send": policySave,
2490
2492
  "settings.approval-send": approvalSave,
@@ -2578,7 +2580,7 @@ ${modeWords(terms)
2578
2580
  return refuse(response, who, 403, "That project is outside your access.", "/settings/project");
2579
2581
  if (who.via !== "cookie" || who.role !== "approver" || store.isDemo() || !store.accountCanAccess(who.name, repo))
2580
2582
  return refuse(response, who, 403, "An approver for this project changes where its builds may write.", "/settings/project");
2581
- const back = (key, words) => redirect(response, `/settings/project?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}#writes`);
2583
+ const back = sayTo(response, `/settings/project?repo=${encodeURIComponent(repo)}`, "#writes");
2582
2584
  const wanted = body.get("writes");
2583
2585
  if (!isWritesMode(wanted))
2584
2586
  return back("problem", "Choose where builds may write. Nothing changed.");
@@ -2587,5 +2589,40 @@ ${modeWords(terms)
2587
2589
  const changed = store.setProjectWrites(repo, wanted, who.name, now);
2588
2590
  return back("said", changed.before === changed.after ? "Nothing changed." : "Saved. Tasks scoped from now on use it; tasks already approved keep what they were approved with.");
2589
2591
  }
2592
+ // Settings → Project → Check access (check-access.ts). Widening is an instance operator's act with a password typed now
2593
+ // (an identity-provider re-check doesn't count); any approver for the project narrows it. The level is compared again
2594
+ // inside the write, so a change made meanwhile can't turn a narrowing into a widening. The ledger keeps before → after.
2595
+ async function projectCheckAccess(ctx) {
2596
+ const { who, response, now, posted } = ctx;
2597
+ const body = readForm(posted, CONSOLE_FORMS.projectCheckAccess);
2598
+ const repo = body.get("repo") ?? "";
2599
+ if (!consoleProjects().includes(repo) || !visible(repo))
2600
+ return refuse(response, who, 403, "That project is outside your access.", "/settings/project");
2601
+ if (who.via !== "cookie" || who.role !== "approver" || store.isDemo() || !store.accountCanAccess(who.name, repo))
2602
+ return refuse(response, who, 403, "An approver for this project changes how far its checks reach.", "/settings/project");
2603
+ const back = sayTo(response, `/settings/project?repo=${encodeURIComponent(repo)}`, "#check-access");
2604
+ const wanted = body.get("level");
2605
+ if (!isCheckAccess(wanted))
2606
+ return back("problem", "Choose how far checks reach. Nothing changed.");
2607
+ // Not available yet (check-access.ts signInsAvailable): refused before the password, whatever the form sent.
2608
+ const unavailable = wanted === "sign-ins" ? signInsUnavailableWords(options.platform ?? process.platform) : null;
2609
+ if (unavailable !== null)
2610
+ return back("problem", `${unavailable} Nothing changed.`);
2611
+ const widening = widerThan(wanted, store.projectCheckAccess(repo));
2612
+ // Sign-ins with an open network: a second, explicit yes, said before the password.
2613
+ if (wanted === "sign-ins" && widening && store.projectNetwork(repo).mode === "open" && body.get("confirm-open") !== "yes") {
2614
+ return back("problem", "This project's network is open: confirm that its checks could send your sign-ins to any host. Nothing changed.");
2615
+ }
2616
+ const proved = authenticateCredential(who, body.get("password") ?? "");
2617
+ if (!proved.ok)
2618
+ return back("problem", "Enter your Toolroll password to change how far checks reach.");
2619
+ if (widening && (proved.method !== "password-now" || !store.isInstanceOperator(who.name))) {
2620
+ return back("problem", "Only an instance operator widens how far checks reach, with their password typed now. Nothing changed.");
2621
+ }
2622
+ const changed = store.setProjectCheckAccess(repo, wanted, who.name, now, { may: widening ? "widen" : "narrow" });
2623
+ if (changed === null)
2624
+ return back("problem", "Check access changed meanwhile, and this would now widen it. Nothing changed.");
2625
+ return back("said", changed.before === changed.after ? "Nothing changed." : "Saved. Tasks scoped from now on use it; tasks already approved keep what they were approved with.");
2626
+ }
2590
2627
  return { registrations, modelsView };
2591
2628
  }
@@ -3,7 +3,7 @@ import { liveAttemptOf, steerControlsFor } from "../steer-controls.js";
3
3
  import { originOf } from "../task-source.js";
4
4
  import { taskBaseOf } from "../task-base.js";
5
5
  import { assignmentPresentationOf, historicalAssessmentReason } from '../assignment-presentation.js';
6
- import { html, htmlString, joinHtml } from "../html.js";
6
+ import { html, joinHtml } from "../html.js";
7
7
  import { assignmentStatusOf, assignmentWithEvidence } from '../assignment-ui.js';
8
8
  import { assignmentOf } from '../assignment.js';
9
9
  import { knowledgeContextHtml } from "../knowledge-ui.js";