toolroll 0.9.63 → 0.9.67

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 (302) hide show
  1. package/CHANGELOG.md +108 -0
  2. package/README.md +33 -5
  3. package/dist/acp-agents.d.ts +43 -0
  4. package/dist/acp-agents.js +62 -0
  5. package/dist/acp-attest.d.ts +44 -0
  6. package/dist/acp-attest.js +269 -0
  7. package/dist/acp-check.d.ts +28 -0
  8. package/dist/acp-check.js +410 -0
  9. package/dist/acp-client.d.ts +163 -0
  10. package/dist/acp-client.js +738 -0
  11. package/dist/acp-inspect.d.ts +23 -0
  12. package/dist/acp-inspect.js +109 -0
  13. package/dist/agent-board.d.ts +19 -6
  14. package/dist/agent-board.js +30 -9
  15. package/dist/agent-fence.d.ts +117 -2
  16. package/dist/agent-fence.js +439 -12
  17. package/dist/agent-pulse.js +9 -2
  18. package/dist/agent-state-words.d.ts +19 -0
  19. package/dist/agent-state-words.js +20 -0
  20. package/dist/agent-state.d.ts +65 -0
  21. package/dist/agent-state.js +229 -0
  22. package/dist/agentconfig.js +39 -13
  23. package/dist/agents-board.d.ts +34 -2
  24. package/dist/agents-board.js +110 -1
  25. package/dist/approval-rules-ui.js +2 -2
  26. package/dist/assignment-delivery.d.ts +14 -0
  27. package/dist/assignment-delivery.js +13 -0
  28. package/dist/assignment.js +13 -0
  29. package/dist/attest.js +7 -0
  30. package/dist/backup-ui.js +2 -2
  31. package/dist/batch-checks.js +3 -2
  32. package/dist/browser/THIRD_PARTY_NOTICES.txt +94 -0
  33. package/dist/browser/terminal.css +37 -0
  34. package/dist/browser/terminal.js +228 -0
  35. package/dist/browser/workspace.css +2 -2
  36. package/dist/browser/workspace.js +42 -38
  37. package/dist/browser-shell.js +6 -3
  38. package/dist/browser-workspace.d.ts +235 -1
  39. package/dist/browser-workspace.js +60 -0
  40. package/dist/builder.d.ts +10 -1
  41. package/dist/builder.js +87 -24
  42. package/dist/chat-actions.d.ts +13 -1
  43. package/dist/chat-actions.js +197 -15
  44. package/dist/chat-ask.d.ts +4 -0
  45. package/dist/chat-ask.js +14 -0
  46. package/dist/chat-channel.js +20 -0
  47. package/dist/chat-continuity.js +5 -4
  48. package/dist/chat-decide.d.ts +0 -14
  49. package/dist/chat-decide.js +4 -1
  50. package/dist/chat-delivery.d.ts +9 -0
  51. package/dist/chat-delivery.js +45 -6
  52. package/dist/chat-messages.d.ts +8 -0
  53. package/dist/chat-messages.js +36 -4
  54. package/dist/check-levels-ui.js +2 -2
  55. package/dist/checkout-proof.d.ts +28 -0
  56. package/dist/checkout-proof.js +62 -0
  57. package/dist/claim.d.ts +12 -0
  58. package/dist/claim.js +19 -0
  59. package/dist/cli-help.d.ts +1 -1
  60. package/dist/cli-help.js +4 -1
  61. package/dist/cli.d.ts +3 -0
  62. package/dist/cli.js +21 -2
  63. package/dist/coding-ui.js +6 -4
  64. package/dist/console-instance.d.ts +124 -0
  65. package/dist/console-instance.js +337 -0
  66. package/dist/contracts/chat-actions.d.ts +16 -0
  67. package/dist/contracts/chat-actions.js +4 -0
  68. package/dist/contracts/chat-content.d.ts +19 -0
  69. package/dist/contracts/chat-content.js +6 -0
  70. package/dist/contracts/console-api.d.ts +13 -4
  71. package/dist/contracts/console-api.js +13 -4
  72. package/dist/contracts/flow-card.d.ts +4 -0
  73. package/dist/contracts/flow-card.js +3 -0
  74. package/dist/contracts/flow.d.ts +46 -0
  75. package/dist/contracts/flow.js +11 -2
  76. package/dist/contracts/lead-tools.d.ts +92 -0
  77. package/dist/contracts/lead-tools.js +13 -0
  78. package/dist/contracts/provider.d.ts +11 -1
  79. package/dist/contracts/provider.js +1 -1
  80. package/dist/contracts/route.d.ts +76 -1
  81. package/dist/contracts/route.js +1 -1
  82. package/dist/contracts/store-json.d.ts +2 -0
  83. package/dist/control-setup.d.ts +1 -1
  84. package/dist/control-ui.js +2 -2
  85. package/dist/converse.d.ts +16 -0
  86. package/dist/converse.js +32 -4
  87. package/dist/demo-chat.js +4 -4
  88. package/dist/demo.js +2 -2
  89. package/dist/desktop-update-gate.js +6 -0
  90. package/dist/desktop-update.d.ts +1 -1
  91. package/dist/discord-chat.js +4 -1
  92. package/dist/doctor.js +1 -1
  93. package/dist/egress-bridge.d.ts +17 -0
  94. package/dist/egress-bridge.js +111 -0
  95. package/dist/egress.d.ts +119 -0
  96. package/dist/egress.js +423 -0
  97. package/dist/exec.d.ts +78 -0
  98. package/dist/exec.js +367 -9
  99. package/dist/export-ui.js +2 -2
  100. package/dist/flow-engine.js +24 -5
  101. package/dist/flow-gallery-ui.js +2 -2
  102. package/dist/flow-gallery.js +27 -8
  103. package/dist/flow-share.js +16 -5
  104. package/dist/flow-triggers.d.ts +77 -8
  105. package/dist/flow-triggers.js +517 -70
  106. package/dist/flows-cli.js +8 -2
  107. package/dist/flows-ui.d.ts +0 -2
  108. package/dist/flows-ui.js +52 -4
  109. package/dist/flows.d.ts +4 -0
  110. package/dist/flows.js +9 -1
  111. package/dist/html.d.ts +9 -0
  112. package/dist/html.js +11 -0
  113. package/dist/invoke.d.ts +35 -0
  114. package/dist/invoke.js +347 -28
  115. package/dist/keys.d.ts +18 -14
  116. package/dist/keys.js +5 -0
  117. package/dist/lead-capabilities.js +3 -2
  118. package/dist/lead-contract.js +2 -0
  119. package/dist/lead-doors.d.ts +2 -0
  120. package/dist/lead-doors.js +7 -0
  121. package/dist/lead-follow.d.ts +2 -0
  122. package/dist/lead-follow.js +58 -2
  123. package/dist/lead-progress.js +1 -0
  124. package/dist/lead-queue-runtime.d.ts +46 -0
  125. package/dist/lead-queue-runtime.js +191 -0
  126. package/dist/lead-queue.d.ts +104 -0
  127. package/dist/lead-queue.js +187 -0
  128. package/dist/lead-sweep.d.ts +67 -0
  129. package/dist/lead-sweep.js +99 -0
  130. package/dist/lead-tools.d.ts +22 -0
  131. package/dist/lead-tools.js +152 -1
  132. package/dist/lead.d.ts +13 -5
  133. package/dist/lead.js +89 -29
  134. package/dist/limits-ui.d.ts +8 -1
  135. package/dist/limits-ui.js +26 -0
  136. package/dist/live-bus.d.ts +8 -0
  137. package/dist/live-bus.js +40 -4
  138. package/dist/live-detail.d.ts +27 -0
  139. package/dist/live-detail.js +57 -0
  140. package/dist/live-notes.d.ts +20 -0
  141. package/dist/live-notes.js +47 -0
  142. package/dist/live-records.d.ts +140 -0
  143. package/dist/live-records.js +935 -0
  144. package/dist/live-terminal.d.ts +269 -0
  145. package/dist/live-terminal.js +686 -0
  146. package/dist/live.d.ts +38 -27
  147. package/dist/live.js +142 -124
  148. package/dist/mcp-oauth.d.ts +6 -3
  149. package/dist/mcp-oauth.js +8 -4
  150. package/dist/memory-pass.js +2 -2
  151. package/dist/mobile-viewport.d.ts +5 -1
  152. package/dist/mobile-viewport.js +18 -2
  153. package/dist/model-catalog.js +4 -0
  154. package/dist/monitoring-ui.js +2 -2
  155. package/dist/needs-you.d.ts +1 -1
  156. package/dist/needs-you.js +11 -4
  157. package/dist/operate.d.ts +4 -0
  158. package/dist/operate.js +335 -19
  159. package/dist/passwordless.d.ts +23 -0
  160. package/dist/passwordless.js +26 -0
  161. package/dist/peek-cli.d.ts +12 -4
  162. package/dist/peek-cli.js +19 -11
  163. package/dist/phase-routing.js +10 -3
  164. package/dist/planner-source.d.ts +3 -0
  165. package/dist/planner-source.js +14 -0
  166. package/dist/planner.d.ts +2 -1
  167. package/dist/planner.js +7 -1
  168. package/dist/policy-ui.js +2 -2
  169. package/dist/policy.d.ts +1 -20
  170. package/dist/policy.js +29 -2
  171. package/dist/project-delete-ui.d.ts +18 -0
  172. package/dist/project-delete-ui.js +60 -9
  173. package/dist/project-delete.js +2 -2
  174. package/dist/project-memory.d.ts +3 -1
  175. package/dist/project-memory.js +57 -25
  176. package/dist/project-tools.d.ts +5 -3
  177. package/dist/project-tools.js +12 -9
  178. package/dist/provider-auth.d.ts +4 -3
  179. package/dist/provider-auth.js +5 -4
  180. package/dist/provider-connection.d.ts +2 -1
  181. package/dist/provider-connection.js +5 -0
  182. package/dist/provider.d.ts +25 -2
  183. package/dist/provider.js +63 -9
  184. package/dist/pull-request-ui.js +2 -2
  185. package/dist/push.js +1 -1
  186. package/dist/quick-start.d.ts +24 -0
  187. package/dist/quick-start.js +127 -0
  188. package/dist/release-adapters.d.ts +6 -0
  189. package/dist/release-adapters.js +70 -3
  190. package/dist/release.d.ts +10 -1
  191. package/dist/release.js +34 -15
  192. package/dist/request-budget-ui.js +2 -2
  193. package/dist/result-follow-ups.js +3 -2
  194. package/dist/result-review.d.ts +1 -1
  195. package/dist/result-review.js +9 -3
  196. package/dist/retention-ui.js +2 -2
  197. package/dist/retention.js +10 -1
  198. package/dist/run-efficiency.d.ts +73 -0
  199. package/dist/run-efficiency.js +228 -0
  200. package/dist/runner.d.ts +5 -1
  201. package/dist/runner.js +22 -1
  202. package/dist/scope.d.ts +183 -5
  203. package/dist/scope.js +256 -31
  204. package/dist/scout-net.d.ts +4 -1
  205. package/dist/scout-net.js +37 -12
  206. package/dist/scout.d.ts +20 -6
  207. package/dist/scout.js +27 -3
  208. package/dist/serve.js +248 -40
  209. package/dist/server/chat.d.ts +1 -1
  210. package/dist/server/chat.js +122 -31
  211. package/dist/server/chrome.d.ts +16 -2
  212. package/dist/server/chrome.js +63 -11
  213. package/dist/server/console.css +107 -42
  214. package/dist/server/flows.js +38 -8
  215. package/dist/server/guards.d.ts +1 -0
  216. package/dist/server/guards.js +23 -9
  217. package/dist/server/http.d.ts +7 -0
  218. package/dist/server/http.js +3 -2
  219. package/dist/server/live.d.ts +34 -1
  220. package/dist/server/live.js +39 -17
  221. package/dist/server/pages-assets.js +5 -0
  222. package/dist/server/pages.js +90 -13
  223. package/dist/server/people-tokens.d.ts +3 -0
  224. package/dist/server/people-tokens.js +55 -17
  225. package/dist/server/remote-hooks.js +12 -6
  226. package/dist/server/remote.d.ts +2 -1
  227. package/dist/server/remote.js +14 -5
  228. package/dist/server/render-chat.d.ts +12 -3
  229. package/dist/server/render-chat.js +47 -16
  230. package/dist/server/render-pages.d.ts +32 -4
  231. package/dist/server/render-pages.js +83 -13
  232. package/dist/server/render-people.d.ts +3 -2
  233. package/dist/server/render-people.js +3 -2
  234. package/dist/server/render-results.d.ts +18 -2
  235. package/dist/server/render-results.js +37 -28
  236. package/dist/server/render-settings.js +6 -5
  237. package/dist/server/render-tasks.d.ts +35 -10
  238. package/dist/server/render-tasks.js +111 -27
  239. package/dist/server/request-context.d.ts +2 -0
  240. package/dist/server/request-context.js +2 -1
  241. package/dist/server/route-table.d.ts +3 -0
  242. package/dist/server/route-table.js +47 -5
  243. package/dist/server/runtime.d.ts +33 -2
  244. package/dist/server/session.d.ts +25 -1
  245. package/dist/server/session.js +18 -4
  246. package/dist/server/settings.js +142 -10
  247. package/dist/server/sweeps.d.ts +4 -0
  248. package/dist/server/sweeps.js +58 -0
  249. package/dist/server/task-views.d.ts +2 -1
  250. package/dist/server/task-views.js +96 -6
  251. package/dist/server/tasks.js +327 -78
  252. package/dist/server/terminals.d.ts +31 -0
  253. package/dist/server/terminals.js +365 -0
  254. package/dist/setup-guide.js +3 -2
  255. package/dist/slack-chat.js +4 -1
  256. package/dist/spend-ui.js +3 -3
  257. package/dist/spend.js +2 -1
  258. package/dist/steer-capabilities.d.ts +32 -0
  259. package/dist/steer-capabilities.js +60 -0
  260. package/dist/steer-controls.d.ts +14 -0
  261. package/dist/steer-controls.js +36 -0
  262. package/dist/steer-status.js +2 -2
  263. package/dist/storage-ui.js +4 -4
  264. package/dist/store-baseline.d.ts +1 -1
  265. package/dist/store-baseline.js +1 -1
  266. package/dist/store.d.ts +440 -15
  267. package/dist/store.js +1072 -52
  268. package/dist/subagent-desk.js +1 -1
  269. package/dist/summary.js +3 -0
  270. package/dist/task-source.d.ts +58 -0
  271. package/dist/task-source.js +93 -0
  272. package/dist/team-leads.js +1 -1
  273. package/dist/teams-chat.js +4 -1
  274. package/dist/teardown-git.d.ts +22 -0
  275. package/dist/teardown-git.js +136 -2
  276. package/dist/telegram-lead.d.ts +3 -0
  277. package/dist/telegram-lead.js +44 -7
  278. package/dist/telegram.d.ts +11 -2
  279. package/dist/telegram.js +51 -17
  280. package/dist/terminal-catalog.d.ts +11 -0
  281. package/dist/terminal-catalog.js +129 -0
  282. package/dist/text-limits.d.ts +2 -0
  283. package/dist/text-limits.js +2 -0
  284. package/dist/tool-broker.d.ts +14 -0
  285. package/dist/tool-broker.js +67 -2
  286. package/dist/toolroll-update-ui.js +2 -2
  287. package/dist/toolroll-update.js +13 -3
  288. package/dist/tools-ui.js +2 -2
  289. package/dist/unfenced-test-support.d.ts +2 -0
  290. package/dist/unfenced-test-support.js +11 -0
  291. package/dist/webhook-limits.d.ts +23 -0
  292. package/dist/webhook-limits.js +30 -0
  293. package/dist/webhook-readers.d.ts +56 -0
  294. package/dist/webhook-readers.js +138 -0
  295. package/dist/webhook-signing.d.ts +27 -0
  296. package/dist/webhook-signing.js +119 -0
  297. package/dist/work-index.d.ts +29 -0
  298. package/dist/work-index.js +16 -2
  299. package/dist/workspace-revision.js +3 -1
  300. package/dist/writes-mode.d.ts +9 -0
  301. package/dist/writes-mode.js +8 -0
  302. package/package.json +15 -2
package/dist/serve.js CHANGED
@@ -1,6 +1,16 @@
1
+ import { taskMovedSince } from "./chat-ask.js";
2
+ import { LIVE_VIEW_CONSENT, liveCommandsShown, liveViewConsentDigest } from "./live-detail.js";
1
3
  import { html, htmlString, jsonScript, textHtml } from "./html.js";
2
4
  import { browserAssetsAvailable, browserWorkspaceDocument } from './browser-shell.js';
3
- import { browserCrewFromIndex, browserCrewOf, browserNavigationOf, browserProjectsOf, needsYouLabelOf } from './browser-workspace.js';
5
+ import { NO_COUNTS, projectStates, STATE_WORDS } from './agent-state.js';
6
+ import { usageChipOf } from './limits-ui.js';
7
+ /** Tasks the rail lists under a project; the rest are a count that links to the project's tasks. */
8
+ const RAIL_TASKS = 6;
9
+ /** Any header a proxy adds: a request carrying one is never "from this computer" (password freshness, one-time links). */
10
+ const PROXY_HEADER = /^(forwarded|via|x-forwarded-.+|x-real-ip|x-client-ip|x-original-.+|x-cluster-client-ip|true-client-ip|cf-connecting-ip|fastly-client-ip|x-proxyuser-ip)$/i;
11
+ /** Asks a project names for the chime (its count is complete beyond them). */
12
+ const ASKS_PER_PROJECT = 100;
13
+ import { boundProjectSummary, browserCrewFromIndex, browserCrewOf, browserNavigationOf, browserProjectsOf, needsYouLabelOf } from './browser-workspace.js';
4
14
  import { sharedActionPayload } from './chat-actions.js';
5
15
  import { prepareCodingContext } from './coding-context.js';
6
16
  import { CodingWorkspace } from './coding-workspace.js';
@@ -28,8 +38,9 @@ import { createTaskViews } from "./server/task-views.js";
28
38
  import { runActivityOf } from "./task-activity.js";
29
39
  import { createTaskRooms, taskFingerprint } from "./task-live.js";
30
40
  import { createTeamRuntime } from './team-runtime.js';
41
+ import { createPersonalQueueRuntime } from './lead-queue-runtime.js';
31
42
  import {} from "./tools-ui.js";
32
- import { workCountsByProject } from "./work-index.js";
43
+ import { WORK_INDEX_MAX_LIMIT, workCountsByProject, workIndexPage } from "./work-index.js";
33
44
  import { WORKSPACE_MOTION_SCRIPT } from "./workspace-motion.js";
34
45
  import { prepareWorkspaceRevision, WorkspaceValidatorCache } from "./workspace-revision.js";
35
46
  /**
@@ -111,10 +122,13 @@ import { limitWords, FLUSH_MS as REQUEST_BUDGET_FLUSH_MS, RequestBudget, SOURCE_
111
122
  import { REQUEST_TOKEN, RESULT_REVIEW_SCRIPT } from "./result-review.js";
112
123
  import { buildReviewOf } from "./review-switch.js";
113
124
  import { isAlive as runnerAlive } from "./runner.js";
114
- import { freshIdentitySignIn } from "./scope.js";
125
+ import { FRESH_WINDOW_MS, freshFacts, ssoNowAvailable } from "./scope.js";
115
126
  import { instrumentRequest } from "./server-telemetry.js";
116
127
  import { createChatHandlers } from './server/chat.js';
117
128
  import { createLiveHandlers } from './server/live.js';
129
+ import { createTerminalHandlers, terminalHostFor } from './server/terminals.js';
130
+ import { passwordlessOn } from './passwordless.js';
131
+ import { createSweepHandlers } from './server/sweeps.js';
118
132
  import { createFlowsHandlers } from './server/flows.js';
119
133
  import { createPagesHandlers } from './server/pages.js';
120
134
  import { createPeopleHandlers } from './server/people-tokens.js';
@@ -122,10 +136,10 @@ import { createSettingsHandlers } from './server/settings.js';
122
136
  import { requestContext } from "./server/request-context.js";
123
137
  import { chromeScript, DEMO_BANNER, DEMO_BANNER_SHORT, focusDocument, KBD_HELP, pinnedTheme, QUEUE_VIEW, refuse, screen, SENSITIVE_INPUT, shell, sidebarScript } from "./server/chrome.js";
124
138
  import { BODY_CAP, form, loginHref, matchTaskPath, page, projectChatHref, redactedPath, redirect, respond, safeReturn, SHUTDOWN_WAIT_MS, taskChatHref } from "./server/http.js";
125
- import { decisionsFor, LEAD_BY_DEFAULT_FACT, leadBrowserMessages, leadChatVersion, teamProposalCardParts } from "./server/render-chat.js";
139
+ import { decisionsFor, LEAD_BY_DEFAULT_FACT, leadBrowserMessages, leadChatVersion, TASK_COMPOSER_MODES, teamProposalCardParts } from "./server/render-chat.js";
126
140
  import {} from "./server/render-pages.js";
127
141
  import { ssoStepUps, wrongHostPage } from "./server/render-people.js";
128
- import { NO_PROJECT, NO_TOUCH_FRAGMENTS, NONCE_CAP, NONCE_TTL_MS, PersistentSessions, SESSION_ABSOLUTE_MS, SESSION_IDLE_MS, SIGN_IN_LINK_MS, SIGN_IN_LINK_PATH } from "./server/session.js";
142
+ import { NO_PROJECT, NO_TOUCH_FRAGMENTS, NONCE_CAP, NONCE_TTL_MS, PersistentSessions, SESSION_ABSOLUTE_MS, SESSION_IDLE_MS, sessionWithinLifetime, SIGN_IN_LINK_MS, SIGN_IN_LINK_PATH } from "./server/session.js";
129
143
  import { createTasksHandlers } from './server/tasks.js';
130
144
  import { DEFAULT_GUARD_POLICY, passwordGuardOf, provenPasswordAccount, SourceBudget, withPasswordSource } from "./sign-in-guard.js";
131
145
  import { sourceKey } from "./source-key.js";
@@ -156,7 +170,7 @@ export function createDecisionServer(options) {
156
170
  get dockedConversation() { return dockedConversation; },
157
171
  get liveRefreshSeconds() { return liveRefreshSeconds; },
158
172
  });
159
- const { authenticateApprover, authorizeMutation, projectOf, resolveRouteProject, projectRequestAllowed, identify, lookupSession, restricted, visible, admissionList, codingActorAllowed, codingProjectAllowed, workAccess, familyOf, taskRepoOf, runVisible } = createSharedGuards({
173
+ const { authenticateApprover, authenticateCredential, authorizeMutation, projectOf, resolveRouteProject, projectRequestAllowed, identify, lookupSession, restricted, visible, admissionList, codingActorAllowed, codingProjectAllowed, workAccess, familyOf, taskRepoOf, runVisible } = createSharedGuards({
160
174
  get store() { return store; },
161
175
  get liveCeiling() { return liveCeiling; },
162
176
  get codingProjects() { return codingProjects; },
@@ -186,6 +200,8 @@ export function createDecisionServer(options) {
186
200
  }
187
201
  }
188
202
  }
203
+ // Live terminals (docs/plans/live-terminal.md): on a machine that runs agents, when this install has a pty library.
204
+ const terminals = terminalHostFor(store, options);
189
205
  const clock = options.clock ?? (() => new Date());
190
206
  // Heavy reads (the work index behind `task list`) run on read-only worker connections, off the request loop.
191
207
  const readFile = store.isDemo() ? null : store.databaseFile();
@@ -286,13 +302,14 @@ export function createDecisionServer(options) {
286
302
  /** That source as every per-source budget counts it: a native IPv6 caller by its /64 (source-key.ts). */
287
303
  function joinSourceOf(request) { return sourceKey(forwardedSourceOf(request)); }
288
304
  const signInBudget = new SourceBudget();
305
+ // A re-check or a link is the session that started it: its kept hash and credential generation ride the visit and the
306
+ // hand-off, and finish refuses any other session (a sign-out and a new sign-in in between never inherit it).
289
307
  const ssoVisits = new Map();
290
308
  const ssoHandoffs = new Map();
291
309
  let ssoProvider = null;
292
310
  const ssoSettings = () => readSsoSettings(options.configDir);
293
311
  const ssoOffer = () => { const settings = ssoSettings(); return settings === null ? null : { label: settings.label, operatorsOnly: settings.passwords === "operators" }; };
294
312
  /** A step-up within this long of the provider checking someone needs no password. */
295
- const SSO_FRESH_MS = 10 * 60_000;
296
313
  // The one-time sign-in link `up` opens in the browser: single use, ten minutes, from this computer only. Only a hash
297
314
  // of the code is kept, in memory; the code itself is never written to a log or the ledger.
298
315
  const signInLinks = new Map();
@@ -310,13 +327,50 @@ export function createDecisionServer(options) {
310
327
  signInLinks.set(linkKey(code), { account, expires: Date.now() + SIGN_IN_LINK_MS });
311
328
  return `${SIGN_IN_LINK_PATH}${code}`;
312
329
  }
313
- /** This computer, and nothing in front of it: a loopback peer that names a loopback address, with no forwarding proxy. */
330
+ /** This computer, and nothing in front of it: a loopback peer that names a loopback address, with no sign of a proxy —
331
+ * any forwarding header at all (Forwarded, X-Forwarded-*, X-Real-IP, Via, …) disqualifies the request. */
314
332
  const fromThisComputer = (request) => {
315
333
  const peer = request.socket.remoteAddress ?? "";
316
- const loopback = LOOPBACK_PEERS.has(peer);
317
- return loopback && request.headers["x-forwarded-for"] === undefined && request.headers["forwarded"] === undefined
318
- && /^(localhost|127\.0\.0\.1|\[::1\]):[0-9]{1,5}$/.test(request.headers.host ?? "");
334
+ if (!LOOPBACK_PEERS.has(peer))
335
+ return false;
336
+ for (const name of Object.keys(request.headers))
337
+ if (PROXY_HEADER.test(name))
338
+ return false;
339
+ return /^(localhost|127\.0\.0\.1|\[::1\]):[0-9]{1,5}$/.test(request.headers.host ?? "");
319
340
  };
341
+ /**
342
+ * Cockpit B2: a request's password-freshness facts — the one place they are made (scope.ts honours them). Every
343
+ * reader looks at the session as it is at the moment of the act, never a snapshot taken before the body arrived:
344
+ * still signed in here, under the account's current credential generation, after any Forget.
345
+ */
346
+ function freshFactsOf(request, who, allowlisted = false) {
347
+ if (who?.via !== 'cookie')
348
+ return null;
349
+ const session = who.session, local = fromThisComputer(request);
350
+ // The same validity lookupSession applies, read again at the act: held here, within its idle and absolute
351
+ // lifetimes, under the account's current credential generation, not revoked.
352
+ const live = () => {
353
+ const now = Date.now(), account = store.accountOf(who.name);
354
+ return sessions.holds(session) && sessions.savedNow(session) && sessionWithinLifetime(session, now)
355
+ && account !== null && account.revokedAt === null && account.generation === session.generation;
356
+ };
357
+ return { actor: who.name, local, allowlisted, live,
358
+ passwordAt: () => session.passwordAt ?? null, ssoReauthAt: () => session.ssoReauthAt ?? null,
359
+ ssoNow: () => session.ssoNowAt ?? null,
360
+ consumeSsoNow: () => {
361
+ const was = session.ssoNowAt;
362
+ delete session.ssoNowAt;
363
+ // A refund puts back only what this spend took, once, and only if nothing newer arrived (Forget still floors it).
364
+ let refunded = false;
365
+ return () => { if (refunded)
366
+ return; refunded = true; if (was !== undefined && session.ssoNowAt === undefined)
367
+ session.ssoNowAt = was; };
368
+ },
369
+ floor: () => session.freshFloor ?? 0,
370
+ // Only a direct local request may start a password window (a typed password elsewhere proves now, nothing more).
371
+ stamp: at => { if (local && live())
372
+ session.passwordAt = at; } };
373
+ }
320
374
  async function providerFor(issuer) {
321
375
  if (ssoProvider !== null && ssoProvider.issuer === issuer && Date.now() - ssoProvider.at < 10 * 60_000)
322
376
  return { ok: true, provider: ssoProvider.provider };
@@ -392,6 +446,14 @@ export function createDecisionServer(options) {
392
446
  canDelete: who.via === "cookie" && store.isInstanceOperator(who.name),
393
447
  builds: projectBuildsOf(repo, who),
394
448
  access: projectAccessOf(repo),
449
+ network: { setting: store.projectNetwork(repo), canChange: who.via === "cookie" && who.role === "approver" && !store.isDemo() && store.accountCanAccess(who.name, repo) },
450
+ writes: { setting: store.projectWrites(repo), canChange: who.via === "cookie" && who.role === "approver" && !store.isDemo() && store.accountCanAccess(who.name, repo) },
451
+ liveView: (() => {
452
+ const commands = liveCommandsShown(store, repo);
453
+ const canChange = who.via === "cookie" && who.role === "approver" && !store.isDemo() && store.accountCanAccess(who.name, repo);
454
+ // Showing commands is consent to one exact change: a single-use nonce for this person, project and terms.
455
+ return { commands, canChange, ...(canChange && !commands ? { nonce: mintApprovalNonce(who.name, LIVE_VIEW_CONSENT, liveViewConsentDigest(repo)) } : {}) };
456
+ })(),
395
457
  });
396
458
  /** Settings → Project → Build access: the project's named tools and whether each loads for its builds. */
397
459
  const projectAccessOf = (repo) => {
@@ -474,6 +536,32 @@ export function createDecisionServer(options) {
474
536
  provider: teamChatProvider,
475
537
  ...(options.subscriptionChatRunner ? { subscriptionRunner: options.subscriptionChatRunner } : {}),
476
538
  });
539
+ // The personal lead queue (docs/plans/lead-message-queue.md). Projects and a message's task are re-proved AS the
540
+ // person (never from a live request's context), and its live reply display is the console's own (chat handlers below,
541
+ // reached only once the server is listening).
542
+ const asActor = (account, work) => requestContext.run({ csrf: "", returnTo: "/", actor: account }, work);
543
+ const admittedReposFor = (account) => asActor(account, () => managedRepos());
544
+ const personalQueue = createPersonalQueueRuntime({
545
+ store, evidenceRoot, clock, provider: teamChatProvider, admittedReposFor,
546
+ contextStands: (account, context) => asActor(account, () => {
547
+ const admitted = managedRepos();
548
+ if (context.project !== undefined && !admitted.includes(context.project))
549
+ return false;
550
+ if (context.task === undefined)
551
+ return true;
552
+ const family = store.taskFamilyOf(context.task, admitted, false);
553
+ if (family === null)
554
+ return false;
555
+ if (context.result === undefined)
556
+ return true;
557
+ const run = store.getRun(context.result);
558
+ return run !== null && family.versions.some(one => one.refId === run.taskRef);
559
+ }),
560
+ names: { project: projectName, modes: TASK_COMPOSER_MODES },
561
+ live: { begin: thread => chatHandlers.beginLiveTurn(thread), end: (thread, ok, progress) => chatHandlers.endLiveTurn(thread, ok, progress) },
562
+ ...(options.chatFetcher ? { fetcher: options.chatFetcher } : {}),
563
+ ...(options.subscriptionChatRunner ? { subscriptionRunner: options.subscriptionChatRunner } : {}),
564
+ });
477
565
  // Only browser sessions receive HTML cards. The CLI keeps its summary contract.
478
566
  const teamBrowserReply = (reply, actor, csrf) => {
479
567
  const snapshot = reply.snapshot;
@@ -676,7 +764,7 @@ export function createDecisionServer(options) {
676
764
  // Passive polls authenticate without extending the browser session.
677
765
  const fragmentName = url.searchParams.get("fragment");
678
766
  const workspaceRead = method === "GET" && url.searchParams.get('format') === 'workspace';
679
- const touch = !(method === "GET" && (edgeRoute?.domain === 'live' || workspaceRead || (fragmentName !== null && NO_TOUCH_FRAGMENTS.has(fragmentName)) || /^\/code\/[a-f0-9]{32}\/state$/.test(url.pathname)));
767
+ const touch = !(method === "GET" && (edgeRoute?.domain === 'live' || workspaceRead || (fragmentName !== null && NO_TOUCH_FRAGMENTS.has(fragmentName)) || /^\/code\/[a-f0-9]{32}\/state$/.test(url.pathname) || url.pathname === '/term/state'));
680
768
  const refuseBudget = (admitted) => {
681
769
  if (admitted.ok)
682
770
  return false;
@@ -702,8 +790,9 @@ export function createDecisionServer(options) {
702
790
  if (who?.via === "bearer" && refuseBudget(admitBearer(request, who)))
703
791
  return;
704
792
  const passwordProof = who?.via === 'cookie' ? { name: who.name, generation: who.session.generation } : null;
793
+ const fresh = freshFactsOf(request, who, matchedRoute?.freshness === 'allowlist');
705
794
  if (edgeRoute?.domain === 'people')
706
- return provenPasswordAccount.run(passwordProof, () => dispatchEdge(edgeRoute, { url, who, request, response, method }));
795
+ return freshFacts.run(fresh, () => provenPasswordAccount.run(passwordProof, () => dispatchEdge(edgeRoute, { url, who, request, response, method })));
707
796
  if (who === null) {
708
797
  // A password on a request that did not sign in is a machine's refusal, never a login page.
709
798
  if (isPasswordBearer(request.headers.authorization))
@@ -725,15 +814,18 @@ export function createDecisionServer(options) {
725
814
  appOrigins: [consoleOrigin(request.headers.host), publicOrigin?.origin ?? null, options.configDir === undefined ? null : loadConsoleUrl(process.env, options.configDir)],
726
815
  // Every signed-in page shares the workspace shell; pages showing a one-time secret opt out (forceSensitive).
727
816
  browser: who.via === 'cookie',
817
+ // Passwordless (read once for rendering; every act proves it again at the act): ceremony fields render as nothing.
818
+ passwordless: who.via === 'cookie' && passwordlessOn(store),
728
819
  // v100: signed in with the identity provider: its label, and whether it checked them recently enough to stand in for a password.
729
- sso: who.via === 'cookie' && who.session.sso !== undefined ? { label: ssoSettings()?.label ?? "your identity provider", fresh: Date.now() - who.session.sso.at < SSO_FRESH_MS } : undefined,
820
+ // Cockpit B2: "fresh" is an explicit re-check made on request and not yet spent (read live; one step-up uses it).
821
+ sso: who.via === 'cookie' && who.session.sso !== undefined ? { label: ssoSettings()?.label ?? "your identity provider",
822
+ get fresh() { return freshFacts.getStore() != null && ssoNowAvailable(who.name); } } : undefined,
730
823
  refusal: (answer, status, body) => sendScreen(answer, status, screen(status === 404 ? "Not found" : "Request refused", body, { chrome: chromeFor(who.via === "cookie" ? who.session.project : null, "work") })),
731
824
  workspaceRead,
732
825
  workspaceRequest: url.searchParams.get('request'),
733
826
  };
734
- // An empty password stands for a fresh identity-provider sign-in, for this person only (v100).
735
827
  if (method === "GET" || method === "POST")
736
- return freshIdentitySignIn.run({ actor: requestFacts.sso?.fresh === true ? who.name : null }, () => provenPasswordAccount.run(passwordProof, () => requestContext.run(requestFacts, async () => {
828
+ return freshFacts.run(fresh, () => provenPasswordAccount.run(passwordProof, () => requestContext.run(requestFacts, async () => {
737
829
  if (edgeRoute?.domain === 'live')
738
830
  return dispatchEdge(edgeRoute, { url, who, request, response, method });
739
831
  const body = method === "POST" ? await form(request, (matchedRoute?.stage === "console" ? matchedRoute.bodyCap : undefined) ?? BODY_CAP) : null;
@@ -855,7 +947,7 @@ export function createDecisionServer(options) {
855
947
  const ref = run === null ? null : store.refForId(run.taskRef);
856
948
  return { repo: ref?.repo ?? null, taskId: ref?.externalId ?? null, runId, action: `${resource[1] === "d" ? "decision" : resource[1] === "i" ? "incident" : "run"} ${resource[3] ?? "view"}` };
857
949
  }
858
- const known = new Set(["/recipes/prepare", "/recipes/preview", "/recipes/import", "/recipes/save", "/recipes/launch", "/tasks/add", "/queue/move", "/queue/note", "/people/invite", "/people/invite-revoke", "/people/revoke", "/people/projects", "/projects/select", "/projects/open", "/mode/confirm", "/mode/sign", "/mode/revoke", "/settings/chat-approval/save", "/settings/chat-approval/off"]);
950
+ const known = new Set(["/recipes/prepare", "/recipes/preview", "/recipes/import", "/recipes/save", "/recipes/launch", "/tasks/add", "/queue/move", "/queue/note", "/people/invite", "/people/invite-revoke", "/people/revoke", "/people/projects", "/projects/select", "/projects/open", "/mode/confirm", "/mode/sign", "/mode/revoke", "/settings/chat-approval/save", "/settings/chat-approval/off", "/settings/project/live-view"]);
859
951
  const placed = url.pathname === "/tasks/add" ? body?.get("repo")?.trim() : null;
860
952
  return { repo: url.pathname.startsWith("/people/") ? null : placed ? canonicalProject(placed) ?? placed : projectOf(who, request) ?? null, taskId: null, runId: null,
861
953
  action: known.has(url.pathname) ? url.pathname.slice(1).replaceAll("/", " ") : "console request" };
@@ -1044,9 +1136,15 @@ export function createDecisionServer(options) {
1044
1136
  const messages = store.listLeadMessages(opened.thread.id, 40);
1045
1137
  // The lead's question with buttons, while its reply is the last word in the thread.
1046
1138
  const last = messages.at(-1);
1047
- const ask = last?.role === "assistant" && last.turn !== null && store.getLeadTurn(last.turn)?.approver === who.name ? store.leadAskOpen(last.turn, now) : null;
1139
+ const asking = last?.role === "assistant" && last.turn !== null && store.getLeadTurn(last.turn)?.approver === who.name ? store.leadAskOpen(last.turn, now) : null;
1140
+ // A question about this task loses its buttons once the version shown moved after it was asked (its plan
1141
+ // rewritten or approved, a new attempt started): its options answered a state that is gone. Its words stay, marked so.
1142
+ const staleAsk = asking !== null && focusTask !== null && taskMovedSince(store, focusTask.executionId, asking.createdAt) ? asking.turn : null;
1143
+ const ask = staleAsk === null ? asking : null;
1048
1144
  // Answered questions stay readable above the answer; only their buttons go.
1049
1145
  const asks = new Map(messages.flatMap(one => { const asked = one.role === "assistant" && one.turn !== null ? store.leadAsk(one.turn) : null; return asked === null ? [] : [[asked.turn, asked]]; }));
1146
+ // The person's queued messages in this thread: each shows waiting (with Remove) or why it was not sent.
1147
+ const queue = new Map(personalQueue.queue.forThread(opened.thread.id).map(row => [row.message, row]));
1050
1148
  // The thread a ceiling change replaced (ruling 9): read after the open, display only.
1051
1149
  const replaced = store.replacedLeadThread(opened.thread);
1052
1150
  const previousProposals = replaced === null ? [] : store.listLeadProposals(replaced.id);
@@ -1054,7 +1152,9 @@ export function createDecisionServer(options) {
1054
1152
  previous: replaced === null ? null : { messages: store.listLeadMessages(replaced.id, 40), proposals: previousProposals, decisions: decisionsFor(store, previousProposals) },
1055
1153
  messages,
1056
1154
  ask,
1155
+ staleAsk,
1057
1156
  asks,
1157
+ queue,
1058
1158
  proposals,
1059
1159
  decisions: decisionsFor(store, [...proposals, ...coordinatorProposals]),
1060
1160
  coordinatorProposals,
@@ -1270,6 +1370,12 @@ export function createDecisionServer(options) {
1270
1370
  * are what sensitivity strips.
1271
1371
  */
1272
1372
  function sendScreen(response, status, s) {
1373
+ // Sensitivity is the page as authored (cockpit §2, R14): its own explicit word, else a password field it was written
1374
+ // with — judged before an identity-provider sign-in replaces that field, so an SSO step-up page stays sensitive.
1375
+ const authored = s.sensitive ?? (s.forceSensitive === true ||
1376
+ SENSITIVE_INPUT.test(htmlString(s.body)) ||
1377
+ (s.workspace?.pageHtml != null && SENSITIVE_INPUT.test(htmlString(s.workspace.pageHtml))) ||
1378
+ (s.chrome?.listPane !== undefined && SENSITIVE_INPUT.test(htmlString(s.chrome.listPane))));
1273
1379
  // v100: signed in with the identity provider, a step-up's password field is that sign-in instead.
1274
1380
  const ssoFacts = requestContext.getStore()?.sso;
1275
1381
  if (ssoFacts !== undefined) {
@@ -1279,9 +1385,7 @@ export function createDecisionServer(options) {
1279
1385
  s = { ...s, workspace: { ...s.workspace, view: { ...s.workspace.view, stepUp: { label: ssoFacts.label, fresh: ssoFacts.fresh, confirmHref: `/login/sso?reauth=1&return=${encodeURIComponent(back)}` } } } };
1280
1386
  }
1281
1387
  // D5: team chat (the central team service) is deprecated; /chat?team=1 still opens this release, but nothing links to it.
1282
- const sensitive = s.forceSensitive === true ||
1283
- SENSITIVE_INPUT.test(htmlString(s.body)) ||
1284
- (s.chrome?.listPane !== undefined && SENSITIVE_INPUT.test(htmlString(s.chrome.listPane)));
1388
+ const sensitive = authored || s.forceSensitive === true;
1285
1389
  const requestFacts = requestContext.getStore();
1286
1390
  if (requestFacts?.browser && s.chrome && !s.forceSensitive && (requestFacts.workspaceRead || browserAssetsAvailable())) {
1287
1391
  const path = new URL(requestFacts.returnTo, 'http://standing-orders.local');
@@ -1352,11 +1456,44 @@ export function createDecisionServer(options) {
1352
1456
  const request = requestFacts.workspaceRequest;
1353
1457
  const workspace = {
1354
1458
  version: 1, path: currentPath, title: s.title, user: requestFacts.actor ?? '', csrf: requestFacts.csrf, sensitive,
1459
+ ...(requestFacts.passwordless === true ? { passwordless: true } : {}),
1355
1460
  ...(requestFacts.actor ? { leadName: leadNameOf(store, requestFacts.actor) } : {}),
1356
1461
  refreshUrl: path.pathname + path.search,
1357
1462
  receipt: conversation !== null && request && REQUEST_TOKEN.test(request)
1358
- ? { request, received: store.leadRequestReceipt(conversation.sessionId, request) !== null } : null,
1463
+ ? { request, received: store.leadRequestReceived(conversation.sessionId, request) } : null,
1359
1464
  projects: browserProjectsOf(s.chrome.projects ?? []), ...crew,
1465
+ // The sidebar's project states: never on a sensitive page (no rail, no summary stream there).
1466
+ ...(() => { if (sensitive)
1467
+ return {}; try {
1468
+ return { projectStates: projectSummary() };
1469
+ }
1470
+ catch {
1471
+ return {};
1472
+ } })(),
1473
+ // Cockpit B2: the remembered password's chip, only where it could be used (a direct local request).
1474
+ ...(() => {
1475
+ const facts = freshFacts.getStore();
1476
+ if (sensitive || facts == null || !facts.local || !facts.live())
1477
+ return {};
1478
+ const at = facts.passwordAt();
1479
+ if (at === null || at <= facts.floor())
1480
+ return {};
1481
+ const left = FRESH_WINDOW_MS - (Date.now() - at);
1482
+ return left <= 0 ? {} : { fresh: { minutes: Math.max(1, Math.ceil(left / 60_000)), forget: '/session/forget-password' } };
1483
+ })(),
1484
+ // The usage chip: the Limits view's own audience (an operator on a browser session), never a sensitive page.
1485
+ ...(() => {
1486
+ const actor = requestFacts.actor;
1487
+ if (sensitive || !actor || requestFacts.csrf === '' || !store.isInstanceOperator(actor))
1488
+ return {};
1489
+ try {
1490
+ const usage = usageChipOf(store.providerLimits(), clock());
1491
+ return usage === null ? {} : { usage };
1492
+ }
1493
+ catch {
1494
+ return {};
1495
+ }
1496
+ })(),
1360
1497
  conversation, ...(extras.team ? { team: extras.team } : {}), focus: extras.focus ?? null, result: extras.result ?? null,
1361
1498
  catchUpHtml: extras.catchUpHtml === undefined ? '' : htmlString(extras.catchUpHtml), controlsHtml: extras.controlsHtml === undefined ? '' : htmlString(extras.controlsHtml), notices, view: extras.view ?? null,
1362
1499
  ...(s.chrome.demo ? { demo: { text: DEMO_BANNER, short: DEMO_BANNER_SHORT } } : {}),
@@ -1421,6 +1558,77 @@ export function createDecisionServer(options) {
1421
1558
  const bustBadge = () => {
1422
1559
  paletteCache.clear();
1423
1560
  };
1561
+ /** Enrolled projects first (most recently opened), then the repos this server was told to serve that nobody has
1562
+ * opened yet — the switcher lists every project it is allowed to show, each exactly once. */
1563
+ function sidebarProjects() {
1564
+ const seen = new Set();
1565
+ const rows = [];
1566
+ for (const one of [
1567
+ ...store.listProjects().map(one => ({ path: one.path, name: one.name })),
1568
+ ...managedRepos().map(path => ({ path, name: projectName(path) })),
1569
+ ]) {
1570
+ if (seen.has(one.path) || !visible(one.path) || goneRepos().has(one.path))
1571
+ continue;
1572
+ seen.add(one.path);
1573
+ rows.push(one);
1574
+ }
1575
+ return rows;
1576
+ }
1577
+ /** The sidebar's projects with their states (agent-state.ts), for the request's own reader: the page's first read
1578
+ * and the summary room's snapshots are this one function, so the two never disagree. Never marks anything seen.
1579
+ * Work with no project yet is its own entry when this reader may see it and something counts there. */
1580
+ function projectSummary() {
1581
+ const projects = sidebarProjects();
1582
+ const actor = requestContext.getStore()?.actor ?? '';
1583
+ const states = new Map(projectStates(store, workAccess(), actor, clock(), evidenceRoot, projects.map(one => one.path)).map(one => [one.repo, one]));
1584
+ // A row that needs a person or failed says what the Tasks list says it needs (its own next step); a row at work says
1585
+ // what its agent did last; a finished one says Done.
1586
+ const asking = [...states.values()].flatMap(one => one.families.slice(0, RAIL_TASKS)).filter(one => one.state === 'needs-you' || one.state === 'failed').map(one => one.rootId);
1587
+ const needWords = new Map();
1588
+ if (asking.length > 0) {
1589
+ try {
1590
+ // Every row the rail may list, in pages the index allows.
1591
+ for (let at = 0; at < asking.length; at += WORK_INDEX_MAX_LIMIT)
1592
+ for (const item of workIndexPage(store, clock(), workAccess(), { roots: asking.slice(at, at + WORK_INDEX_MAX_LIMIT), limit: WORK_INDEX_MAX_LIMIT, root: evidenceRoot }).items)
1593
+ needWords.set(item.rootId, item.status.detail !== '' ? item.status.detail : item.status.label);
1594
+ }
1595
+ catch { /* the rows keep their state's words */ }
1596
+ }
1597
+ // The chime's subjects: an opaque key per obligation and version, scoped to this reader (no words, no ids in it).
1598
+ const keyOf = (...parts) => createHash('sha256').update([actor, ...parts].join('\u0000')).digest('base64url').slice(0, 22);
1599
+ // Newest first, so a new ask always has a key however many older ones wait (those were heard when they were new).
1600
+ const asksOf = (state) => [
1601
+ ...(state?.families ?? []).flatMap(one => one.ask === undefined ? [] : [{ at: one.ask.at, ask: { key: keyOf('task', one.rootId, one.ask.version), task: one.rootId, run: one.ask.run, at: one.ask.at } }]),
1602
+ ...(state?.flowAsks ?? []).map(one => ({ at: one.at, ask: { key: keyOf('flow', one.card, one.stage, one.entry), task: null, run: null, at: one.at } })),
1603
+ ].sort((a, b) => b.at.localeCompare(a.at)).slice(0, ASKS_PER_PROJECT).map(one => one.ask);
1604
+ const railOf = (state) => {
1605
+ const families = state?.families ?? [];
1606
+ const tasks = families.slice(0, RAIL_TASKS).flatMap((one) => {
1607
+ const task = store.getTask(one.rootId);
1608
+ const line = one.state === 'needs-you' || one.state === 'failed' ? needWords.get(one.rootId) ?? null : one.line;
1609
+ return task === null ? [] : [{ id: one.rootId, title: task.title, href: `/t/${encodeURIComponent(one.rootId)}`, state: one.state, line: line ?? STATE_WORDS[one.state] }];
1610
+ });
1611
+ return { tasks, more: Math.max(0, families.length - RAIL_TASKS), asks: asksOf(state) };
1612
+ };
1613
+ const rows = projects.map(one => {
1614
+ const state = states.get(one.path);
1615
+ return { path: one.path, name: one.name, href: `/work?project=${encodeURIComponent(one.path)}`, state: state?.state ?? 'idle', counts: state?.counts ?? { ...NO_COUNTS }, ...railOf(state) };
1616
+ });
1617
+ const unplaced = states.get(null);
1618
+ if (unplaced !== undefined && unplaced.state !== 'idle')
1619
+ rows.push({ path: null, name: 'No project', href: '/work', state: unplaced.state, counts: unplaced.counts, ...railOf(unplaced) });
1620
+ return boundProjectSummary(rows);
1621
+ }
1622
+ /** What one account may see now, as the summary room keys it: a narrowed ceiling or grant is a different key, so a
1623
+ * snapshot read for the wider scope never reaches it. Null: no such account. */
1624
+ const projectScopeFor = (account) => store.accountOf(account) === null ? null : requestContext.run({ csrf: '', returnTo: '/', actor: account }, () => {
1625
+ // Canonical: what may be seen, never the order it was last opened in.
1626
+ const access = workAccess();
1627
+ return JSON.stringify({ projects: sidebarProjects().map(one => one.path).sort(), access: { ...access, repos: access.repos === null ? null : [...access.repos].sort() } });
1628
+ });
1629
+ /** One account's summary snapshot, read as that account for this exact scope (the summary room's fingerprint): null
1630
+ * when the account is gone or its scope moved. */
1631
+ const projectSummaryFor = (account, scope) => projectScopeFor(account) !== scope ? null : requestContext.run({ csrf: '', returnTo: '/', actor: account }, () => JSON.stringify(projectSummary()));
1424
1632
  /** The sidebar's facts for this request. */
1425
1633
  function chromeFor(project, active, listPane, scope) {
1426
1634
  if (restricted() && !visible(project))
@@ -1454,20 +1662,7 @@ export function createDecisionServer(options) {
1454
1662
  // Enrolled projects first (most recently opened), then the repos this
1455
1663
  // server was told to serve that nobody has opened yet — the switcher
1456
1664
  // lists every project it is allowed to show, each exactly once.
1457
- projects: (() => {
1458
- const seen = new Set();
1459
- const rows = [];
1460
- for (const one of [
1461
- ...store.listProjects().map(one => ({ path: one.path, name: one.name })),
1462
- ...managedRepos().map(path => ({ path, name: projectName(path) })),
1463
- ]) {
1464
- if (seen.has(one.path) || !visible(one.path) || goneRepos().has(one.path))
1465
- continue;
1466
- seen.add(one.path);
1467
- rows.push(one);
1468
- }
1469
- return rows;
1470
- })(),
1665
+ projects: sidebarProjects(),
1471
1666
  ...(facts === undefined ? {} : { csrf: facts.csrf, returnTo: facts.returnTo }),
1472
1667
  inboxCount: badge.count,
1473
1668
  inboxSaturated: badge.saturated,
@@ -1503,6 +1698,8 @@ export function createDecisionServer(options) {
1503
1698
  },
1504
1699
  }),
1505
1700
  ...(!restricted() && !unscopedMode && managedRepos().length > 0 ? { chat: true } : {}),
1701
+ // Live terminals: offered to an installation-wide operator (the routes prove it again).
1702
+ ...(actor !== undefined && !store.isDemo() && store.isInstanceOperator(actor) ? { code: true } : {}),
1506
1703
  ...(listPane === undefined ? {} : { listPane }),
1507
1704
  ...(scope === undefined ? {} : { scope }),
1508
1705
  };
@@ -1592,6 +1789,7 @@ export function createDecisionServer(options) {
1592
1789
  let leadClosing = false;
1593
1790
  server.once('listening', () => {
1594
1791
  team.start();
1792
+ personalQueue.start();
1595
1793
  leadMaintenance = startMaintenance({ intervalMs: 5_000, shouldStop: () => leadClosing,
1596
1794
  run: () => runLeadFollowPass({ store, repos: managedRepos, evidenceRoot, clock,
1597
1795
  provider: () => { const enabled = chatEnablement(); return enabled.ok ? { config: enabled.config, key: enabled.key } : null; },
@@ -1605,6 +1803,8 @@ export function createDecisionServer(options) {
1605
1803
  // it closes at once and never waits behind a model turn: a stop must release
1606
1804
  // the owner record well inside the service's exit window (Oct 2: deploys
1607
1805
  // over 0.9.11 found it still held by a killed process).
1806
+ /** Set once the terminal handlers exist (below): drops every socket and ends every program. */
1807
+ const terminalClosing = {};
1608
1808
  let codingClosing = null;
1609
1809
  const closeCoding = () => {
1610
1810
  leadClosing = true;
@@ -1626,7 +1826,9 @@ export function createDecisionServer(options) {
1626
1826
  liveHandlers.close();
1627
1827
  detachReads();
1628
1828
  liveFollower.close();
1629
- void Promise.all([closeCoding(), bounded(team.close()), bounded(leadMaintenance?.stop()), bounded(reads?.close())]).then(() => closeServer(callback)).catch(error => {
1829
+ void Promise.all([closeCoding(), terminalClosing.close?.().then(problem => { terminalClosing.problem = problem; }), bounded(team.close()), bounded(personalQueue.close()), bounded(leadMaintenance?.stop()), bounded(reads?.close())])
1830
+ // The server always closes; a terminal whose processes couldn't be proved stopped is the close's error.
1831
+ .then(() => closeServer(error => callback?.(error ?? terminalClosing.problem ?? undefined))).catch(error => {
1630
1832
  if (callback)
1631
1833
  callback(error instanceof Error ? error : Error('Coding session shutdown failed.'));
1632
1834
  else
@@ -1686,6 +1888,7 @@ export function createDecisionServer(options) {
1686
1888
  get chatScopeOf() { return chatScopeOf; },
1687
1889
  get liveTurns() { return liveTurns; },
1688
1890
  get liveTurnStarted() { return liveTurnStarted; },
1891
+ get personalQueue() { return personalQueue; },
1689
1892
  get leadConversationRows() { return leadConversationRows; },
1690
1893
  get demoLeadHere() { return demoLeadHere; },
1691
1894
  get teamBrowserReply() { return teamBrowserReply; },
@@ -1744,6 +1947,7 @@ export function createDecisionServer(options) {
1744
1947
  get signInLinks() { return signInLinks; },
1745
1948
  get linkKey() { return linkKey; },
1746
1949
  get fromThisComputer() { return fromThisComputer; },
1950
+ get freshFactsOf() { return freshFactsOf; },
1747
1951
  get passwordAllowed() { return passwordAllowed; },
1748
1952
  get arrival() { return arrival; },
1749
1953
  get cookieSecure() { return cookieSecure; },
@@ -1776,6 +1980,7 @@ export function createDecisionServer(options) {
1776
1980
  get revisionViewOf() { return revisionViewOf; },
1777
1981
  get revisionLedgerOf() { return revisionLedgerOf; },
1778
1982
  get firstTasks() { return firstTasks; },
1983
+ get authenticateCredential() { return authenticateCredential; },
1779
1984
  };
1780
1985
  const tasksHandlers = createTasksHandlers(handlerRuntime);
1781
1986
  const pagesHandlers = createPagesHandlers(handlerRuntime);
@@ -1785,6 +1990,7 @@ export function createDecisionServer(options) {
1785
1990
  const peopleHandlers = createPeopleHandlers(handlerRuntime);
1786
1991
  const remoteRuntime = {
1787
1992
  get identify() { return identify; },
1993
+ get freshFactsOf() { return freshFactsOf; },
1788
1994
  get store() { return store; },
1789
1995
  get admitPasswordSource() { return admitPasswordSource; },
1790
1996
  get admitBearer() { return admitBearer; },
@@ -1802,19 +2008,21 @@ export function createDecisionServer(options) {
1802
2008
  get managedRepos() { return managedRepos; },
1803
2009
  get joinSourceOf() { return joinSourceOf; },
1804
2010
  get ssoSettings() { return ssoSettings; },
1805
- get SSO_FRESH_MS() { return SSO_FRESH_MS; },
1806
2011
  get oauthTokenBudget() { return oauthTokenBudget; },
1807
2012
  get hookContext() { return hookContext; },
1808
2013
  get teamsSourceBudget() { return teamsSourceBudget; },
1809
2014
  get teamsTenantBudget() { return teamsTenantBudget; },
1810
2015
  };
1811
- const liveHandlers = createLiveHandlers(handlerRuntime, { bus: liveBus, workspaceRevision, chatProjectOf: chatHandlers.chatProjectOf });
2016
+ const liveHandlers = createLiveHandlers(handlerRuntime, { bus: liveBus, workspaceRevision, chatProjectOf: chatHandlers.chatProjectOf, projectSummaryFor, projectScopeFor });
1812
2017
  liveTurnStarted = liveHandlers.turnStarted;
2018
+ // The one upgrade this server takes is a terminal's socket (terminals.ts listens); anything else is dropped.
2019
+ const terminalHandlers = createTerminalHandlers(handlerRuntime, { ...terminals, bus: liveBus, allowedHost, publicOrigin, forwardedSourceOf, server });
2020
+ terminalClosing.close = terminalHandlers.close;
1813
2021
  const remoteHandlers = createRemoteHandlers(remoteRuntime);
1814
2022
  const registry = createHandlerRegistry([
1815
2023
  ...tasksHandlers.registrations, ...flowsHandlers.registrations, ...chatHandlers.registrations,
1816
2024
  ...settingsHandlers.registrations, ...peopleHandlers.registrations, ...pagesHandlers.registrations,
1817
- ...remoteHandlers.registrations, ...liveHandlers.registrations,
2025
+ ...remoteHandlers.registrations, ...liveHandlers.registrations, ...terminalHandlers.registrations, ...createSweepHandlers(handlerRuntime).registrations,
1818
2026
  ]);
1819
2027
  /** The row's role, after admission and before its one handler. */
1820
2028
  function dispatchConsole(route, ctx) {
@@ -15,7 +15,7 @@ export declare function createChatHandlers(runtime: ServerRuntime): {
15
15
  }, now: Date) => BrowserPhoneCard | undefined;
16
16
  chatProjectOf: (values: string[]) => string | null | undefined;
17
17
  beginLiveTurn: (thread: number) => (event: LeadProgress) => void;
18
- endLiveTurn: (thread: number, ok: boolean) => void;
18
+ endLiveTurn: (thread: number, ok: boolean, progress?: (event: LeadProgress) => void) => void;
19
19
  noteLead: (csrf: string, turn: number | null, message: string) => void;
20
20
  takeLeadNote: (csrf: string, liveSession: number | null) => string | null;
21
21
  demoResultView: (exchange: DemoExchange) => DemoResultView | null;