toolroll 0.9.63 → 0.9.65

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 (292) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +15 -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 +50 -2
  16. package/dist/agent-fence.js +157 -10
  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 +36 -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/browser/THIRD_PARTY_NOTICES.txt +48 -0
  32. package/dist/browser/terminal.css +37 -0
  33. package/dist/browser/terminal.js +117 -0
  34. package/dist/browser/workspace.css +2 -2
  35. package/dist/browser/workspace.js +42 -38
  36. package/dist/browser-shell.js +6 -3
  37. package/dist/browser-workspace.d.ts +235 -1
  38. package/dist/browser-workspace.js +60 -0
  39. package/dist/builder.d.ts +6 -1
  40. package/dist/builder.js +34 -13
  41. package/dist/chat-actions.d.ts +13 -1
  42. package/dist/chat-actions.js +197 -15
  43. package/dist/chat-ask.d.ts +4 -0
  44. package/dist/chat-ask.js +14 -0
  45. package/dist/chat-channel.js +20 -0
  46. package/dist/chat-continuity.js +5 -4
  47. package/dist/chat-decide.d.ts +0 -14
  48. package/dist/chat-decide.js +4 -1
  49. package/dist/chat-delivery.d.ts +9 -0
  50. package/dist/chat-delivery.js +45 -6
  51. package/dist/chat-messages.d.ts +8 -0
  52. package/dist/chat-messages.js +36 -4
  53. package/dist/check-levels-ui.js +2 -2
  54. package/dist/checkout-proof.d.ts +28 -0
  55. package/dist/checkout-proof.js +62 -0
  56. package/dist/claim.d.ts +12 -0
  57. package/dist/claim.js +19 -0
  58. package/dist/cli-help.d.ts +1 -1
  59. package/dist/cli-help.js +4 -1
  60. package/dist/cli.d.ts +3 -0
  61. package/dist/cli.js +21 -2
  62. package/dist/coding-ui.js +6 -4
  63. package/dist/console-instance.d.ts +124 -0
  64. package/dist/console-instance.js +337 -0
  65. package/dist/contracts/chat-actions.d.ts +16 -0
  66. package/dist/contracts/chat-actions.js +4 -0
  67. package/dist/contracts/chat-content.d.ts +19 -0
  68. package/dist/contracts/chat-content.js +6 -0
  69. package/dist/contracts/console-api.d.ts +12 -4
  70. package/dist/contracts/console-api.js +12 -4
  71. package/dist/contracts/flow-card.d.ts +4 -0
  72. package/dist/contracts/flow-card.js +3 -0
  73. package/dist/contracts/flow.d.ts +46 -0
  74. package/dist/contracts/flow.js +11 -2
  75. package/dist/contracts/lead-tools.d.ts +92 -0
  76. package/dist/contracts/lead-tools.js +13 -0
  77. package/dist/contracts/provider.d.ts +11 -1
  78. package/dist/contracts/provider.js +1 -1
  79. package/dist/contracts/route.d.ts +76 -1
  80. package/dist/contracts/route.js +1 -1
  81. package/dist/contracts/store-json.d.ts +2 -0
  82. package/dist/control-setup.d.ts +1 -1
  83. package/dist/control-ui.js +2 -2
  84. package/dist/converse.d.ts +16 -0
  85. package/dist/converse.js +32 -4
  86. package/dist/demo-chat.js +4 -4
  87. package/dist/demo.js +2 -2
  88. package/dist/desktop-update-gate.js +6 -0
  89. package/dist/desktop-update.d.ts +1 -1
  90. package/dist/discord-chat.js +4 -1
  91. package/dist/doctor.js +1 -1
  92. package/dist/egress-bridge.d.ts +17 -0
  93. package/dist/egress-bridge.js +111 -0
  94. package/dist/egress.d.ts +119 -0
  95. package/dist/egress.js +423 -0
  96. package/dist/exec.d.ts +78 -0
  97. package/dist/exec.js +367 -9
  98. package/dist/export-ui.js +2 -2
  99. package/dist/flow-engine.js +24 -5
  100. package/dist/flow-gallery-ui.js +2 -2
  101. package/dist/flow-gallery.js +27 -8
  102. package/dist/flow-share.js +16 -5
  103. package/dist/flow-triggers.d.ts +77 -8
  104. package/dist/flow-triggers.js +517 -70
  105. package/dist/flows-cli.js +8 -2
  106. package/dist/flows-ui.d.ts +0 -2
  107. package/dist/flows-ui.js +52 -4
  108. package/dist/flows.d.ts +4 -0
  109. package/dist/flows.js +9 -1
  110. package/dist/html.d.ts +9 -0
  111. package/dist/html.js +11 -0
  112. package/dist/invoke.d.ts +17 -0
  113. package/dist/invoke.js +217 -21
  114. package/dist/keys.d.ts +18 -14
  115. package/dist/keys.js +5 -0
  116. package/dist/lead-capabilities.js +3 -2
  117. package/dist/lead-contract.js +2 -0
  118. package/dist/lead-doors.d.ts +2 -0
  119. package/dist/lead-doors.js +7 -0
  120. package/dist/lead-follow.d.ts +2 -0
  121. package/dist/lead-follow.js +58 -2
  122. package/dist/lead-progress.js +1 -0
  123. package/dist/lead-queue-runtime.d.ts +46 -0
  124. package/dist/lead-queue-runtime.js +191 -0
  125. package/dist/lead-queue.d.ts +104 -0
  126. package/dist/lead-queue.js +187 -0
  127. package/dist/lead-sweep.d.ts +67 -0
  128. package/dist/lead-sweep.js +99 -0
  129. package/dist/lead-tools.d.ts +22 -0
  130. package/dist/lead-tools.js +152 -1
  131. package/dist/lead.d.ts +13 -5
  132. package/dist/lead.js +89 -29
  133. package/dist/limits-ui.d.ts +8 -1
  134. package/dist/limits-ui.js +26 -0
  135. package/dist/live-bus.d.ts +8 -0
  136. package/dist/live-bus.js +40 -4
  137. package/dist/live-detail.d.ts +27 -0
  138. package/dist/live-detail.js +57 -0
  139. package/dist/live-notes.d.ts +20 -0
  140. package/dist/live-notes.js +47 -0
  141. package/dist/live-records.d.ts +140 -0
  142. package/dist/live-records.js +935 -0
  143. package/dist/live-terminal.d.ts +258 -0
  144. package/dist/live-terminal.js +649 -0
  145. package/dist/live.d.ts +38 -27
  146. package/dist/live.js +142 -124
  147. package/dist/mcp-oauth.d.ts +6 -3
  148. package/dist/mcp-oauth.js +8 -4
  149. package/dist/memory-pass.js +2 -2
  150. package/dist/mobile-viewport.d.ts +5 -1
  151. package/dist/mobile-viewport.js +18 -2
  152. package/dist/model-catalog.js +4 -0
  153. package/dist/monitoring-ui.js +2 -2
  154. package/dist/needs-you.d.ts +1 -1
  155. package/dist/needs-you.js +11 -4
  156. package/dist/operate.d.ts +2 -0
  157. package/dist/operate.js +275 -17
  158. package/dist/passwordless.d.ts +23 -0
  159. package/dist/passwordless.js +26 -0
  160. package/dist/peek-cli.d.ts +12 -4
  161. package/dist/peek-cli.js +19 -11
  162. package/dist/phase-routing.js +10 -3
  163. package/dist/planner-source.d.ts +3 -0
  164. package/dist/planner-source.js +14 -0
  165. package/dist/planner.d.ts +2 -1
  166. package/dist/planner.js +7 -1
  167. package/dist/policy-ui.js +2 -2
  168. package/dist/policy.d.ts +1 -20
  169. package/dist/policy.js +29 -2
  170. package/dist/project-delete-ui.d.ts +12 -0
  171. package/dist/project-delete-ui.js +41 -9
  172. package/dist/project-delete.js +2 -2
  173. package/dist/project-memory.d.ts +3 -1
  174. package/dist/project-memory.js +57 -25
  175. package/dist/project-tools.d.ts +5 -3
  176. package/dist/project-tools.js +12 -9
  177. package/dist/provider-auth.d.ts +4 -3
  178. package/dist/provider-auth.js +5 -4
  179. package/dist/provider-connection.d.ts +2 -1
  180. package/dist/provider-connection.js +5 -0
  181. package/dist/provider.d.ts +25 -2
  182. package/dist/provider.js +63 -9
  183. package/dist/pull-request-ui.js +2 -2
  184. package/dist/push.js +1 -1
  185. package/dist/quick-start.d.ts +24 -0
  186. package/dist/quick-start.js +127 -0
  187. package/dist/release.d.ts +3 -0
  188. package/dist/release.js +14 -8
  189. package/dist/request-budget-ui.js +2 -2
  190. package/dist/result-review.d.ts +1 -1
  191. package/dist/result-review.js +9 -3
  192. package/dist/retention-ui.js +2 -2
  193. package/dist/retention.js +10 -1
  194. package/dist/run-efficiency.d.ts +73 -0
  195. package/dist/run-efficiency.js +228 -0
  196. package/dist/runner.d.ts +5 -1
  197. package/dist/runner.js +22 -1
  198. package/dist/scope.d.ts +170 -5
  199. package/dist/scope.js +251 -31
  200. package/dist/scout-net.d.ts +4 -1
  201. package/dist/scout-net.js +37 -12
  202. package/dist/scout.d.ts +20 -6
  203. package/dist/scout.js +27 -3
  204. package/dist/serve.js +247 -40
  205. package/dist/server/chat.d.ts +1 -1
  206. package/dist/server/chat.js +122 -31
  207. package/dist/server/chrome.d.ts +6 -1
  208. package/dist/server/chrome.js +18 -7
  209. package/dist/server/console.css +52 -38
  210. package/dist/server/flows.js +38 -8
  211. package/dist/server/guards.d.ts +1 -0
  212. package/dist/server/guards.js +23 -9
  213. package/dist/server/http.d.ts +7 -0
  214. package/dist/server/http.js +3 -2
  215. package/dist/server/live.d.ts +34 -1
  216. package/dist/server/live.js +39 -17
  217. package/dist/server/pages-assets.js +5 -0
  218. package/dist/server/pages.js +90 -13
  219. package/dist/server/people-tokens.d.ts +3 -0
  220. package/dist/server/people-tokens.js +55 -17
  221. package/dist/server/remote-hooks.js +12 -6
  222. package/dist/server/remote.d.ts +2 -1
  223. package/dist/server/remote.js +14 -5
  224. package/dist/server/render-chat.d.ts +12 -3
  225. package/dist/server/render-chat.js +47 -16
  226. package/dist/server/render-pages.d.ts +4 -1
  227. package/dist/server/render-pages.js +5 -4
  228. package/dist/server/render-people.d.ts +3 -2
  229. package/dist/server/render-people.js +3 -2
  230. package/dist/server/render-results.d.ts +18 -2
  231. package/dist/server/render-results.js +37 -28
  232. package/dist/server/render-settings.js +6 -5
  233. package/dist/server/render-tasks.d.ts +35 -10
  234. package/dist/server/render-tasks.js +110 -27
  235. package/dist/server/request-context.d.ts +2 -0
  236. package/dist/server/request-context.js +2 -1
  237. package/dist/server/route-table.d.ts +3 -0
  238. package/dist/server/route-table.js +46 -5
  239. package/dist/server/runtime.d.ts +33 -2
  240. package/dist/server/session.d.ts +25 -1
  241. package/dist/server/session.js +18 -4
  242. package/dist/server/settings.js +121 -10
  243. package/dist/server/sweeps.d.ts +4 -0
  244. package/dist/server/sweeps.js +58 -0
  245. package/dist/server/task-views.d.ts +2 -1
  246. package/dist/server/task-views.js +96 -6
  247. package/dist/server/tasks.js +320 -74
  248. package/dist/server/terminals.d.ts +31 -0
  249. package/dist/server/terminals.js +365 -0
  250. package/dist/setup-guide.js +3 -2
  251. package/dist/slack-chat.js +4 -1
  252. package/dist/spend-ui.js +3 -3
  253. package/dist/spend.js +2 -1
  254. package/dist/steer-capabilities.d.ts +32 -0
  255. package/dist/steer-capabilities.js +60 -0
  256. package/dist/steer-controls.d.ts +14 -0
  257. package/dist/steer-controls.js +36 -0
  258. package/dist/steer-status.js +2 -2
  259. package/dist/storage-ui.js +4 -4
  260. package/dist/store-baseline.d.ts +1 -1
  261. package/dist/store-baseline.js +1 -1
  262. package/dist/store.d.ts +430 -15
  263. package/dist/store.js +1041 -52
  264. package/dist/subagent-desk.js +1 -1
  265. package/dist/summary.js +3 -0
  266. package/dist/task-source.d.ts +58 -0
  267. package/dist/task-source.js +93 -0
  268. package/dist/team-leads.js +1 -1
  269. package/dist/teams-chat.js +4 -1
  270. package/dist/telegram-lead.d.ts +3 -0
  271. package/dist/telegram-lead.js +44 -7
  272. package/dist/telegram.d.ts +11 -2
  273. package/dist/telegram.js +51 -17
  274. package/dist/terminal-catalog.d.ts +11 -0
  275. package/dist/terminal-catalog.js +129 -0
  276. package/dist/text-limits.d.ts +2 -0
  277. package/dist/text-limits.js +2 -0
  278. package/dist/tool-broker.d.ts +2 -0
  279. package/dist/tool-broker.js +1 -0
  280. package/dist/toolroll-update-ui.js +2 -2
  281. package/dist/toolroll-update.js +13 -3
  282. package/dist/tools-ui.js +2 -2
  283. package/dist/webhook-limits.d.ts +23 -0
  284. package/dist/webhook-limits.js +30 -0
  285. package/dist/webhook-readers.d.ts +56 -0
  286. package/dist/webhook-readers.js +138 -0
  287. package/dist/webhook-signing.d.ts +27 -0
  288. package/dist/webhook-signing.js +119 -0
  289. package/dist/work-index.d.ts +29 -0
  290. package/dist/work-index.js +16 -2
  291. package/dist/workspace-revision.js +3 -1
  292. package/package.json +11 -2
package/dist/scope.d.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  export { ACCEPTANCE_LIMITS, EVIDENCE_KINDS, type EvidenceKind } from "./contracts/acceptance-terms.js";
2
2
  import { AsyncLocalStorage } from "node:async_hooks";
3
+ import { isAcpProfile, type AcpProviderId } from "./acp-agents.js";
4
+ export { isAcpProfile };
5
+ import { type NetworkProfile } from "./egress.js";
3
6
  import { type EvidenceKind } from "./contracts/acceptance-terms.js";
4
7
  import { type AcceptanceCriterion, type AcceptanceProblem, type ScopeTerms } from "./contracts/scope.js";
5
8
  import type { Store, Mutation } from "./store.js";
@@ -42,6 +45,13 @@ export declare const GEMINI_LIMITS: {
42
45
  readonly timeoutSeconds: 1200;
43
46
  readonly repairTimeoutSeconds: 300;
44
47
  };
48
+ /** An ACP agent runs under two clocks at once: the whole attempt, and silence (no line from the agent). */
49
+ export declare const ACP_LIMITS: {
50
+ readonly timeoutSeconds: 3600;
51
+ readonly idleTimeoutSeconds: 900;
52
+ readonly repairTimeoutSeconds: 900;
53
+ readonly repairIdleTimeoutSeconds: 300;
54
+ };
45
55
  export type ClaudeProfile = {
46
56
  provider: "claude";
47
57
  /** The exact model string the argv carries. Never empty, never "default". */
@@ -61,6 +71,9 @@ export type ClaudeProfile = {
61
71
  /** Exact model for repairs, or the stable literal "inherit" (= the
62
72
  * build model, which is itself exact). */
63
73
  repairModel: string;
74
+ /** What the build may reach (egress.ts), resolved when the scope was written; absent on approvals sealed before
75
+ * network policies, which run narrowed (egress.ts legacyNetwork). */
76
+ network?: NetworkProfile;
64
77
  };
65
78
  export type CodexShapedProfile = {
66
79
  provider: "codex" | "openrouter";
@@ -77,6 +90,7 @@ export type CodexShapedProfile = {
77
90
  timeoutKind?: "idle";
78
91
  repairTimeoutSeconds: number;
79
92
  repairModel: string;
93
+ network?: NetworkProfile;
80
94
  };
81
95
  export type GeminiProfile = {
82
96
  provider: "gemini";
@@ -95,8 +109,40 @@ export type GeminiProfile = {
95
109
  timeoutKind?: "idle";
96
110
  repairTimeoutSeconds: number;
97
111
  repairModel: string;
112
+ network?: NetworkProfile;
113
+ };
114
+ /**
115
+ * An agent over the Agent Client Protocol (acp-agents.ts, docs/plans/acp-adapter.md).
116
+ * Full access inside the OS fence is the only posture: nothing else could be
117
+ * made true (an agent may run commands without asking). There is no turn
118
+ * limit; both clocks bound every attempt, and a repair has its own pair.
119
+ */
120
+ export type AcpProfile = {
121
+ provider: AcpProviderId;
122
+ /** Exact, and one the agent offered when it was checked. */
123
+ model: string;
124
+ permission: "full";
125
+ maxTurns: "unsupported";
126
+ repairMaxTurns: "unsupported";
127
+ timeoutSeconds: number;
128
+ idleTimeoutSeconds: number;
129
+ repairTimeoutSeconds: number;
130
+ repairIdleTimeoutSeconds: number;
131
+ repairModel: string;
132
+ network?: NetworkProfile;
133
+ };
134
+ export type ExecutionProfile = ClaudeProfile | CodexShapedProfile | GeminiProfile | AcpProfile;
135
+ /** Whether a profile's clock is a no-progress window (an ACP profile has both clocks; its overall one is a deadline). */
136
+ export declare function idleClock(profile: ExecutionProfile): boolean;
137
+ /**
138
+ * The watchdogs an attempt runs under, from its sealed profile and the overall bound the caller resolved: an ACP
139
+ * profile gets both clocks at once (the deadline and its silence window — a repair its own pair); the others one,
140
+ * a no-progress window when the profile says idle, else a deadline.
141
+ */
142
+ export declare function watchdogsOf(profile: ExecutionProfile, timeoutMs: number, repair?: boolean): {
143
+ timeoutMs?: number;
144
+ idleTimeoutMs?: number;
98
145
  };
99
- export type ExecutionProfile = ClaudeProfile | CodexShapedProfile | GeminiProfile;
100
146
  /** The durable unattended permission choices exposed by the console. The
101
147
  * provider-specific argv is still what gets sealed into an approval; this
102
148
  * small cross-provider type is only the operator-facing policy. */
@@ -461,6 +507,11 @@ export type ApproveResult = {
461
507
  ok: false;
462
508
  reason: "policy";
463
509
  message: string;
510
+ }
511
+ /** Cockpit B2: a remembered password can't approve these terms (protected, escalated, or a quarantined filing). */
512
+ | {
513
+ ok: false;
514
+ reason: "password-now";
464
515
  };
465
516
  /**
466
517
  * Whether this name-and-token pair is a person the store knows. Shared by
@@ -494,12 +545,114 @@ export declare function authenticateAccount(store: Store, by: string, secret: st
494
545
  * approval, chat-action review, the console's own ceremonies). A typed
495
546
  * password is always checked, and nothing else stands in.
496
547
  */
497
- export declare const freshIdentitySignIn: AsyncLocalStorage<{
498
- actor: string | null;
548
+ /**
549
+ * PASSWORD FRESHNESS (docs/plans/agents-cockpit.md B2), and the one place an empty password field is ever honoured.
550
+ *
551
+ * - An identity-provider re-check made **on request** (a step-up's re-authentication, never an ordinary sign-in) stands
552
+ * in for one password typed now, for ONE step-up of any kind, within five minutes: it is consumed by the act it
553
+ * proves (`sso-now`). Never reused across acts.
554
+ * - A password verified on this session, or that explicit re-check, stands in for fifteen minutes — but only inside an
555
+ * allowlisted act (`withFreshness`), only for this person, only on a direct local request (`fresh-password`,
556
+ * `fresh-sso`). Never extended by use.
557
+ * Every fact is read at the moment of the act (`FreshFacts` are live readers, not a snapshot), so a sign-out, a session
558
+ * revoked or rotated, a credential change or Forget made while a request was in flight is seen by it.
559
+ */
560
+ export type FreshOp = "approve-standard" | "chat-send" | "reply-question";
561
+ /** "session": Passwordless is on and this is the person's own live browser session (docs/plans/passwordless.md). */
562
+ /** "remote-token" / "lead-token": a machine credential stood in for the person (a remote run's API token, a lead's
563
+ * token) — fine for ordinary acts it is allowed, never for a credential act (it would mint a way in with no password). */
564
+ export type AuthMethod = "password-now" | "sso-now" | "fresh-password" | "fresh-sso" | "session" | "cli-login" | "remote-token" | "lead-token";
565
+ /**
566
+ * The local CLI's remembered login, marked for the one command that read it (operate.ts credentialsFrom): with
567
+ * Passwordless on, a proof made with it records "cli-login" — never a typed password — and no credential act accepts it.
568
+ * Scoped to that command's async chain; a browser request in the console's own process never carries it.
569
+ */
570
+ export declare const rememberedLogin: AsyncLocalStorage<{
571
+ login: {
572
+ name: string;
573
+ token: string;
574
+ } | null;
499
575
  }>;
500
- export declare const hasFreshIdentitySignIn: (by: string) => boolean;
501
- export declare function authenticateApprover(store: Store, by: string, token: string, repo?: string | null): {
576
+ export declare const FRESH_WINDOW_MS: number;
577
+ /** How long an explicit provider re-check stands in for one password typed now. */
578
+ export declare const SSO_NOW_MS: number;
579
+ export type FreshFacts = {
580
+ actor: string;
581
+ /** Straight from this computer: a loopback peer naming a loopback host, with no forwarding headers of any kind. */
582
+ local: boolean;
583
+ /** The request's route is on the freshness allowlist (route-table.ts `freshness: "allowlist"`): the table decides. */
584
+ allowlisted: boolean;
585
+ /** The session is still signed in, under the account's current credential generation, not revoked. */
586
+ live: () => boolean;
587
+ /** When a password was last verified on this session; when the provider last re-checked on request (window start);
588
+ * and an unconsumed re-check for one step-up. */
589
+ passwordAt: () => number | null;
590
+ ssoReauthAt: () => number | null;
591
+ ssoNow: () => number | null;
592
+ /** Spend the re-check; the answer puts it back (a refund) when the act it proved doesn't happen. */
593
+ consumeSsoNow: () => () => void;
594
+ /** Nothing before this counts (Forget). */
595
+ floor: () => number;
596
+ /** Record a verified password entry on this session (the console decides whether this request may start one). */
597
+ stamp: (at: number) => void;
598
+ };
599
+ export declare const freshFacts: AsyncLocalStorage<FreshFacts | null>;
600
+ /**
601
+ * Run an act a re-check may prove, spending the re-check only if the act happens: when it throws (a rolled-back
602
+ * transaction) or `failed` says it was refused, every re-check it spent is put back. ONLY for acts whose every effect
603
+ * is in one database transaction (an approval, a chat approval proposal, a chat turn opening): there, "refused" and
604
+ * "thrown" prove nothing happened. Every other step-up spends its re-check whatever comes of it (fail-closed: at worst
605
+ * the person confirms with their provider again). Synchronous acts only.
606
+ */
607
+ export declare function spendingOnlyOnSuccess<T>(act: () => T, failed: (result: T) => boolean): T;
608
+ /** Run an allowlisted act: within it, a fresh password or re-check may stand in for one typed now. */
609
+ export declare function withFreshness<T>(op: FreshOp, act: () => T): T;
610
+ /** The freshness this act may stand on now, or null: only inside an allowlisted act on an allowlisted route, for this
611
+ * person, on a direct local request, on a live session, within the window, after any Forget. `preview` asks it from a
612
+ * page (a GET) about the allowlisted act its form posts to — for words only, never to authorise. */
613
+ export declare function freshnessFor(by: string, now?: number, preview?: boolean): {
614
+ method: "fresh-password" | "fresh-sso";
615
+ at: number;
616
+ } | null;
617
+ /** Whether an explicit provider re-check is waiting to stand in for one step-up (the page says "Confirmed with …"). */
618
+ export declare function ssoNowAvailable(by: string, now?: number): boolean;
619
+ /**
620
+ * Proof that this person is here now, for any account step-up (an approver's or not): a password typed now, or an
621
+ * explicit provider re-check (consumed). The core of every console step-up; `authenticateApprover` adds standing.
622
+ */
623
+ export declare function proveAccountNow(store: Store, by: string, token: string, options?: {
624
+ credential?: boolean;
625
+ }): {
626
+ ok: true;
627
+ method: AuthMethod;
628
+ at: number;
629
+ } | {
630
+ ok: false;
631
+ reason: "no-approvers" | "not-an-approver";
632
+ };
633
+ /**
634
+ * A credential act (docs/plans/passwordless.md, the closed list: one that creates or widens a way in): only a password
635
+ * typed now, or a one-use identity-provider re-check, proves it — never a session, never a remembered password.
636
+ */
637
+ /** Whether an empty password would pass for `by` right now because Passwordless is on and this is their live browser
638
+ * session. Read only — the act itself proves it again inside `proveAccountNow`. */
639
+ export declare function passwordlessSessionFor(store: Store, by: string): boolean;
640
+ /** Whether a proof is one a credential act accepts: a password typed now, or a one-use identity-provider re-check. */
641
+ export declare const credentialProof: (method: AuthMethod) => boolean;
642
+ export declare function proveCredential(store: Store, by: string, token: string, repo?: string | null): {
502
643
  ok: true;
644
+ method: AuthMethod;
645
+ at: number;
646
+ } | {
647
+ ok: false;
648
+ reason: "no-approvers" | "not-an-approver";
649
+ };
650
+ export declare function authenticateApprover(store: Store, by: string, token: string, repo?: string | null, options?: {
651
+ credential?: boolean;
652
+ }): {
653
+ ok: true;
654
+ method: AuthMethod;
655
+ at: number;
503
656
  } | {
504
657
  ok: false;
505
658
  reason: "no-approvers" | "not-an-approver";
@@ -548,6 +701,18 @@ export declare function fileAndSealUnderMode(store: Store, input: ScopeInput & {
548
701
  detail?: string;
549
702
  };
550
703
  export declare function approve(store: Store, taskId: string, by: string, now: Date, sawDigest: string, token: string, mutation?: Mutation): ApproveResult;
704
+ /** The ledger act that records how an approval's person was proved (password-now, sso-now, fresh-password, fresh-sso). */
705
+ export declare const APPROVAL_PROOF = "approval proved";
706
+ /** The proof row's detail: when the person was proved, the generation, and the exact digest they said yes to. */
707
+ export declare const approvalProofDetail: (at: number, generation: number, digest: string) => string;
708
+ /** The proof of THIS approval: the approver's own row for these exact bytes (never another approval's words). */
709
+ export declare function approvalProofOf(store: Store, taskId: string, by: string, digest: string): {
710
+ method: string;
711
+ at: string;
712
+ } | null;
713
+ /** Whether a remembered password may approve these terms: one approver (no protected work), standard permissions
714
+ * (never escalated), and not filed by a coordinator, a lead or a scout (those keep their own password ceremony). */
715
+ export declare function freshlyApprovable(store: Store, taskId: string, scope: Scope, singleApprover: boolean): boolean;
551
716
  /**
552
717
  * Whether this task may be built.
553
718
  *
package/dist/scope.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // The rubric's vocabulary loads before anything that reaches the plan contract (see contracts/acceptance-terms.ts).
2
2
  export { ACCEPTANCE_LIMITS, EVIDENCE_KINDS } from "./contracts/acceptance-terms.js";
3
+ import { passwordlessOn } from "./passwordless.js";
3
4
  import { currentPasswordSource, passwordGuardOf, provenPasswordAccount } from "./sign-in-guard.js";
4
5
  import { validateScopeText } from "./task-text.js";
5
6
  import { contractChangesOf } from "./contract-changes.js";
@@ -8,6 +9,9 @@ import { projectAuthority } from "./project-access.js";
8
9
  import { activeRemote } from "./remote-run.js";
9
10
  import { claimActor, currentActor } from "./actor.js";
10
11
  import { parseLeadToken } from "./api-tokens.js";
12
+ import { isAcpProfile, isAcpProvider } from "./acp-agents.js";
13
+ import { levelOfProfile } from "./policy.js";
14
+ export { isAcpProfile };
11
15
  /**
12
16
  * What a task is allowed to become, agreed before anything builds it.
13
17
  *
@@ -34,6 +38,7 @@ import { parseLeadToken } from "./api-tokens.js";
34
38
  * glance is the whole safety property.
35
39
  */
36
40
  import { createHash, randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
41
+ import { networkFromValue, networkWords } from "./egress.js";
37
42
  import { z } from "zod";
38
43
  import {} from "./contracts/acceptance-terms.js";
39
44
  import { parseContract } from "./contracts/contract.js";
@@ -105,6 +110,27 @@ export const GEMINI_LIMITS = {
105
110
  timeoutSeconds: 1200,
106
111
  repairTimeoutSeconds: 300,
107
112
  };
113
+ /** An ACP agent runs under two clocks at once: the whole attempt, and silence (no line from the agent). */
114
+ export const ACP_LIMITS = {
115
+ timeoutSeconds: 3600,
116
+ idleTimeoutSeconds: 900,
117
+ repairTimeoutSeconds: 900,
118
+ repairIdleTimeoutSeconds: 300,
119
+ };
120
+ /** Whether a profile's clock is a no-progress window (an ACP profile has both clocks; its overall one is a deadline). */
121
+ export function idleClock(profile) {
122
+ return !isAcpProfile(profile) && profile.timeoutKind === "idle";
123
+ }
124
+ /**
125
+ * The watchdogs an attempt runs under, from its sealed profile and the overall bound the caller resolved: an ACP
126
+ * profile gets both clocks at once (the deadline and its silence window — a repair its own pair); the others one,
127
+ * a no-progress window when the profile says idle, else a deadline.
128
+ */
129
+ export function watchdogsOf(profile, timeoutMs, repair = false) {
130
+ if (isAcpProfile(profile))
131
+ return { timeoutMs, idleTimeoutMs: (repair ? profile.repairIdleTimeoutSeconds : profile.idleTimeoutSeconds) * 1000 };
132
+ return profile.timeoutKind === "idle" ? { idleTimeoutMs: timeoutMs } : { timeoutMs };
133
+ }
108
134
  export const PROFILE_DIGEST_VERSION = 2;
109
135
  /** Deterministic JSON: object keys sorted recursively, arrays in order. */
110
136
  function canonicalJson(value) {
@@ -332,8 +358,13 @@ export function profileFromJson(json) {
332
358
  const num = (v) => safePositiveInteger(v);
333
359
  const clock = (v) => timerSafeSeconds(v);
334
360
  const PROVIDER_KEYS = ["provider", "model", "maxTurns", "repairMaxTurns", "timeoutSeconds", "repairTimeoutSeconds", "repairModel"];
361
+ // The network a scope was written with: present and exact, or absent (an approval from before network policies).
362
+ const network = p["network"] === undefined ? undefined : networkFromValue(p["network"]);
363
+ if (network === null)
364
+ return null;
365
+ const withNetwork = network === undefined ? {} : { network };
335
366
  if (p["provider"] === "claude") {
336
- if (exactKeys(p, [...PROVIDER_KEYS, "permissionArgv"], ["timeoutKind"]) &&
367
+ if (exactKeys(p, [...PROVIDER_KEYS, "permissionArgv"], ["timeoutKind", "network"]) &&
337
368
  str(p["model"]) &&
338
369
  (p["permissionArgv"] === "auto" || p["permissionArgv"] === "acceptEdits" || p["permissionArgv"] === "bypassPermissions") &&
339
370
  num(p["maxTurns"]) && num(p["repairMaxTurns"]) &&
@@ -349,12 +380,13 @@ export function profileFromJson(json) {
349
380
  ...(p["timeoutKind"] === "idle" ? { timeoutKind: "idle" } : {}),
350
381
  repairTimeoutSeconds: p["repairTimeoutSeconds"],
351
382
  repairModel: p["repairModel"],
383
+ ...withNetwork,
352
384
  };
353
385
  }
354
386
  return null;
355
387
  }
356
388
  if (p["provider"] === "codex" || p["provider"] === "openrouter") {
357
- if (exactKeys(p, [...PROVIDER_KEYS, "sandboxMode"], ["timeoutKind"]) &&
389
+ if (exactKeys(p, [...PROVIDER_KEYS, "sandboxMode"], ["timeoutKind", "network"]) &&
358
390
  str(p["model"]) &&
359
391
  (p["sandboxMode"] === "workspace-write" || p["sandboxMode"] === "danger-full-access") &&
360
392
  p["maxTurns"] === "unsupported" && p["repairMaxTurns"] === "unsupported" &&
@@ -370,12 +402,13 @@ export function profileFromJson(json) {
370
402
  ...(p["timeoutKind"] === "idle" ? { timeoutKind: "idle" } : {}),
371
403
  repairTimeoutSeconds: p["repairTimeoutSeconds"],
372
404
  repairModel: p["repairModel"],
405
+ ...withNetwork,
373
406
  };
374
407
  }
375
408
  return null;
376
409
  }
377
410
  if (p["provider"] === "gemini") {
378
- if (exactKeys(p, [...PROVIDER_KEYS, "approvalArgv"], ["timeoutKind"]) &&
411
+ if (exactKeys(p, [...PROVIDER_KEYS, "approvalArgv"], ["timeoutKind", "network"]) &&
379
412
  str(p["model"]) &&
380
413
  (p["approvalArgv"] === "auto_edit" || p["approvalArgv"] === "yolo") &&
381
414
  p["maxTurns"] === "unsupported" && p["repairMaxTurns"] === "unsupported" &&
@@ -391,6 +424,30 @@ export function profileFromJson(json) {
391
424
  ...(p["timeoutKind"] === "idle" ? { timeoutKind: "idle" } : {}),
392
425
  repairTimeoutSeconds: p["repairTimeoutSeconds"],
393
426
  repairModel: p["repairModel"],
427
+ ...withNetwork,
428
+ };
429
+ }
430
+ return null;
431
+ }
432
+ if (typeof p["provider"] === "string" && isAcpProvider(p["provider"])) {
433
+ if (exactKeys(p, [...PROVIDER_KEYS, "permission", "idleTimeoutSeconds", "repairIdleTimeoutSeconds"], ["network"]) &&
434
+ str(p["model"]) &&
435
+ p["permission"] === "full" &&
436
+ p["maxTurns"] === "unsupported" && p["repairMaxTurns"] === "unsupported" &&
437
+ clock(p["timeoutSeconds"]) && clock(p["idleTimeoutSeconds"]) && clock(p["repairTimeoutSeconds"]) && clock(p["repairIdleTimeoutSeconds"]) &&
438
+ repairRef(p["repairModel"])) {
439
+ return {
440
+ provider: p["provider"],
441
+ model: p["model"],
442
+ permission: "full",
443
+ maxTurns: "unsupported",
444
+ repairMaxTurns: "unsupported",
445
+ timeoutSeconds: p["timeoutSeconds"],
446
+ idleTimeoutSeconds: p["idleTimeoutSeconds"],
447
+ repairTimeoutSeconds: p["repairTimeoutSeconds"],
448
+ repairIdleTimeoutSeconds: p["repairIdleTimeoutSeconds"],
449
+ repairModel: p["repairModel"],
450
+ ...withNetwork,
394
451
  };
395
452
  }
396
453
  return null;
@@ -678,7 +735,9 @@ password) {
678
735
  // 1, finding 1): a viewer's or a revoked person's credential is real
679
736
  // and still vouches for nothing — the CLI is a ceremony site like any
680
737
  // other, and this was the one road that forgot.
681
- const vouching = by === undefined ? { ok: false } : authenticateApprover(store, by.name, by.token);
738
+ // Creating an approver is a credential act (docs/plans/passwordless.md): a password typed now, never a session or
739
+ // the CLI's remembered login under Passwordless.
740
+ const vouching = by === undefined ? { ok: false } : proveCredential(store, by.name, by.token);
682
741
  if (!vouching.ok) {
683
742
  return { ok: false, reason: "not-an-approver" };
684
743
  }
@@ -754,24 +813,129 @@ export function authenticateAccount(store, by, secret) {
754
813
  return { ok: true, role: account.role, generation: account.generation };
755
814
  }
756
815
  /**
757
- * v100: someone who signed in with the identity provider moments ago has no
758
- * password to type again. The console marks such a request with this, for
759
- * that person only, and an EMPTY password then stands for the fresh sign-in
760
- * at every step-up that reaches `authenticateApprover` (scope
761
- * approval, chat-action review, the console's own ceremonies). A typed
762
- * password is always checked, and nothing else stands in.
816
+ * The local CLI's remembered login, marked for the one command that read it (operate.ts credentialsFrom): with
817
+ * Passwordless on, a proof made with it records "cli-login" — never a typed password — and no credential act accepts it.
818
+ * Scoped to that command's async chain; a browser request in the console's own process never carries it.
819
+ */
820
+ export const rememberedLogin = new AsyncLocalStorage();
821
+ export const FRESH_WINDOW_MS = 15 * 60_000;
822
+ /** How long an explicit provider re-check stands in for one password typed now. */
823
+ export const SSO_NOW_MS = 5 * 60_000;
824
+ export const freshFacts = new AsyncLocalStorage();
825
+ const refunds = new AsyncLocalStorage();
826
+ /**
827
+ * Run an act a re-check may prove, spending the re-check only if the act happens: when it throws (a rolled-back
828
+ * transaction) or `failed` says it was refused, every re-check it spent is put back. ONLY for acts whose every effect
829
+ * is in one database transaction (an approval, a chat approval proposal, a chat turn opening): there, "refused" and
830
+ * "thrown" prove nothing happened. Every other step-up spends its re-check whatever comes of it (fail-closed: at worst
831
+ * the person confirms with their provider again). Synchronous acts only.
832
+ */
833
+ export function spendingOnlyOnSuccess(act, failed) {
834
+ const spent = [];
835
+ let result;
836
+ try {
837
+ result = refunds.run(spent, act);
838
+ }
839
+ catch (error) {
840
+ for (const refund of spent)
841
+ refund();
842
+ throw error;
843
+ }
844
+ if (failed(result))
845
+ for (const refund of spent)
846
+ refund();
847
+ // Succeeded here, but an enclosing act (an outer transaction, the request itself) may still fail: it owns them now.
848
+ else
849
+ refunds.getStore()?.push(...spent);
850
+ return result;
851
+ }
852
+ const freshScope = new AsyncLocalStorage();
853
+ /** Run an allowlisted act: within it, a fresh password or re-check may stand in for one typed now. */
854
+ export function withFreshness(op, act) { return freshScope.run(op, act); }
855
+ const within = (at, floor, spanMs, now) => at !== null && at > 0 && at > floor && at <= now + 60_000 && now - at < spanMs;
856
+ /** The freshness this act may stand on now, or null: only inside an allowlisted act on an allowlisted route, for this
857
+ * person, on a direct local request, on a live session, within the window, after any Forget. `preview` asks it from a
858
+ * page (a GET) about the allowlisted act its form posts to — for words only, never to authorise. */
859
+ export function freshnessFor(by, now = Date.now(), preview = false) {
860
+ const op = freshScope.getStore(), facts = freshFacts.getStore();
861
+ if (op === undefined || facts == null || facts.actor !== by || !(facts.allowlisted || preview) || !facts.local || !facts.live())
862
+ return null;
863
+ const floor = facts.floor(), password = facts.passwordAt(), sso = facts.ssoReauthAt();
864
+ if (within(password, floor, FRESH_WINDOW_MS, now))
865
+ return { method: "fresh-password", at: password };
866
+ if (within(sso, floor, FRESH_WINDOW_MS, now))
867
+ return { method: "fresh-sso", at: sso };
868
+ return null;
869
+ }
870
+ /** Whether an explicit provider re-check is waiting to stand in for one step-up (the page says "Confirmed with …"). */
871
+ export function ssoNowAvailable(by, now = Date.now()) {
872
+ const facts = freshFacts.getStore();
873
+ return facts != null && facts.actor === by && facts.live() && within(facts.ssoNow(), facts.floor(), SSO_NOW_MS, now);
874
+ }
875
+ /**
876
+ * Proof that this person is here now, for any account step-up (an approver's or not): a password typed now, or an
877
+ * explicit provider re-check (consumed). The core of every console step-up; `authenticateApprover` adds standing.
763
878
  */
764
- export const freshIdentitySignIn = new AsyncLocalStorage();
765
- export const hasFreshIdentitySignIn = (by) => freshIdentitySignIn.getStore()?.actor === by;
766
- export function authenticateApprover(store, by, token, repo) {
879
+ export function proveAccountNow(store, by, token, options = {}) {
880
+ const now = Date.now();
881
+ const facts = freshFacts.getStore();
882
+ if (facts != null && facts.actor === by && !facts.live())
883
+ return { ok: false, reason: "not-an-approver" };
767
884
  // An empty password is never checked as a password (and never counts toward a lockout).
768
- if (token === "")
769
- return hasFreshIdentitySignIn(by) ? approverStanding(store, by, repo) : { ok: false, reason: "not-an-approver" };
885
+ if (token === "") {
886
+ // Passwordless (read live): the person's own live browser session is the proof. Only a browser request carries facts;
887
+ // a CLI, a token, a chat bridge or detached work has none, and falls through to the rules below.
888
+ // A credential act never takes this road, so its own proofs (a one-use provider re-check) are reached and spent.
889
+ if (options.credential !== true && facts != null && facts.actor === by && facts.live() && passwordlessOn(store))
890
+ return { ok: true, method: "session", at: now };
891
+ if (ssoNowAvailable(by, now)) {
892
+ const at = facts.ssoNow();
893
+ const refund = facts.consumeSsoNow();
894
+ refunds.getStore()?.push(refund);
895
+ return { ok: true, method: "sso-now", at };
896
+ }
897
+ const fresh = freshnessFor(by, now);
898
+ return fresh === null ? { ok: false, reason: "not-an-approver" } : { ok: true, method: fresh.method, at: fresh.at };
899
+ }
770
900
  const account = authenticateAccount(store, by, token);
771
- if (!account.ok) {
901
+ if (!account.ok)
772
902
  return { ok: false, reason: account.reason === "no-approvers" ? "no-approvers" : "not-an-approver" };
773
- }
774
- return approverStanding(store, by, repo);
903
+ // A machine credential named the person (authenticateAccount takes it as them): say so, never "a password typed now".
904
+ if (activeRemote() !== null)
905
+ return { ok: true, method: "remote-token", at: now };
906
+ if (currentActor()?.lead === true && parseLeadToken(token) !== null)
907
+ return { ok: true, method: "lead-token", at: now };
908
+ const remembered = rememberedLogin.getStore()?.login;
909
+ if (remembered != null && remembered.name === by && remembered.token === token && passwordlessOn(store))
910
+ return { ok: true, method: "cli-login", at: now };
911
+ if (facts != null && facts.actor === by)
912
+ facts.stamp(now);
913
+ return { ok: true, method: "password-now", at: now };
914
+ }
915
+ /**
916
+ * A credential act (docs/plans/passwordless.md, the closed list: one that creates or widens a way in): only a password
917
+ * typed now, or a one-use identity-provider re-check, proves it — never a session, never a remembered password.
918
+ */
919
+ /** Whether an empty password would pass for `by` right now because Passwordless is on and this is their live browser
920
+ * session. Read only — the act itself proves it again inside `proveAccountNow`. */
921
+ export function passwordlessSessionFor(store, by) {
922
+ const facts = freshFacts.getStore();
923
+ return facts != null && facts.actor === by && facts.live() && passwordlessOn(store);
924
+ }
925
+ /** Whether a proof is one a credential act accepts: a password typed now, or a one-use identity-provider re-check. */
926
+ export const credentialProof = (method) => method === "password-now" || method === "sso-now";
927
+ export function proveCredential(store, by, token, repo) {
928
+ const proved = authenticateApprover(store, by, token, repo, { credential: true });
929
+ if (!proved.ok)
930
+ return proved;
931
+ return credentialProof(proved.method) ? proved : { ok: false, reason: "not-an-approver" };
932
+ }
933
+ export function authenticateApprover(store, by, token, repo, options = {}) {
934
+ const proved = proveAccountNow(store, by, token, options);
935
+ if (!proved.ok)
936
+ return proved;
937
+ const standing = approverStanding(store, by, repo);
938
+ return standing.ok ? proved : standing;
775
939
  }
776
940
  /**
777
941
  * What an approval needs besides the password (v100: an identity-provider
@@ -910,25 +1074,79 @@ export function approve(store, taskId, by, now, sawDigest, token, mutation = {})
910
1074
  const gate = store.approvalGate(taskId, by, "person");
911
1075
  if (gate.verdict === "refuse")
912
1076
  return { ok: false, reason: gate.reason };
1077
+ // A remembered password (cockpit B2) approves only everyday work: one approver, standard permissions, terms a
1078
+ // person filed. Protected work, escalated terms and quarantined filings take a password typed now.
1079
+ if ((authenticated.method === "fresh-password" || authenticated.method === "fresh-sso") && !freshlyApprovable(store, taskId, scope, gate.verdict === "seal" && !gate.protectedWork)) {
1080
+ return { ok: false, reason: "password-now" };
1081
+ }
1082
+ // How the person was proved, beside every yes they give (a vote kept for protected work, or the seal): a password
1083
+ // typed now, a re-check now, or a remembered one — with when, the credential generation and the exact digest.
1084
+ const proved = () => store.recordAction({ at: now.toISOString(), actor: by, repo: store.lookupRef(taskId)?.repo ?? null, taskId, runId: null, action: APPROVAL_PROOF,
1085
+ outcome: authenticated.method, source: "work", detail: approvalProofDetail(authenticated.at, store.accountOf(by)?.generation ?? 0, scope.digest) });
913
1086
  if (gate.verdict === "vote") {
1087
+ // The same person again adds nothing: no new vote, no proof row (and the approval roads refund a re-check).
1088
+ if (gate.already)
1089
+ return { ok: false, reason: "second-approver", have: gate.have, need: gate.need, already: true };
914
1090
  store.recordApprovalVote(taskId, scope.digest, by, now);
1091
+ proved();
915
1092
  return { ok: false, reason: "second-approver", have: gate.have, need: gate.need, already: gate.already };
916
1093
  }
917
- if (gate.protectedWork)
918
- store.recordApprovalVote(taskId, scope.digest, by, now);
919
- // SEAL, never re-resolve: the approval snapshots the stored working
920
- // profile — the exact bytes the digest the approver signed was bound
921
- // to. Routing saveScope here would re-run resolution and could sign a
922
- // profile nobody saw.
923
- const sealed = store.sealScopeApproval(taskId, by, now, mutation);
924
- if (!sealed)
925
- return { ok: false, reason: "changed" };
926
- const approved = store.getScope(taskId);
927
- if (approved === null)
928
- return { ok: false, reason: "no-scope" };
929
- return { ok: true, scope: approved };
1094
+ // The vote, the seal and its proof stand or fall together: a seal that is refused takes this yes back with it
1095
+ // (savepoint rolled back), so a refusal leaves nothing written — the approval roads may then refund a re-check.
1096
+ const refused = {};
1097
+ try {
1098
+ return store.savepoint(() => {
1099
+ if (gate.protectedWork)
1100
+ store.recordApprovalVote(taskId, scope.digest, by, now);
1101
+ // SEAL, never re-resolve: the approval snapshots the stored working
1102
+ // profile — the exact bytes the digest the approver signed was bound
1103
+ // to. Routing saveScope here would re-run resolution and could sign a
1104
+ // profile nobody saw.
1105
+ const sealed = store.sealScopeApproval(taskId, by, now, mutation);
1106
+ const approved = sealed ? store.getScope(taskId) : null;
1107
+ if (!sealed || approved === null) {
1108
+ refused.result = { ok: false, reason: sealed ? "no-scope" : "changed" };
1109
+ throw refused;
1110
+ }
1111
+ proved();
1112
+ return { ok: true, scope: approved };
1113
+ });
1114
+ }
1115
+ catch (error) {
1116
+ if (error === refused && refused.result !== undefined)
1117
+ return refused.result;
1118
+ throw error;
1119
+ }
930
1120
  });
931
1121
  }
1122
+ /** The ledger act that records how an approval's person was proved (password-now, sso-now, fresh-password, fresh-sso). */
1123
+ export const APPROVAL_PROOF = "approval proved";
1124
+ /** The proof row's detail: when the person was proved, the generation, and the exact digest they said yes to. */
1125
+ export const approvalProofDetail = (at, generation, digest) => `${new Date(at).toISOString()} · generation ${generation} · digest ${digest}`;
1126
+ /** The proof of THIS approval: the approver's own row for these exact bytes (never another approval's words). */
1127
+ export function approvalProofOf(store, taskId, by, digest) {
1128
+ const rows = store.handle.prepare("SELECT outcome, detail FROM action_ledger WHERE task_id = ? AND action = ? AND actor = ? ORDER BY id DESC LIMIT 50").all(taskId, APPROVAL_PROOF, by);
1129
+ for (const row of rows) {
1130
+ const parts = typeof row["detail"] === "string" ? row["detail"].split(" · ") : [];
1131
+ if (parts.length !== 3 || parts[2] !== `digest ${digest}` || Number.isNaN(Date.parse(parts[0])))
1132
+ continue;
1133
+ return { method: String(row["outcome"]), at: parts[0] };
1134
+ }
1135
+ return null;
1136
+ }
1137
+ /** Whether a remembered password may approve these terms: one approver (no protected work), standard permissions
1138
+ * (never escalated), and not filed by a coordinator, a lead or a scout (those keep their own password ceremony). */
1139
+ export function freshlyApprovable(store, taskId, scope, singleApprover) {
1140
+ if (!singleApprover)
1141
+ return false;
1142
+ if (scope.profile == null || levelOfProfile(scope.profile) === "escalated")
1143
+ return false;
1144
+ const ref = store.handle.prepare("SELECT coordinator_cid FROM task_ref WHERE backend = 'built-in' AND external_id = ?").get(taskId);
1145
+ if (ref === undefined || ref["coordinator_cid"] != null)
1146
+ return false;
1147
+ const via = store.handle.prepare("SELECT proposed_via FROM task_scope WHERE task_id = ?").get(taskId)?.["proposed_via"];
1148
+ return !(via === "mate" || via === "coordinator" || via === "scout");
1149
+ }
932
1150
  /**
933
1151
  * Whether this task may be built.
934
1152
  *
@@ -1058,6 +1276,8 @@ export function describeScope(scope, readiness = NO_READINESS) {
1058
1276
  // The ROUTE, in the words the yes agrees to (v47): posture and every
1059
1277
  // leg with its reason and readiness — before anyone signs.
1060
1278
  ...scopeRouteWords(scope, readiness),
1279
+ // What the build may reach (egress.ts), part of what the yes agrees to.
1280
+ ...(scope.profile === null || scope.profile === undefined ? [] : [` network ${networkWords(scope.profile.network ?? null).replace(/^Network: /, "")}`]),
1061
1281
  ` reference ${scope.digest}`,
1062
1282
  ` approved ${describeApproval(approval)}`,
1063
1283
  ];
@@ -1,5 +1,8 @@
1
1
  import { type Socket } from "node:net";
2
- /** Whether an IP literal is a public internet address. IPv4 inside IPv6 (mapped, compatible or NAT64) is judged as IPv4. */
2
+ /**
3
+ * Whether an IP literal is a public internet address. IPv4 inside IPv6 (mapped, translated, compatible or NAT64) is
4
+ * judged as IPv4, read from the address's groups so every way of writing it is caught (an expanded NAT64 form too).
5
+ */
3
6
  export declare function isPublicAddress(address: string): boolean;
4
7
  export type Lookup = (host: string) => Promise<readonly string[]>;
5
8
  /**