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.
- package/CHANGELOG.md +108 -0
- package/README.md +33 -5
- package/dist/acp-agents.d.ts +43 -0
- package/dist/acp-agents.js +62 -0
- package/dist/acp-attest.d.ts +44 -0
- package/dist/acp-attest.js +269 -0
- package/dist/acp-check.d.ts +28 -0
- package/dist/acp-check.js +410 -0
- package/dist/acp-client.d.ts +163 -0
- package/dist/acp-client.js +738 -0
- package/dist/acp-inspect.d.ts +23 -0
- package/dist/acp-inspect.js +109 -0
- package/dist/agent-board.d.ts +19 -6
- package/dist/agent-board.js +30 -9
- package/dist/agent-fence.d.ts +117 -2
- package/dist/agent-fence.js +439 -12
- package/dist/agent-pulse.js +9 -2
- package/dist/agent-state-words.d.ts +19 -0
- package/dist/agent-state-words.js +20 -0
- package/dist/agent-state.d.ts +65 -0
- package/dist/agent-state.js +229 -0
- package/dist/agentconfig.js +39 -13
- package/dist/agents-board.d.ts +34 -2
- package/dist/agents-board.js +110 -1
- package/dist/approval-rules-ui.js +2 -2
- package/dist/assignment-delivery.d.ts +14 -0
- package/dist/assignment-delivery.js +13 -0
- package/dist/assignment.js +13 -0
- package/dist/attest.js +7 -0
- package/dist/backup-ui.js +2 -2
- package/dist/batch-checks.js +3 -2
- package/dist/browser/THIRD_PARTY_NOTICES.txt +94 -0
- package/dist/browser/terminal.css +37 -0
- package/dist/browser/terminal.js +228 -0
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +42 -38
- package/dist/browser-shell.js +6 -3
- package/dist/browser-workspace.d.ts +235 -1
- package/dist/browser-workspace.js +60 -0
- package/dist/builder.d.ts +10 -1
- package/dist/builder.js +87 -24
- package/dist/chat-actions.d.ts +13 -1
- package/dist/chat-actions.js +197 -15
- package/dist/chat-ask.d.ts +4 -0
- package/dist/chat-ask.js +14 -0
- package/dist/chat-channel.js +20 -0
- package/dist/chat-continuity.js +5 -4
- package/dist/chat-decide.d.ts +0 -14
- package/dist/chat-decide.js +4 -1
- package/dist/chat-delivery.d.ts +9 -0
- package/dist/chat-delivery.js +45 -6
- package/dist/chat-messages.d.ts +8 -0
- package/dist/chat-messages.js +36 -4
- package/dist/check-levels-ui.js +2 -2
- package/dist/checkout-proof.d.ts +28 -0
- package/dist/checkout-proof.js +62 -0
- package/dist/claim.d.ts +12 -0
- package/dist/claim.js +19 -0
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +4 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +21 -2
- package/dist/coding-ui.js +6 -4
- package/dist/console-instance.d.ts +124 -0
- package/dist/console-instance.js +337 -0
- package/dist/contracts/chat-actions.d.ts +16 -0
- package/dist/contracts/chat-actions.js +4 -0
- package/dist/contracts/chat-content.d.ts +19 -0
- package/dist/contracts/chat-content.js +6 -0
- package/dist/contracts/console-api.d.ts +13 -4
- package/dist/contracts/console-api.js +13 -4
- package/dist/contracts/flow-card.d.ts +4 -0
- package/dist/contracts/flow-card.js +3 -0
- package/dist/contracts/flow.d.ts +46 -0
- package/dist/contracts/flow.js +11 -2
- package/dist/contracts/lead-tools.d.ts +92 -0
- package/dist/contracts/lead-tools.js +13 -0
- package/dist/contracts/provider.d.ts +11 -1
- package/dist/contracts/provider.js +1 -1
- package/dist/contracts/route.d.ts +76 -1
- package/dist/contracts/route.js +1 -1
- package/dist/contracts/store-json.d.ts +2 -0
- package/dist/control-setup.d.ts +1 -1
- package/dist/control-ui.js +2 -2
- package/dist/converse.d.ts +16 -0
- package/dist/converse.js +32 -4
- package/dist/demo-chat.js +4 -4
- package/dist/demo.js +2 -2
- package/dist/desktop-update-gate.js +6 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/discord-chat.js +4 -1
- package/dist/doctor.js +1 -1
- package/dist/egress-bridge.d.ts +17 -0
- package/dist/egress-bridge.js +111 -0
- package/dist/egress.d.ts +119 -0
- package/dist/egress.js +423 -0
- package/dist/exec.d.ts +78 -0
- package/dist/exec.js +367 -9
- package/dist/export-ui.js +2 -2
- package/dist/flow-engine.js +24 -5
- package/dist/flow-gallery-ui.js +2 -2
- package/dist/flow-gallery.js +27 -8
- package/dist/flow-share.js +16 -5
- package/dist/flow-triggers.d.ts +77 -8
- package/dist/flow-triggers.js +517 -70
- package/dist/flows-cli.js +8 -2
- package/dist/flows-ui.d.ts +0 -2
- package/dist/flows-ui.js +52 -4
- package/dist/flows.d.ts +4 -0
- package/dist/flows.js +9 -1
- package/dist/html.d.ts +9 -0
- package/dist/html.js +11 -0
- package/dist/invoke.d.ts +35 -0
- package/dist/invoke.js +347 -28
- package/dist/keys.d.ts +18 -14
- package/dist/keys.js +5 -0
- package/dist/lead-capabilities.js +3 -2
- package/dist/lead-contract.js +2 -0
- package/dist/lead-doors.d.ts +2 -0
- package/dist/lead-doors.js +7 -0
- package/dist/lead-follow.d.ts +2 -0
- package/dist/lead-follow.js +58 -2
- package/dist/lead-progress.js +1 -0
- package/dist/lead-queue-runtime.d.ts +46 -0
- package/dist/lead-queue-runtime.js +191 -0
- package/dist/lead-queue.d.ts +104 -0
- package/dist/lead-queue.js +187 -0
- package/dist/lead-sweep.d.ts +67 -0
- package/dist/lead-sweep.js +99 -0
- package/dist/lead-tools.d.ts +22 -0
- package/dist/lead-tools.js +152 -1
- package/dist/lead.d.ts +13 -5
- package/dist/lead.js +89 -29
- package/dist/limits-ui.d.ts +8 -1
- package/dist/limits-ui.js +26 -0
- package/dist/live-bus.d.ts +8 -0
- package/dist/live-bus.js +40 -4
- package/dist/live-detail.d.ts +27 -0
- package/dist/live-detail.js +57 -0
- package/dist/live-notes.d.ts +20 -0
- package/dist/live-notes.js +47 -0
- package/dist/live-records.d.ts +140 -0
- package/dist/live-records.js +935 -0
- package/dist/live-terminal.d.ts +269 -0
- package/dist/live-terminal.js +686 -0
- package/dist/live.d.ts +38 -27
- package/dist/live.js +142 -124
- package/dist/mcp-oauth.d.ts +6 -3
- package/dist/mcp-oauth.js +8 -4
- package/dist/memory-pass.js +2 -2
- package/dist/mobile-viewport.d.ts +5 -1
- package/dist/mobile-viewport.js +18 -2
- package/dist/model-catalog.js +4 -0
- package/dist/monitoring-ui.js +2 -2
- package/dist/needs-you.d.ts +1 -1
- package/dist/needs-you.js +11 -4
- package/dist/operate.d.ts +4 -0
- package/dist/operate.js +335 -19
- package/dist/passwordless.d.ts +23 -0
- package/dist/passwordless.js +26 -0
- package/dist/peek-cli.d.ts +12 -4
- package/dist/peek-cli.js +19 -11
- package/dist/phase-routing.js +10 -3
- package/dist/planner-source.d.ts +3 -0
- package/dist/planner-source.js +14 -0
- package/dist/planner.d.ts +2 -1
- package/dist/planner.js +7 -1
- package/dist/policy-ui.js +2 -2
- package/dist/policy.d.ts +1 -20
- package/dist/policy.js +29 -2
- package/dist/project-delete-ui.d.ts +18 -0
- package/dist/project-delete-ui.js +60 -9
- package/dist/project-delete.js +2 -2
- package/dist/project-memory.d.ts +3 -1
- package/dist/project-memory.js +57 -25
- package/dist/project-tools.d.ts +5 -3
- package/dist/project-tools.js +12 -9
- package/dist/provider-auth.d.ts +4 -3
- package/dist/provider-auth.js +5 -4
- package/dist/provider-connection.d.ts +2 -1
- package/dist/provider-connection.js +5 -0
- package/dist/provider.d.ts +25 -2
- package/dist/provider.js +63 -9
- package/dist/pull-request-ui.js +2 -2
- package/dist/push.js +1 -1
- package/dist/quick-start.d.ts +24 -0
- package/dist/quick-start.js +127 -0
- package/dist/release-adapters.d.ts +6 -0
- package/dist/release-adapters.js +70 -3
- package/dist/release.d.ts +10 -1
- package/dist/release.js +34 -15
- package/dist/request-budget-ui.js +2 -2
- package/dist/result-follow-ups.js +3 -2
- package/dist/result-review.d.ts +1 -1
- package/dist/result-review.js +9 -3
- package/dist/retention-ui.js +2 -2
- package/dist/retention.js +10 -1
- package/dist/run-efficiency.d.ts +73 -0
- package/dist/run-efficiency.js +228 -0
- package/dist/runner.d.ts +5 -1
- package/dist/runner.js +22 -1
- package/dist/scope.d.ts +183 -5
- package/dist/scope.js +256 -31
- package/dist/scout-net.d.ts +4 -1
- package/dist/scout-net.js +37 -12
- package/dist/scout.d.ts +20 -6
- package/dist/scout.js +27 -3
- package/dist/serve.js +248 -40
- package/dist/server/chat.d.ts +1 -1
- package/dist/server/chat.js +122 -31
- package/dist/server/chrome.d.ts +16 -2
- package/dist/server/chrome.js +63 -11
- package/dist/server/console.css +107 -42
- package/dist/server/flows.js +38 -8
- package/dist/server/guards.d.ts +1 -0
- package/dist/server/guards.js +23 -9
- package/dist/server/http.d.ts +7 -0
- package/dist/server/http.js +3 -2
- package/dist/server/live.d.ts +34 -1
- package/dist/server/live.js +39 -17
- package/dist/server/pages-assets.js +5 -0
- package/dist/server/pages.js +90 -13
- package/dist/server/people-tokens.d.ts +3 -0
- package/dist/server/people-tokens.js +55 -17
- package/dist/server/remote-hooks.js +12 -6
- package/dist/server/remote.d.ts +2 -1
- package/dist/server/remote.js +14 -5
- package/dist/server/render-chat.d.ts +12 -3
- package/dist/server/render-chat.js +47 -16
- package/dist/server/render-pages.d.ts +32 -4
- package/dist/server/render-pages.js +83 -13
- package/dist/server/render-people.d.ts +3 -2
- package/dist/server/render-people.js +3 -2
- package/dist/server/render-results.d.ts +18 -2
- package/dist/server/render-results.js +37 -28
- package/dist/server/render-settings.js +6 -5
- package/dist/server/render-tasks.d.ts +35 -10
- package/dist/server/render-tasks.js +111 -27
- package/dist/server/request-context.d.ts +2 -0
- package/dist/server/request-context.js +2 -1
- package/dist/server/route-table.d.ts +3 -0
- package/dist/server/route-table.js +47 -5
- package/dist/server/runtime.d.ts +33 -2
- package/dist/server/session.d.ts +25 -1
- package/dist/server/session.js +18 -4
- package/dist/server/settings.js +142 -10
- package/dist/server/sweeps.d.ts +4 -0
- package/dist/server/sweeps.js +58 -0
- package/dist/server/task-views.d.ts +2 -1
- package/dist/server/task-views.js +96 -6
- package/dist/server/tasks.js +327 -78
- package/dist/server/terminals.d.ts +31 -0
- package/dist/server/terminals.js +365 -0
- package/dist/setup-guide.js +3 -2
- package/dist/slack-chat.js +4 -1
- package/dist/spend-ui.js +3 -3
- package/dist/spend.js +2 -1
- package/dist/steer-capabilities.d.ts +32 -0
- package/dist/steer-capabilities.js +60 -0
- package/dist/steer-controls.d.ts +14 -0
- package/dist/steer-controls.js +36 -0
- package/dist/steer-status.js +2 -2
- package/dist/storage-ui.js +4 -4
- package/dist/store-baseline.d.ts +1 -1
- package/dist/store-baseline.js +1 -1
- package/dist/store.d.ts +440 -15
- package/dist/store.js +1072 -52
- package/dist/subagent-desk.js +1 -1
- package/dist/summary.js +3 -0
- package/dist/task-source.d.ts +58 -0
- package/dist/task-source.js +93 -0
- package/dist/team-leads.js +1 -1
- package/dist/teams-chat.js +4 -1
- package/dist/teardown-git.d.ts +22 -0
- package/dist/teardown-git.js +136 -2
- package/dist/telegram-lead.d.ts +3 -0
- package/dist/telegram-lead.js +44 -7
- package/dist/telegram.d.ts +11 -2
- package/dist/telegram.js +51 -17
- package/dist/terminal-catalog.d.ts +11 -0
- package/dist/terminal-catalog.js +129 -0
- package/dist/text-limits.d.ts +2 -0
- package/dist/text-limits.js +2 -0
- package/dist/tool-broker.d.ts +14 -0
- package/dist/tool-broker.js +67 -2
- package/dist/toolroll-update-ui.js +2 -2
- package/dist/toolroll-update.js +13 -3
- package/dist/tools-ui.js +2 -2
- package/dist/unfenced-test-support.d.ts +2 -0
- package/dist/unfenced-test-support.js +11 -0
- package/dist/webhook-limits.d.ts +23 -0
- package/dist/webhook-limits.js +30 -0
- package/dist/webhook-readers.d.ts +56 -0
- package/dist/webhook-readers.js +138 -0
- package/dist/webhook-signing.d.ts +27 -0
- package/dist/webhook-signing.js +119 -0
- package/dist/work-index.d.ts +29 -0
- package/dist/work-index.js +16 -2
- package/dist/workspace-revision.js +3 -1
- package/dist/writes-mode.d.ts +9 -0
- package/dist/writes-mode.js +8 -0
- package/package.json +15 -2
package/dist/server/runtime.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { FreshFacts } from '../scope.js';
|
|
2
|
+
import type { createPersonalQueueRuntime } from '../lead-queue-runtime.js';
|
|
1
3
|
import type { FlowRooms } from '../flow-live.js';
|
|
2
4
|
import type { IntegrationMonitor } from '../integrations.js';
|
|
3
5
|
import type { OidcSettings } from '../oidc.js';
|
|
@@ -7,7 +9,8 @@ import type { TaskRooms } from '../task-live.js';
|
|
|
7
9
|
/** Captured server state and shared guards. Getters preserve live state across requests. */
|
|
8
10
|
import { type IncomingMessage, type ServerResponse } from "node:http";
|
|
9
11
|
import { type AssignmentSnapshot } from '../assignment.js';
|
|
10
|
-
import { type BrowserPhoneCard, type BrowserUpdates } from '../browser-workspace.js';
|
|
12
|
+
import { type BrowserPhoneCard, type BrowserResultView, type BrowserTaskTab, type BrowserUpdates } from '../browser-workspace.js';
|
|
13
|
+
import type { Html } from '../html.js';
|
|
11
14
|
import { CodingWorkspace } from '../coding-workspace.js';
|
|
12
15
|
import { type FormFieldOf, type FormView } from "../contracts/console-api.js";
|
|
13
16
|
import type { DemoLead } from "../demo.js";
|
|
@@ -93,7 +96,7 @@ export interface ServerRuntime {
|
|
|
93
96
|
repos: string[];
|
|
94
97
|
roots: readonly string[];
|
|
95
98
|
};
|
|
96
|
-
taskScreen: (response: ServerResponse, who: Who, taskId: string, problem: string | null, status: number, scopeDraft?: URLSearchParams, cancelDraft?: string, editPlan?: boolean) => void;
|
|
99
|
+
taskScreen: (response: ServerResponse, who: Who, taskId: string, problem: string | null, status: number, scopeDraft?: URLSearchParams, cancelDraft?: string, editPlan?: boolean, page?: TaskPageExtras) => void;
|
|
97
100
|
runVisible: (run: Run) => boolean;
|
|
98
101
|
runIsLive: (run: Pick<Run, "outcome" | "leaseId" | "taskRef">) => boolean;
|
|
99
102
|
options: ServeOptions;
|
|
@@ -134,6 +137,8 @@ export interface ServerRuntime {
|
|
|
134
137
|
generation: number;
|
|
135
138
|
}, csrf: string) => TeamResponse;
|
|
136
139
|
team: ReturnType<typeof createTeamRuntime>;
|
|
140
|
+
/** The personal lead queue (lead-queue-runtime.ts): a message sent while the person's lead is busy waits here. */
|
|
141
|
+
personalQueue: ReturnType<typeof createPersonalQueueRuntime>;
|
|
137
142
|
teamChatProvider: TeamChatProviderResolver;
|
|
138
143
|
pullRequestTargetOf: (runId: number) => string | null;
|
|
139
144
|
chatEnablement: () => ChatEnablement;
|
|
@@ -176,6 +181,8 @@ export interface ServerRuntime {
|
|
|
176
181
|
settingsUpdates: (actor: string, csrf: string) => BrowserUpdates | null;
|
|
177
182
|
phoneSetup: (who: Who) => BrowserPhoneCard | undefined;
|
|
178
183
|
authenticateApprover: (who: Who, token: string, repo?: string | null) => ReturnType<typeof checkApproverPassword>;
|
|
184
|
+
/** A credential act (passwordless.md's closed list): a password typed now, never a session or a remembered one. */
|
|
185
|
+
authenticateCredential: (who: Who, token: string, repo?: string | null) => ReturnType<typeof checkApproverPassword>;
|
|
179
186
|
projectOf: (who: Who, request: IncomingMessage) => string | null | undefined;
|
|
180
187
|
bustBadge: () => void;
|
|
181
188
|
revisionDestination: (child: string, back: string) => string;
|
|
@@ -223,6 +230,8 @@ export interface ServerRuntime {
|
|
|
223
230
|
}>;
|
|
224
231
|
linkKey: (code: string) => string;
|
|
225
232
|
fromThisComputer: (request: IncomingMessage) => boolean;
|
|
233
|
+
/** Cockpit B2: the request's live password-freshness facts (null off a browser session). */
|
|
234
|
+
freshFactsOf: (request: IncomingMessage, who: Who | null) => FreshFacts | null;
|
|
226
235
|
passwordAllowed: (name: string) => boolean;
|
|
227
236
|
arrival: (request: IncomingMessage) => {
|
|
228
237
|
agent: string | null;
|
|
@@ -236,6 +245,11 @@ export interface ServerRuntime {
|
|
|
236
245
|
intent: SsoIntent;
|
|
237
246
|
returnTo: string;
|
|
238
247
|
expires: number;
|
|
248
|
+
startedAt: number;
|
|
249
|
+
session: {
|
|
250
|
+
hash: string;
|
|
251
|
+
generation: number;
|
|
252
|
+
} | null;
|
|
239
253
|
}>;
|
|
240
254
|
ssoHandoffs: Map<string, {
|
|
241
255
|
claims: OidcClaims;
|
|
@@ -243,6 +257,11 @@ export interface ServerRuntime {
|
|
|
243
257
|
intent: SsoIntent;
|
|
244
258
|
returnTo: string;
|
|
245
259
|
expires: number;
|
|
260
|
+
checkedAt: number | null;
|
|
261
|
+
session: {
|
|
262
|
+
hash: string;
|
|
263
|
+
generation: number;
|
|
264
|
+
} | null;
|
|
246
265
|
}>;
|
|
247
266
|
joinSourceOf: (request: IncomingMessage) => string;
|
|
248
267
|
signInBudget: SourceBudget;
|
|
@@ -286,3 +305,15 @@ export interface ServerRuntime {
|
|
|
286
305
|
reading: boolean;
|
|
287
306
|
}>;
|
|
288
307
|
}
|
|
308
|
+
/** What the task page's GET adds (docs/plans/one-page-task-and-agent.md): the tab its address names, and the Result tab's
|
|
309
|
+
* selected result (its no-script HTML and its view), whether it is the task's current one. Refusals re-render without. */
|
|
310
|
+
export type TaskPageExtras = {
|
|
311
|
+
tab?: BrowserTaskTab;
|
|
312
|
+
result?: {
|
|
313
|
+
html: Html;
|
|
314
|
+
selected: NonNullable<BrowserResultView["selected"]>;
|
|
315
|
+
current: boolean;
|
|
316
|
+
} | null;
|
|
317
|
+
/** The result runs the attempt picker may offer (the Result tab would show each); absent: every finished result run. */
|
|
318
|
+
admissible?: ReadonlySet<number>;
|
|
319
|
+
};
|
package/dist/server/session.d.ts
CHANGED
|
@@ -14,6 +14,11 @@ export declare const SIGN_IN_LINK_MS: number;
|
|
|
14
14
|
/** A cookie idles out after half a day and dies outright after a week. */
|
|
15
15
|
export declare const SESSION_IDLE_MS: number;
|
|
16
16
|
export declare const SESSION_ABSOLUTE_MS: number;
|
|
17
|
+
/** A session's lifetime, the one reading: within its idle and absolute limits (sign-in lookup and every act use it). */
|
|
18
|
+
export declare const sessionWithinLifetime: (session: {
|
|
19
|
+
lastSeen: number;
|
|
20
|
+
createdAt: number;
|
|
21
|
+
}, now: number) => boolean;
|
|
17
22
|
/** An approval nonce is a rendered form, not a standing right — it ages out fast. */
|
|
18
23
|
export declare const NONCE_TTL_MS: number;
|
|
19
24
|
export declare const NONCE_CAP = 500;
|
|
@@ -37,6 +42,15 @@ export type Session = {
|
|
|
37
42
|
sso?: {
|
|
38
43
|
at: number;
|
|
39
44
|
};
|
|
45
|
+
/** Cockpit B2: when a password was last verified on this session (in memory only: a restart forgets it). */
|
|
46
|
+
passwordAt?: number;
|
|
47
|
+
/** Cockpit B2: when the identity provider last re-checked this person on request (a step-up's re-authentication).
|
|
48
|
+
* Only this, or a password, starts a freshness window; an ordinary sign-in never does. In memory only. */
|
|
49
|
+
ssoReauthAt?: number;
|
|
50
|
+
/** Cockpit B2: an explicit provider re-check not yet spent: stands in for ONE step-up (consumed by it). In memory only. */
|
|
51
|
+
ssoNowAt?: number;
|
|
52
|
+
/** Forget: nothing verified before this counts as fresh. */
|
|
53
|
+
freshFloor?: number;
|
|
40
54
|
/** v101: the browser and address it signed in from, for the person to recognise it. */
|
|
41
55
|
agent?: string | null;
|
|
42
56
|
address?: string | null;
|
|
@@ -121,12 +135,22 @@ export declare class PersistentSessions extends Map<string, Session> {
|
|
|
121
135
|
static hash(id: string): string;
|
|
122
136
|
set(id: string, session: Session): this;
|
|
123
137
|
delete(id: string): boolean;
|
|
124
|
-
/**
|
|
138
|
+
/**
|
|
139
|
+
* Keep a changed session (its project, its provider check); `seen` only once a minute. Only ever an update: a session
|
|
140
|
+
* ended elsewhere (another process, Sessions) is dropped here, never written back by its cached copy (live terminals
|
|
141
|
+
* review, rounds 2–3, finding 1).
|
|
142
|
+
*/
|
|
125
143
|
persist(session: Session, seen?: boolean): void;
|
|
144
|
+
/** Whether this exact session is still saved in the database (ended from another process, it isn't): read at the act. */
|
|
145
|
+
savedNow(session: Session): boolean;
|
|
146
|
+
/** Whether this cookie's session is still saved (one ended in another process is not). */
|
|
147
|
+
stillSaved(id: string): boolean;
|
|
126
148
|
/** A cookie from before a restart, if its session is still kept. */
|
|
127
149
|
load(id: string): Session | null;
|
|
128
150
|
/** End one session by its kept hash (from the Sessions page). */
|
|
129
151
|
endByHash(idHash: string): void;
|
|
152
|
+
/** Whether this exact session is still signed in here (not signed out, ended from Sessions, expired or replaced). */
|
|
153
|
+
holds(session: Session): boolean;
|
|
130
154
|
hashOf(session: Session): string | null;
|
|
131
155
|
}
|
|
132
156
|
/** The console's server, and the one-time sign-in link `up` opens: a path on this server, or null for no such approver.
|
package/dist/server/session.js
CHANGED
|
@@ -26,6 +26,8 @@ export const SIGN_IN_LINK_MS = 10 * 60_000;
|
|
|
26
26
|
/** A cookie idles out after half a day and dies outright after a week. */
|
|
27
27
|
export const SESSION_IDLE_MS = 12 * 60 * 60_000;
|
|
28
28
|
export const SESSION_ABSOLUTE_MS = 7 * 24 * 60 * 60_000;
|
|
29
|
+
/** A session's lifetime, the one reading: within its idle and absolute limits (sign-in lookup and every act use it). */
|
|
30
|
+
export const sessionWithinLifetime = (session, now) => now - session.lastSeen <= SESSION_IDLE_MS && now - session.createdAt <= SESSION_ABSOLUTE_MS;
|
|
29
31
|
/** An approval nonce is a rendered form, not a standing right — it ages out fast. */
|
|
30
32
|
export const NONCE_TTL_MS = 15 * 60_000;
|
|
31
33
|
export const NONCE_CAP = 500;
|
|
@@ -55,22 +57,32 @@ export class PersistentSessions extends Map {
|
|
|
55
57
|
set(id, session) {
|
|
56
58
|
super.set(id, session);
|
|
57
59
|
this.ids.set(session, id);
|
|
58
|
-
this.
|
|
60
|
+
this.savedAt.set(session, Date.now());
|
|
61
|
+
this.db.saveWebSession({ idHash: PersistentSessions.hash(id), account: session.name, csrf: session.csrf, role: session.role, generation: session.generation, createdAt: session.createdAt,
|
|
62
|
+
lastSeen: session.lastSeen, project: session.project, projectRevision: session.projectRevision, ssoAt: session.sso?.at ?? null, agent: session.agent ?? null, address: session.address ?? null });
|
|
59
63
|
return this;
|
|
60
64
|
}
|
|
61
65
|
delete(id) {
|
|
62
66
|
this.db.dropWebSession(PersistentSessions.hash(id));
|
|
63
67
|
return super.delete(id);
|
|
64
68
|
}
|
|
65
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* Keep a changed session (its project, its provider check); `seen` only once a minute. Only ever an update: a session
|
|
71
|
+
* ended elsewhere (another process, Sessions) is dropped here, never written back by its cached copy (live terminals
|
|
72
|
+
* review, rounds 2–3, finding 1).
|
|
73
|
+
*/
|
|
66
74
|
persist(session, seen = false) {
|
|
67
75
|
const id = this.ids.get(session);
|
|
68
76
|
if (id === undefined || (seen && Date.now() - (this.savedAt.get(session) ?? 0) < 60_000))
|
|
69
77
|
return;
|
|
70
78
|
this.savedAt.set(session, Date.now());
|
|
71
|
-
this.db.
|
|
72
|
-
|
|
79
|
+
if (!this.db.updateWebSession({ idHash: PersistentSessions.hash(id), lastSeen: session.lastSeen, project: session.project, projectRevision: session.projectRevision, ssoAt: session.sso?.at ?? null }))
|
|
80
|
+
super.delete(id);
|
|
73
81
|
}
|
|
82
|
+
/** Whether this exact session is still saved in the database (ended from another process, it isn't): read at the act. */
|
|
83
|
+
savedNow(session) { const hash = this.hashOf(session); return hash !== null && this.db.webSession(hash) !== null; }
|
|
84
|
+
/** Whether this cookie's session is still saved (one ended in another process is not). */
|
|
85
|
+
stillSaved(id) { return this.db.webSession(PersistentSessions.hash(id)) !== null; }
|
|
74
86
|
/** A cookie from before a restart, if its session is still kept. */
|
|
75
87
|
load(id) {
|
|
76
88
|
const row = this.db.webSession(PersistentSessions.hash(id));
|
|
@@ -90,5 +102,7 @@ export class PersistentSessions extends Map {
|
|
|
90
102
|
super.delete(id);
|
|
91
103
|
this.db.dropWebSession(idHash);
|
|
92
104
|
}
|
|
105
|
+
/** Whether this exact session is still signed in here (not signed out, ended from Sessions, expired or replaced). */
|
|
106
|
+
holds(session) { const id = this.ids.get(session); return id !== undefined && super.get(id) === session; }
|
|
93
107
|
hashOf(session) { const id = this.ids.get(session); return id === undefined ? null : PersistentSessions.hash(id); }
|
|
94
108
|
}
|
package/dist/server/settings.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { PASSWORDLESS_TRADE, passwordlessSetting, setPasswordless } from "../passwordless.js";
|
|
2
|
+
import { ACP_AGENTS, isKeyedProvider } from "../acp-agents.js";
|
|
3
|
+
import { LIVE_VIEW_CONSENT, liveViewConsentDigest, setLiveCommands } from "../live-detail.js";
|
|
1
4
|
import { CONSOLE_FORMS, readForm } from "../contracts/console-api.js";
|
|
2
5
|
import { galleryTemplateOf } from "../flow-gallery.js";
|
|
3
6
|
import { finishGoogleConsent, GOOGLE_CALLBACK } from "../google-mail.js";
|
|
@@ -30,7 +33,7 @@ import { PACKAGE_VERSION } from "../version.js";
|
|
|
30
33
|
import { handlersOf } from './handler-registry.js';
|
|
31
34
|
import { refuse, screen } from "./chrome.js";
|
|
32
35
|
import { HANDOFF_STYLE, redirect, respond, safeReturn, signInSpent, startedHere } from "./http.js";
|
|
33
|
-
import { html, htmlString, joinHtml, postForm } from "../html.js";
|
|
36
|
+
import { html, htmlString, joinHtml, postForm, reauth } from "../html.js";
|
|
34
37
|
import { browserCaller } from "./session.js";
|
|
35
38
|
/** settings handlers, moved without changing their route bodies. */
|
|
36
39
|
import { DEFAULT_ACCENT, normalHex } from "../accent-colors.js";
|
|
@@ -76,6 +79,8 @@ import { openRouterPickerScript } from "../openrouter-models.js";
|
|
|
76
79
|
import { policyHtml } from "../policy-ui.js";
|
|
77
80
|
import { checkPolicy, parseList, policyParts } from "../policy.js";
|
|
78
81
|
import { parseProjectConcurrency, saveProjectConcurrency } from "../project-concurrency.js";
|
|
82
|
+
import { MAX_NETWORK_HOSTS, normalizeHost } from "../egress.js";
|
|
83
|
+
import { isWritesMode } from "../writes-mode.js";
|
|
79
84
|
import { projectDeleteConfirmHtml, projectSettingsHtml } from "../project-delete-ui.js";
|
|
80
85
|
import { deleteProject, holdingsWords } from "../project-delete.js";
|
|
81
86
|
import { applySavedKnowledge, changeKnowledge, knowledgeVersion, knowledgeView } from "../project-knowledge.js";
|
|
@@ -122,7 +127,7 @@ import { personChip, ROLE_TITLES } from "./chrome.js";
|
|
|
122
127
|
import { chatReturnWithSaid, goOutside } from "./http.js";
|
|
123
128
|
import { latestReleaseFor, leadSettingsHtml, notificationProjects, SETTINGS_AUTOSAVE_SCRIPT, settingsPage, settingsWorkers, telegramDeliveryWords, telegramTrouble } from "./render-settings.js";
|
|
124
129
|
export function createSettingsHandlers(runtime) {
|
|
125
|
-
const { store, providerHome, managedRepos, consoleProjects, sendScreen, chromeFor, connectionCheck, clock, modelCatalog, modelSeams, toolsViewOf, codexServers, projectViewOf, sessions, restricted, options, integrations, consoleOrigin, evidenceRoot, storagePool, visible, ssoSettings, checkLocalAgents, chatKeyFor, chatCatalog, leadWords, agentSignInCommand, settingsUpdates, phoneSetup, authenticateApprover, deletedRepos, recordSignIn, projectOf, providerFor, admissionList, toolHome, connectVisits, mintApprovalNonce, consumeApprovalNonce, defaultProject, googleVisits } = runtime;
|
|
130
|
+
const { store, providerHome, managedRepos, consoleProjects, sendScreen, chromeFor, connectionCheck, clock, modelCatalog, modelSeams, toolsViewOf, codexServers, projectViewOf, sessions, restricted, options, integrations, consoleOrigin, evidenceRoot, storagePool, visible, ssoSettings, checkLocalAgents, chatKeyFor, chatCatalog, leadWords, agentSignInCommand, settingsUpdates, phoneSetup, authenticateApprover, authenticateCredential, deletedRepos, recordSignIn, projectOf, providerFor, admissionList, toolHome, connectVisits, mintApprovalNonce, consumeApprovalNonce, defaultProject, googleVisits } = runtime;
|
|
126
131
|
// v105: what agent work cost, by month, and the monthly budgets. An instance operator's page.
|
|
127
132
|
async function spendPage(ctx) {
|
|
128
133
|
const { url, who, response, now } = ctx;
|
|
@@ -461,6 +466,40 @@ ${connectionHtml(asked, connection, task)}${command === null ? "" : html `<p>On
|
|
|
461
466
|
const body = ssoSettingsHtml({ settings, redirect: origin === null ? null : `${origin}${SSO_CALLBACK}`, projects, linked: settings !== null && store.ssoIdentitiesOf(who.name).some(one => one.issuer.replace(/\/+$/, "") === settings.issuer) }, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") });
|
|
462
467
|
return sendScreen(response, 200, screen("Sign-in", html `<p><a href="/settings">Settings</a></p><h1>Sign-in</h1>${body}`, { chrome: chromeFor(project, "settings") }));
|
|
463
468
|
}
|
|
469
|
+
/** Passwordless (docs/plans/passwordless.md): the switch, and the trade in plain words. */
|
|
470
|
+
async function passwordlessPage(ctx) {
|
|
471
|
+
const { url, response, project } = ctx;
|
|
472
|
+
browserCaller(ctx.who);
|
|
473
|
+
const setting = passwordlessSetting(store);
|
|
474
|
+
const said = url.searchParams.get("said"), problem = url.searchParams.get("problem");
|
|
475
|
+
const note = problem ? html `<p class="problem" role="alert">${problem}</p>` : said ? html `<p role="status">${said}</p>` : "";
|
|
476
|
+
const since = setting.at === null ? "" : html ` <span class="meta">(${setting.on ? "on" : "off"} since ${new Date(setting.at).toLocaleString("en-US", { dateStyle: "medium", timeStyle: "short" })}, by ${setting.setBy ?? "someone"})</span>`;
|
|
477
|
+
const control = setting.on
|
|
478
|
+
? postForm("/settings/passwordless", html `<button class="secondary">Turn Passwordless off</button>`, { hidden: { action: "off" } })
|
|
479
|
+
: postForm("/settings/passwordless", html `<label>Your password<input type="password" name="password" autocomplete="current-password" required></label><button>Turn Passwordless on</button>`, { attrs: { class: "card approve-form" }, hidden: { action: "on" } });
|
|
480
|
+
return sendScreen(response, 200, screen("Passwordless", html `<p><a href="/settings">Settings</a></p><h1>Passwordless</h1>${note}\
|
|
481
|
+
<p>When it's on, signing in is the only password: approvals, settings, budgets, signing a mode and starting a terminal no longer ask again — in the browser, on the phone, and in this computer's <code>toolroll</code> command (through its remembered login).</p>\
|
|
482
|
+
<p class="meta">${PASSWORDLESS_TRADE}</p>\
|
|
483
|
+
<p><strong>Passwordless is ${setting.on ? "on" : "off"}.</strong>${since}</p>${control}`, { chrome: chromeFor(project, "settings") }));
|
|
484
|
+
}
|
|
485
|
+
async function passwordlessSave(ctx) {
|
|
486
|
+
const { response, now, posted } = ctx;
|
|
487
|
+
const who = browserCaller(ctx.who);
|
|
488
|
+
const body = readForm(posted, CONSOLE_FORMS.passwordless);
|
|
489
|
+
const back = (key, words) => redirect(response, `/settings/passwordless?${key}=${encodeURIComponent(words)}`);
|
|
490
|
+
const action = body.get("action");
|
|
491
|
+
if (action === "off") {
|
|
492
|
+
setPasswordless(store, false, who.name, now);
|
|
493
|
+
return back("said", "Passwordless is off. Step-ups ask for your password again.");
|
|
494
|
+
}
|
|
495
|
+
if (action !== "on")
|
|
496
|
+
return back("problem", "Choose on or off.");
|
|
497
|
+
// Turning it on is a credential act: a password typed now (or a one-use provider re-check), never a session.
|
|
498
|
+
if (!authenticateCredential(who, body.get("password") ?? "").ok)
|
|
499
|
+
return back("problem", "Enter your Toolroll password to turn Passwordless on.");
|
|
500
|
+
setPasswordless(store, true, who.name, now);
|
|
501
|
+
return back("said", "Passwordless is on. Signing in is the only password now.");
|
|
502
|
+
}
|
|
464
503
|
async function knowledgePage(ctx) {
|
|
465
504
|
const { url, who, response, project } = ctx;
|
|
466
505
|
const projects = consoleProjects();
|
|
@@ -600,13 +639,14 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
|
|
|
600
639
|
};
|
|
601
640
|
const providerKeys = who.role !== "approver"
|
|
602
641
|
? null
|
|
603
|
-
|
|
642
|
+
// ACP agents sign in their own way; Toolroll keeps no key for them, so the key card lists only the others.
|
|
643
|
+
: await Promise.all(PROVIDER_IDS.filter(isKeyedProvider).map(async (provider) => ({
|
|
604
644
|
provider,
|
|
605
645
|
envName: PROVIDER_KEY_ENV[provider],
|
|
606
646
|
...keyStatus(provider, providerHome),
|
|
607
647
|
connection: await connectionCheck(provider, url.searchParams.get("check-connection") === provider),
|
|
608
648
|
ambient: (process.env[PROVIDER_KEY_ENV[provider]] ?? "") !== "",
|
|
609
|
-
mode: readAuthMode(provider, providerHome),
|
|
649
|
+
mode: readAuthMode(provider, providerHome) === "api-key" ? "api-key" : "subscription",
|
|
610
650
|
subscriptionCapable: SUBSCRIPTION_CAPABLE[provider],
|
|
611
651
|
})));
|
|
612
652
|
const telegramConfigured = loadBotToken(process.env, options.telegramTokenFile) !== null;
|
|
@@ -656,6 +696,18 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
|
|
|
656
696
|
return redirect(response, "/work");
|
|
657
697
|
}
|
|
658
698
|
// The update notice is dismissed per browser and per version: a newer release shows it again.
|
|
699
|
+
// Cockpit B2: Forget — nothing verified on this session before now counts as fresh (a CSRF-checked post, like every act).
|
|
700
|
+
async function forgetPassword(ctx) {
|
|
701
|
+
const { who, response, posted } = ctx;
|
|
702
|
+
const body = readForm(posted, CONSOLE_FORMS.forgetPassword);
|
|
703
|
+
if (who.via === "cookie") {
|
|
704
|
+
who.session.freshFloor = Date.now();
|
|
705
|
+
delete who.session.passwordAt;
|
|
706
|
+
delete who.session.ssoReauthAt;
|
|
707
|
+
delete who.session.ssoNowAt;
|
|
708
|
+
}
|
|
709
|
+
return redirect(response, safeReturn(body.get("return") ?? "/"));
|
|
710
|
+
}
|
|
659
711
|
async function updatesDismiss(ctx) {
|
|
660
712
|
const { who, response, posted } = ctx;
|
|
661
713
|
const body = readForm(posted, CONSOLE_FORMS.updatesDismiss);
|
|
@@ -808,6 +860,51 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
|
|
|
808
860
|
store.bumpWake();
|
|
809
861
|
return back("said", changed.before === changed.after ? "Nothing changed." : `Saved. Up to ${n} at once.`);
|
|
810
862
|
}
|
|
863
|
+
// Settings → Project → Live view (live-detail.ts): an approver for the project shows commands with their password,
|
|
864
|
+
// or hides them in one step; the ledger keeps each change. It applies from the next run.
|
|
865
|
+
async function projectLiveView(ctx) {
|
|
866
|
+
const { who, response, now, posted } = ctx;
|
|
867
|
+
const body = readForm(posted, CONSOLE_FORMS.projectLiveView);
|
|
868
|
+
const repo = body.get("repo") ?? "";
|
|
869
|
+
if (!consoleProjects().includes(repo) || !visible(repo))
|
|
870
|
+
return refuse(response, who, 403, "That project is outside your access.", "/settings/project");
|
|
871
|
+
if (who.via !== "cookie" || who.role !== "approver" || store.isDemo() || !store.accountCanAccess(who.name, repo))
|
|
872
|
+
return refuse(response, who, 403, "An approver for this project changes the live view.", "/settings/project");
|
|
873
|
+
const back = (key, words) => redirect(response, `/settings/project?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}`);
|
|
874
|
+
const act = body.get("act");
|
|
875
|
+
if (act === "off")
|
|
876
|
+
return back("said", setLiveCommands(store, { repo, commands: false, by: who.name, via: "the console" }, now).said);
|
|
877
|
+
if (act !== "on")
|
|
878
|
+
return back("problem", "That change isn't available.");
|
|
879
|
+
// Single use, for this person and exactly this change: a replayed or copied form changes nothing.
|
|
880
|
+
if (!consumeApprovalNonce(body.get("nonce") ?? "", who.name, LIVE_VIEW_CONSENT, liveViewConsentDigest(repo)))
|
|
881
|
+
return back("problem", "That form was already used or is out of date. Try again from this page.");
|
|
882
|
+
if (!authenticateApprover(who, body.get("password") ?? "", repo).ok)
|
|
883
|
+
return back("problem", "Enter your Toolroll password to show commands.");
|
|
884
|
+
return back("said", setLiveCommands(store, { repo, commands: true, by: who.name, via: "the console" }, now).said);
|
|
885
|
+
}
|
|
886
|
+
// Settings → Project → Network (egress.ts): what the project's builds may reach. An approver for the project changes it
|
|
887
|
+
// with their password typed now; the ledger keeps before → after. New scopes use it; approved ones keep theirs.
|
|
888
|
+
async function projectNetwork(ctx) {
|
|
889
|
+
const { who, response, now, posted } = ctx;
|
|
890
|
+
const body = readForm(posted, CONSOLE_FORMS.projectNetwork);
|
|
891
|
+
const repo = body.get("repo") ?? "";
|
|
892
|
+
if (!consoleProjects().includes(repo) || !visible(repo))
|
|
893
|
+
return refuse(response, who, 403, "That project is outside your access.", "/settings/project");
|
|
894
|
+
if (who.via !== "cookie" || who.role !== "approver" || store.isDemo() || !store.accountCanAccess(who.name, repo))
|
|
895
|
+
return refuse(response, who, 403, "An approver for this project changes what its builds may reach.", "/settings/project");
|
|
896
|
+
const back = (key, words) => redirect(response, `/settings/project?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}#network`);
|
|
897
|
+
const hosts = (body.get("hosts") ?? "").split(/[\s,]+/).filter(Boolean).map(normalizeHost);
|
|
898
|
+
if (hosts.some(one => one === null))
|
|
899
|
+
return back("problem", "A host is a name like api.stripe.com or *.stripe.com: no address, port or path. Nothing changed.");
|
|
900
|
+
if (hosts.length > MAX_NETWORK_HOSTS)
|
|
901
|
+
return back("problem", `A project lists at most ${MAX_NETWORK_HOSTS} hosts. Nothing changed.`);
|
|
902
|
+
if (!authenticateCredential(who, body.get("password") ?? "").ok)
|
|
903
|
+
return back("problem", "Enter your Toolroll password to change what builds may reach.");
|
|
904
|
+
const changed = store.setProjectNetwork(repo, { mode: body.get("mode") === "open" ? "open" : "allowlist", hosts: hosts, loopback: body.get("local") === "any" ? "any" : "proxy-only" }, who.name, now);
|
|
905
|
+
const same = JSON.stringify(changed.before) === JSON.stringify(changed.after);
|
|
906
|
+
return back("said", same ? "Nothing changed." : "Saved. Tasks scoped from now on use it; tasks already approved keep the network they were approved with.");
|
|
907
|
+
}
|
|
811
908
|
// Delete a project, in two steps: its name, then what goes and the password. An instance operator; the ledger keeps who and what.
|
|
812
909
|
async function projectDelete(ctx) {
|
|
813
910
|
const { response, now, posted } = ctx;
|
|
@@ -917,7 +1014,8 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
|
|
|
917
1014
|
const found = await providerFor(before.issuer);
|
|
918
1015
|
return found.ok ? back("said", `${before.label} answers: it's ready for people to sign in.`) : back("problem", found.said);
|
|
919
1016
|
}
|
|
920
|
-
|
|
1017
|
+
// Sign-in methods and the identity provider are the login itself: a credential act, a password typed now.
|
|
1018
|
+
if (!authenticateCredential(who, body.get("password") ?? "").ok)
|
|
921
1019
|
return back("problem", "Enter your Toolroll password to change sign-in.");
|
|
922
1020
|
if (action === "remove") {
|
|
923
1021
|
removeSsoSettings(options.configDir);
|
|
@@ -1611,7 +1709,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
|
|
|
1611
1709
|
const nonceKey = `project-instructions:${project}`;
|
|
1612
1710
|
if (step === "preview") {
|
|
1613
1711
|
const nonce = mintApprovalNonce(who.name, nonceKey, preview.fingerprint);
|
|
1614
|
-
return sendScreen(response, 200, screen("Review agent instructions", html `<h1>Review agent instructions</h1><p>${preview.plan.skillPath}</p><pre class="recap">${preview.content}</pre>${postForm("/control/instructions-approve", html `<label>Your password<input type="password" name="token" autocomplete="current-password" required></label
|
|
1712
|
+
return sendScreen(response, 200, screen("Review agent instructions", html `<h1>Review agent instructions</h1><p>${preview.plan.skillPath}</p><pre class="recap">${preview.content}</pre>${postForm("/control/instructions-approve", html `${reauth(html `<label>Your password<input type="password" name="token" autocomplete="current-password" required></label>`)}<button>Add these instructions</button>`, { hidden: { repo: project, nonce, fingerprint: preview.fingerprint } })}`, { chrome: chromeFor(project, "settings") }));
|
|
1615
1713
|
}
|
|
1616
1714
|
if (who.via === "cookie" && !consumeApprovalNonce(body.get("nonce") ?? "", who.name, nonceKey, body.get("fingerprint") ?? ""))
|
|
1617
1715
|
return refuse(response, who, 409, "This instruction preview expired. Review it again.", "/control");
|
|
@@ -1655,7 +1753,8 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
|
|
|
1655
1753
|
savePrimary(dir, "slack");
|
|
1656
1754
|
return redirect(response, "/settings/slack");
|
|
1657
1755
|
}
|
|
1658
|
-
|
|
1756
|
+
// Connecting or pairing opens a way to act from Slack: a credential act. Unpairing or disconnecting only closes one.
|
|
1757
|
+
if (!(action === "connect" || action === "pair" ? authenticateCredential(who, body.get("password") ?? "") : authenticateApprover(who, body.get("password") ?? "")).ok)
|
|
1659
1758
|
return show("Enter your Toolroll password to change Slack access.", 403);
|
|
1660
1759
|
const generation = store.accountOf(who.name).generation;
|
|
1661
1760
|
if (action === "connect") {
|
|
@@ -1758,6 +1857,11 @@ ${postForm("/settings/chat-approval/save", html `\
|
|
|
1758
1857
|
return show("Submit one value for each field.");
|
|
1759
1858
|
if (botId === null)
|
|
1760
1859
|
return show("Connect the Telegram bot first.", 409);
|
|
1860
|
+
// Pairing a phone is a credential act (a typed password, always); unpairing only closes a way in.
|
|
1861
|
+
if (op === "unpair" && authenticateApprover(who, body.get("password") ?? "").ok) {
|
|
1862
|
+
store.unpairTelegram(botId, who.name, now);
|
|
1863
|
+
return redirect(response, "/settings/telegram");
|
|
1864
|
+
}
|
|
1761
1865
|
const pairingIdentity = authenticateAccount(store, who.name, body.get("password") ?? "");
|
|
1762
1866
|
if (!pairingIdentity.ok || pairingIdentity.role !== "approver" || pairingIdentity.generation !== who.session.generation)
|
|
1763
1867
|
return show("Enter your Toolroll password to change your phone pairing.", 403);
|
|
@@ -1788,7 +1892,7 @@ ${postForm("/settings/chat-approval/save", html `\
|
|
|
1788
1892
|
savePrimary(dir, "teams");
|
|
1789
1893
|
return redirect(response, "/settings/teams");
|
|
1790
1894
|
}
|
|
1791
|
-
if (!authenticateApprover(who, body.get("password") ?? "").ok)
|
|
1895
|
+
if (!(action === "connect" || action === "pair" ? authenticateCredential(who, body.get("password") ?? "") : authenticateApprover(who, body.get("password") ?? "")).ok)
|
|
1792
1896
|
return show("Enter your Toolroll password to change Teams access.", 403);
|
|
1793
1897
|
const generation = store.accountOf(who.name).generation;
|
|
1794
1898
|
if (action === "connect") {
|
|
@@ -1845,7 +1949,7 @@ ${postForm("/settings/chat-approval/save", html `\
|
|
|
1845
1949
|
savePrimary(dir, "discord");
|
|
1846
1950
|
return redirect(response, "/settings/discord");
|
|
1847
1951
|
}
|
|
1848
|
-
if (!authenticateApprover(who, body.get("password") ?? "").ok)
|
|
1952
|
+
if (!(action === "connect" || action === "pair" ? authenticateCredential(who, body.get("password") ?? "") : authenticateApprover(who, body.get("password") ?? "")).ok)
|
|
1849
1953
|
return show("Enter your Toolroll password to change Discord access.", 403);
|
|
1850
1954
|
const generation = store.accountOf(who.name).generation;
|
|
1851
1955
|
if (action === "connect") {
|
|
@@ -1984,6 +2088,8 @@ ${postForm("/settings/chat-approval/save", html `\
|
|
|
1984
2088
|
const provider = body.get("provider") ?? "";
|
|
1985
2089
|
if (!isProviderId(provider))
|
|
1986
2090
|
return refuse(response, who, 400, "unknown provider", "/settings");
|
|
2091
|
+
if (!isKeyedProvider(provider))
|
|
2092
|
+
return refuse(response, who, 400, `${ACP_AGENTS[provider].name} signs in its own way — Toolroll keeps no key for it`, "/settings");
|
|
1987
2093
|
if (op === "clear") {
|
|
1988
2094
|
const cleared = clearProviderKey(provider, providerHome);
|
|
1989
2095
|
const clearedMode = readAuthMode(provider, providerHome);
|
|
@@ -2167,7 +2273,8 @@ ${modeWords(terms)
|
|
|
2167
2273
|
return refuse(response, who, 409, "the terms moved while you were reading — read them again", "/mode");
|
|
2168
2274
|
}
|
|
2169
2275
|
const token = body.get("token") ?? "";
|
|
2170
|
-
|
|
2276
|
+
// A mode that lets a chat app approve opens a way to act: a credential act. Any other mode is an ordinary one.
|
|
2277
|
+
if (!(body.get("chat-approve") === "1" ? authenticateCredential(who, token) : authenticateApprover(who, token)).ok) {
|
|
2171
2278
|
return refuse(response, who, 403, "signing a mode takes your password, typed again", "/mode");
|
|
2172
2279
|
}
|
|
2173
2280
|
store.signMode({
|
|
@@ -2353,6 +2460,8 @@ ${modeWords(terms)
|
|
|
2353
2460
|
"settings.backups": backupsPage,
|
|
2354
2461
|
"settings.data": dataPage,
|
|
2355
2462
|
"settings.sign-in": signInPage,
|
|
2463
|
+
"settings.passwordless": passwordlessPage,
|
|
2464
|
+
"settings.passwordless-send": passwordlessSave,
|
|
2356
2465
|
"settings.lead": leadPage,
|
|
2357
2466
|
"settings.teams": teamsPage,
|
|
2358
2467
|
"settings.discord": discordPage,
|
|
@@ -2366,12 +2475,16 @@ ${modeWords(terms)
|
|
|
2366
2475
|
"settings.page": settingsHome,
|
|
2367
2476
|
"provider.resume": providerResume,
|
|
2368
2477
|
"settings.updates-dismiss": updatesDismiss,
|
|
2478
|
+
"session.forget-password": forgetPassword,
|
|
2369
2479
|
"settings.updates-checks": updatesChecks,
|
|
2370
2480
|
"settings.skills-revise": skillsRevise,
|
|
2371
2481
|
"settings.skills-import": skillsAct("import"),
|
|
2372
2482
|
"settings.skills-change": skillsAct("change"),
|
|
2373
2483
|
"spend.budget": spendBudget,
|
|
2374
2484
|
"settings.project-concurrency": projectConcurrency,
|
|
2485
|
+
"settings.project-live-view": projectLiveView,
|
|
2486
|
+
"settings.project-network": projectNetwork,
|
|
2487
|
+
"settings.project-writes": projectWrites,
|
|
2375
2488
|
"settings.project-delete": projectDelete,
|
|
2376
2489
|
"settings.policy-send": policySave,
|
|
2377
2490
|
"settings.approval-send": approvalSave,
|
|
@@ -2455,5 +2568,24 @@ ${modeWords(terms)
|
|
|
2455
2568
|
"edge.google-callback": googleCallback,
|
|
2456
2569
|
"edge.connect-callback": connectCallback,
|
|
2457
2570
|
});
|
|
2571
|
+
// Settings → Project → Where builds write (writes-mode.ts). An approver for the project changes it with their password
|
|
2572
|
+
// typed now; the ledger keeps before → after. New scopes use it; approved ones keep theirs.
|
|
2573
|
+
async function projectWrites(ctx) {
|
|
2574
|
+
const { who, response, now, posted } = ctx;
|
|
2575
|
+
const body = readForm(posted, CONSOLE_FORMS.projectWrites);
|
|
2576
|
+
const repo = body.get("repo") ?? "";
|
|
2577
|
+
if (!consoleProjects().includes(repo) || !visible(repo))
|
|
2578
|
+
return refuse(response, who, 403, "That project is outside your access.", "/settings/project");
|
|
2579
|
+
if (who.via !== "cookie" || who.role !== "approver" || store.isDemo() || !store.accountCanAccess(who.name, repo))
|
|
2580
|
+
return refuse(response, who, 403, "An approver for this project changes where its builds may write.", "/settings/project");
|
|
2581
|
+
const back = (key, words) => redirect(response, `/settings/project?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}#writes`);
|
|
2582
|
+
const wanted = body.get("writes");
|
|
2583
|
+
if (!isWritesMode(wanted))
|
|
2584
|
+
return back("problem", "Choose where builds may write. Nothing changed.");
|
|
2585
|
+
if (!authenticateCredential(who, body.get("password") ?? "").ok)
|
|
2586
|
+
return back("problem", "Enter your Toolroll password to change where builds may write.");
|
|
2587
|
+
const changed = store.setProjectWrites(repo, wanted, who.name, now);
|
|
2588
|
+
return back("said", changed.before === changed.after ? "Nothing changed." : "Saved. Tasks scoped from now on use it; tasks already approved keep what they were approved with.");
|
|
2589
|
+
}
|
|
2458
2590
|
return { registrations, modelsView };
|
|
2459
2591
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A lead sweep's table (docs/plans/lead-fan-out.md): every child with its state, its verdict and its result, read from
|
|
3
|
+
* facts on each look (the page follows the console's live beat). The only sweep-wide act is "cancel the rest"; steer,
|
|
4
|
+
* cancel or retry one child is that task's own page.
|
|
5
|
+
*/
|
|
6
|
+
import { CONSOLE_FORMS, readForm } from "../contracts/console-api.js";
|
|
7
|
+
import { html, postForm } from "../html.js";
|
|
8
|
+
import { cancelSweepRest, sweepLine, sweepOpen, sweepOpenDigest, sweepTable } from "../lead-sweep.js";
|
|
9
|
+
import { refuse, screen } from "./chrome.js";
|
|
10
|
+
import { handlersOf } from "./handler-registry.js";
|
|
11
|
+
import { redirect } from "./http.js";
|
|
12
|
+
const SWEEP_PATH = /^\/sweeps\/([1-9][0-9]{0,9})(?:\/cancel-rest)?$/;
|
|
13
|
+
const STATE_WORDS = {
|
|
14
|
+
working: "Working", checking: "Checking", "needs-decision": "Needs you", "ready-to-check": "Ready", complete: "Complete", cancelled: "Cancelled", missing: "Gone",
|
|
15
|
+
};
|
|
16
|
+
export function createSweepHandlers(runtime) {
|
|
17
|
+
const { store, clock, sendScreen, chromeFor, evidenceRoot, consoleProjects, liveRefreshSeconds } = runtime;
|
|
18
|
+
const readable = () => runtime.workAccess().repos ?? consoleProjects();
|
|
19
|
+
async function page(ctx) {
|
|
20
|
+
const { url, who, response } = ctx;
|
|
21
|
+
const id = Number(SWEEP_PATH.exec(url.pathname)?.[1]);
|
|
22
|
+
const table = sweepTable(store, id, clock(), readable(), evidenceRoot);
|
|
23
|
+
if (table === null)
|
|
24
|
+
return refuse(response, who, 404, "There's no sweep at this address you can see.", "/chat");
|
|
25
|
+
const said = url.searchParams.get("said");
|
|
26
|
+
const rows = table.rows.map(row => html `<tr data-sweep-row="${row.ordinal}">
|
|
27
|
+
<td>${row.ordinal + 1}</td>
|
|
28
|
+
<td><a href="/t/${encodeURIComponent(row.task)}">${row.label}</a></td>
|
|
29
|
+
<td><span class="chip">${STATE_WORDS[row.state]}</span></td>
|
|
30
|
+
<td>${row.verdict === null ? html `<span class="meta">—</span>` : html `<strong>${row.verdict.verdict}</strong> <span class="meta">${row.verdict.why}</span>`}</td>
|
|
31
|
+
<td>${row.result === null ? "" : html `<a href="/t/${encodeURIComponent(row.result.taskId)}">Result →</a>`}</td>
|
|
32
|
+
</tr>`);
|
|
33
|
+
// The form names exactly what it would act on (each item's active task, state and attempt): a change is refused.
|
|
34
|
+
const open = sweepOpen(store, table, clock());
|
|
35
|
+
const running = open.filter(one => one.run !== null).length;
|
|
36
|
+
const cancel = who.role !== "approver" || open.length === 0 ? "" : html `<details class="card"><summary>Cancel the rest (${open.length})</summary>${postForm(`/sweeps/${id}/cancel-rest`, html `<p>Cancels ${open.length - running} unfinished item${open.length - running === 1 ? "" : "s"}${running === 0 ? "" : ` and stops ${running} running build${running === 1 ? "" : "s"}`}: ${open.map(one => one.label).join(", ")}. Finished results stay.</p><label class="arm"><input type="checkbox" name="confirm" value="yes" required> Cancel the rest</label><button class="danger">Cancel the rest</button>`, { hidden: { snapshot: sweepOpenDigest(open) } })}</details>`;
|
|
37
|
+
const body = html `<h1>${table.title}</h1>${said === null ? "" : html `<p role="status">${said}</p>`}
|
|
38
|
+
<p class="meta">${sweepLine(table)} · at most ${table.maxParallel} at once · each item is its own task in its own worktree</p>
|
|
39
|
+
<table class="sweep-table"><thead><tr><th>#</th><th>Item</th><th>State</th><th>Verdict</th><th></th></tr></thead><tbody>${rows}</tbody></table>${cancel}`;
|
|
40
|
+
return sendScreen(response, 200, screen(table.title, body, { chrome: chromeFor(table.repo, "work"), refreshSeconds: liveRefreshSeconds() }));
|
|
41
|
+
}
|
|
42
|
+
async function cancelRest(ctx) {
|
|
43
|
+
const { url, who, response, posted } = ctx;
|
|
44
|
+
const id = Number(SWEEP_PATH.exec(url.pathname)?.[1]);
|
|
45
|
+
const body = readForm(posted, CONSOLE_FORMS.sweep);
|
|
46
|
+
const table = sweepTable(store, id, clock(), readable(), evidenceRoot);
|
|
47
|
+
if (table === null)
|
|
48
|
+
return refuse(response, who, 404, "There's no sweep at this address you can see.", "/chat");
|
|
49
|
+
if (body.get("confirm") !== "yes")
|
|
50
|
+
return redirect(response, `/sweeps/${id}?said=${encodeURIComponent("Tick the box to cancel the rest.")}`);
|
|
51
|
+
const now = clock();
|
|
52
|
+
const open = sweepOpen(store, table, now);
|
|
53
|
+
if (body.get("snapshot") !== sweepOpenDigest(open))
|
|
54
|
+
return redirect(response, `/sweeps/${id}?said=${encodeURIComponent("The sweep changed while you were reading. Look again, then cancel.")}`);
|
|
55
|
+
return redirect(response, `/sweeps/${id}?said=${encodeURIComponent(cancelSweepRest(store, who.name, open, now, "web"))}`);
|
|
56
|
+
}
|
|
57
|
+
return { registrations: handlersOf("tasks", { "sweep.page": page, "sweep.cancel-rest": cancelRest }) };
|
|
58
|
+
}
|
|
@@ -11,6 +11,7 @@ import { type ProjectPeek, type WorkRow } from "./render-pages.js";
|
|
|
11
11
|
import { type CompletionReceiptView, type ResultDetail, type TaskPullRequest } from "./render-results.js";
|
|
12
12
|
import { taskBody, type MilestoneProgressView, type PlanContractView, type PlanRevisionLedgerView, type RevisionDocView, type RevisionView, type RouteView } from "./render-tasks.js";
|
|
13
13
|
import { type Who } from "./session.js";
|
|
14
|
+
import type { TaskPageExtras } from "./runtime.js";
|
|
14
15
|
import type { PlanRevision, TaskRef } from "../store.js";
|
|
15
16
|
import { type Run, type Store, type Task, type TaskState } from "../store.js";
|
|
16
17
|
import { type ReviewFacts } from "../workspace-ui.js";
|
|
@@ -74,7 +75,7 @@ export declare function createTaskViews(runtime: TaskViewsRuntime): {
|
|
|
74
75
|
level: "quick" | "full";
|
|
75
76
|
returnTo: string;
|
|
76
77
|
} | null;
|
|
77
|
-
taskScreen: (response: ServerResponse, who: Who, taskId: string, problem: string | null, status: number, scopeDraft?: URLSearchParams, cancelDraft?: string, editPlan?: boolean) => void;
|
|
78
|
+
taskScreen: (response: ServerResponse, who: Who, taskId: string, problem: string | null, status: number, scopeDraft?: URLSearchParams, cancelDraft?: string, editPlan?: boolean, extras?: TaskPageExtras) => void;
|
|
78
79
|
armTaskResume: (response: ServerResponse, who: Who, taskId: string, named: string, returnTo: "chat" | "task", now: Date) => void;
|
|
79
80
|
planViewOf: (taskRef: number) => {
|
|
80
81
|
document: string;
|