toolroll 0.9.62 → 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.
- package/CHANGELOG.md +80 -0
- package/README.md +66 -8
- 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 +203 -10
- package/dist/agent-fence.js +623 -41
- 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 +36 -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/browser/THIRD_PARTY_NOTICES.txt +48 -0
- package/dist/browser/terminal.css +37 -0
- package/dist/browser/terminal.js +117 -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 +263 -1
- package/dist/browser-workspace.js +60 -0
- package/dist/builder.d.ts +6 -1
- package/dist/builder.js +34 -13
- package/dist/chat-actions.d.ts +13 -1
- package/dist/chat-actions.js +199 -17
- 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/check-levels.js +3 -0
- 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 +20 -1
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +5 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +24 -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/cli.d.ts +8 -0
- package/dist/contracts/cli.js +3 -1
- package/dist/contracts/console-api.d.ts +30 -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/integration-metadata.d.ts +2 -1
- package/dist/contracts/integration-metadata.js +2 -1
- 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/discord-chat.js +4 -1
- package/dist/dispatch.js +7 -4
- package/dist/doctor.d.ts +53 -0
- package/dist/doctor.js +427 -0
- 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/evidence-pack.d.ts +1 -1
- package/dist/evidence-pack.js +2 -2
- package/dist/exec.d.ts +81 -0
- package/dist/exec.js +368 -9
- package/dist/export-ui.js +2 -2
- package/dist/flow-code.d.ts +2 -2
- package/dist/flow-code.js +4 -15
- package/dist/flow-engine.js +35 -11
- 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 +84 -11
- package/dist/flow-triggers.js +554 -87
- package/dist/flow-unattended-words.d.ts +28 -0
- package/dist/flow-unattended-words.js +50 -0
- package/dist/flow-unattended.d.ts +227 -0
- package/dist/flow-unattended.js +776 -0
- package/dist/flows-cli.d.ts +28 -1
- package/dist/flows-cli.js +63 -12
- package/dist/flows-ui.d.ts +2 -3
- package/dist/flows-ui.js +56 -5
- package/dist/flows.d.ts +4 -0
- package/dist/flows.js +9 -1
- package/dist/google-mail.d.ts +1 -0
- package/dist/google-mail.js +61 -18
- package/dist/html.d.ts +9 -0
- package/dist/html.js +11 -0
- package/dist/integrations-ui.d.ts +2 -1
- package/dist/integrations-ui.js +6 -3
- package/dist/integrations.d.ts +17 -3
- package/dist/integrations.js +38 -18
- package/dist/invoke.d.ts +17 -0
- package/dist/invoke.js +510 -182
- package/dist/keys.d.ts +18 -14
- package/dist/keys.js +5 -0
- package/dist/landed-cleanup.d.ts +3 -2
- package/dist/landed-cleanup.js +12 -5
- 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 +163 -4
- 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 +258 -0
- package/dist/live-terminal.js +649 -0
- package/dist/live.d.ts +38 -27
- package/dist/live.js +142 -124
- package/dist/mcp-connect.d.ts +8 -7
- package/dist/mcp-connect.js +49 -51
- 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/oauth-run-token.d.ts +51 -0
- package/dist/oauth-run-token.js +127 -0
- package/dist/operate.d.ts +2 -0
- package/dist/operate.js +282 -18
- 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/plan-auto.d.ts +11 -0
- package/dist/plan-auto.js +62 -11
- 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/process-recovery-eligibility.d.ts +1 -1
- package/dist/project-access.d.ts +3 -0
- package/dist/project-access.js +3 -0
- package/dist/project-delete-ui.d.ts +12 -0
- package/dist/project-delete-ui.js +41 -9
- package/dist/project-delete.d.ts +3 -1
- package/dist/project-delete.js +12 -6
- package/dist/project-file.d.ts +3 -0
- package/dist/project-file.js +16 -0
- package/dist/project-memory.d.ts +3 -1
- package/dist/project-memory.js +57 -25
- package/dist/project-tools.d.ts +103 -27
- package/dist/project-tools.js +399 -100
- 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-home.d.ts +22 -0
- package/dist/provider-home.js +99 -0
- package/dist/provider.d.ts +42 -5
- package/dist/provider.js +97 -19
- 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.d.ts +3 -0
- package/dist/release.js +14 -8
- package/dist/remote-main.d.ts +4 -0
- package/dist/remote-main.js +3 -3
- package/dist/request-budget-ui.js +2 -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 +171 -6
- package/dist/scope.js +251 -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 +258 -43
- package/dist/server/chat.d.ts +1 -1
- package/dist/server/chat.js +122 -31
- package/dist/server/chrome.d.ts +6 -1
- package/dist/server/chrome.js +18 -7
- package/dist/server/console.css +52 -38
- package/dist/server/flows.js +65 -13
- 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 +4 -1
- package/dist/server/render-pages.js +5 -4
- 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 +110 -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 +49 -5
- package/dist/server/runtime.d.ts +34 -3
- package/dist/server/session.d.ts +25 -1
- package/dist/server/session.js +18 -4
- package/dist/server/settings.js +132 -13
- 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 +320 -74
- 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-sweep-child.js +3 -1
- package/dist/storage-sweep-process.d.ts +12 -5
- package/dist/storage-sweep-process.js +23 -6
- 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 +466 -19
- package/dist/store.js +1185 -63
- package/dist/subagent-desk.js +1 -1
- package/dist/subagent-tools.d.ts +1 -1
- package/dist/summary.js +3 -0
- package/dist/surface.js +6 -4
- 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 +14 -0
- package/dist/teardown-git.js +119 -0
- 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 +39 -0
- package/dist/tool-broker.js +370 -0
- package/dist/tool-launcher.js +5 -4
- package/dist/tool-secret-store.d.ts +170 -0
- package/dist/tool-secret-store.js +834 -0
- package/dist/toolroll-update-ui.js +2 -2
- package/dist/toolroll-update.js +14 -3
- package/dist/tools-ui.d.ts +7 -1
- package/dist/tools-ui.js +19 -10
- 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 +21 -5
- package/dist/workspace-revision.js +3 -1
- package/dist/worktree.d.ts +10 -4
- package/dist/worktree.js +70 -29
- package/package.json +13 -2
- package/scripts/provider-canary.mjs +210 -31
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,13 +79,15 @@ 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";
|
|
79
83
|
import { projectDeleteConfirmHtml, projectSettingsHtml } from "../project-delete-ui.js";
|
|
80
84
|
import { deleteProject, holdingsWords } from "../project-delete.js";
|
|
81
85
|
import { applySavedKnowledge, changeKnowledge, knowledgeVersion, knowledgeView } from "../project-knowledge.js";
|
|
82
86
|
import { changeLearning } from "../project-learning.js";
|
|
83
87
|
import { listDecisions, recordDecision, retireDecision, searchMemory } from "../project-memory.js";
|
|
84
88
|
import { changeSkills, githubSkill, importSkill, reviseSkillTest, skillsView, testSkill } from "../project-skills.js";
|
|
85
|
-
import {
|
|
89
|
+
import { signedIn } from "../oauth-run-token.js";
|
|
90
|
+
import { addToolTo, catalogTool, localAppOf, removeToolFrom, setToolSecret, splitCommandLine, testToolOf, toolSecretStorage } from "../project-tools.js";
|
|
86
91
|
import { liftAuthPause } from "../provider-auth.js";
|
|
87
92
|
import { isProviderId, PROVIDER_IDS, validateSpec, validModelId } from "../provider.js";
|
|
88
93
|
import { checkPublishing, MERGE_METHODS, saveMergeSettings, savePublishing } from '../pull-request-flow.js';
|
|
@@ -121,7 +126,7 @@ import { personChip, ROLE_TITLES } from "./chrome.js";
|
|
|
121
126
|
import { chatReturnWithSaid, goOutside } from "./http.js";
|
|
122
127
|
import { latestReleaseFor, leadSettingsHtml, notificationProjects, SETTINGS_AUTOSAVE_SCRIPT, settingsPage, settingsWorkers, telegramDeliveryWords, telegramTrouble } from "./render-settings.js";
|
|
123
128
|
export function createSettingsHandlers(runtime) {
|
|
124
|
-
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;
|
|
129
|
+
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;
|
|
125
130
|
// v105: what agent work cost, by month, and the monthly budgets. An instance operator's page.
|
|
126
131
|
async function spendPage(ctx) {
|
|
127
132
|
const { url, who, response, now } = ctx;
|
|
@@ -350,7 +355,7 @@ ${connectionHtml(asked, connection, task)}${command === null ? "" : html `<p>On
|
|
|
350
355
|
if (!options.configDir)
|
|
351
356
|
return refuse(response, who, 403, "An installation approver sees integrations.", "/settings");
|
|
352
357
|
const list = integrations.list();
|
|
353
|
-
return sendScreen(response, 200, screen("Integrations", html `<p><a href="/settings">Settings</a></p><h1>Integrations</h1>${(integrationsHtml(list, who.session.csrf, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem"), checking: integrations.checking }))}`, { chrome: chromeFor(project, "settings") }));
|
|
358
|
+
return sendScreen(response, 200, screen("Integrations", html `<p><a href="/settings">Settings</a></p><h1>Integrations</h1>${(integrationsHtml(list, who.session.csrf, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem"), checking: integrations.checking, secrets: toolSecretStorage() }))}`, { chrome: chromeFor(project, "settings") }));
|
|
354
359
|
}
|
|
355
360
|
// v104: Settings → Monitoring, where the audit stream and traces go. An instance operator's page.
|
|
356
361
|
async function monitoringPage(ctx) {
|
|
@@ -460,6 +465,40 @@ ${connectionHtml(asked, connection, task)}${command === null ? "" : html `<p>On
|
|
|
460
465
|
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") });
|
|
461
466
|
return sendScreen(response, 200, screen("Sign-in", html `<p><a href="/settings">Settings</a></p><h1>Sign-in</h1>${body}`, { chrome: chromeFor(project, "settings") }));
|
|
462
467
|
}
|
|
468
|
+
/** Passwordless (docs/plans/passwordless.md): the switch, and the trade in plain words. */
|
|
469
|
+
async function passwordlessPage(ctx) {
|
|
470
|
+
const { url, response, project } = ctx;
|
|
471
|
+
browserCaller(ctx.who);
|
|
472
|
+
const setting = passwordlessSetting(store);
|
|
473
|
+
const said = url.searchParams.get("said"), problem = url.searchParams.get("problem");
|
|
474
|
+
const note = problem ? html `<p class="problem" role="alert">${problem}</p>` : said ? html `<p role="status">${said}</p>` : "";
|
|
475
|
+
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>`;
|
|
476
|
+
const control = setting.on
|
|
477
|
+
? postForm("/settings/passwordless", html `<button class="secondary">Turn Passwordless off</button>`, { hidden: { action: "off" } })
|
|
478
|
+
: 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" } });
|
|
479
|
+
return sendScreen(response, 200, screen("Passwordless", html `<p><a href="/settings">Settings</a></p><h1>Passwordless</h1>${note}\
|
|
480
|
+
<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>\
|
|
481
|
+
<p class="meta">${PASSWORDLESS_TRADE}</p>\
|
|
482
|
+
<p><strong>Passwordless is ${setting.on ? "on" : "off"}.</strong>${since}</p>${control}`, { chrome: chromeFor(project, "settings") }));
|
|
483
|
+
}
|
|
484
|
+
async function passwordlessSave(ctx) {
|
|
485
|
+
const { response, now, posted } = ctx;
|
|
486
|
+
const who = browserCaller(ctx.who);
|
|
487
|
+
const body = readForm(posted, CONSOLE_FORMS.passwordless);
|
|
488
|
+
const back = (key, words) => redirect(response, `/settings/passwordless?${key}=${encodeURIComponent(words)}`);
|
|
489
|
+
const action = body.get("action");
|
|
490
|
+
if (action === "off") {
|
|
491
|
+
setPasswordless(store, false, who.name, now);
|
|
492
|
+
return back("said", "Passwordless is off. Step-ups ask for your password again.");
|
|
493
|
+
}
|
|
494
|
+
if (action !== "on")
|
|
495
|
+
return back("problem", "Choose on or off.");
|
|
496
|
+
// Turning it on is a credential act: a password typed now (or a one-use provider re-check), never a session.
|
|
497
|
+
if (!authenticateCredential(who, body.get("password") ?? "").ok)
|
|
498
|
+
return back("problem", "Enter your Toolroll password to turn Passwordless on.");
|
|
499
|
+
setPasswordless(store, true, who.name, now);
|
|
500
|
+
return back("said", "Passwordless is on. Signing in is the only password now.");
|
|
501
|
+
}
|
|
463
502
|
async function knowledgePage(ctx) {
|
|
464
503
|
const { url, who, response, project } = ctx;
|
|
465
504
|
const projects = consoleProjects();
|
|
@@ -599,13 +638,14 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
|
|
|
599
638
|
};
|
|
600
639
|
const providerKeys = who.role !== "approver"
|
|
601
640
|
? null
|
|
602
|
-
|
|
641
|
+
// ACP agents sign in their own way; Toolroll keeps no key for them, so the key card lists only the others.
|
|
642
|
+
: await Promise.all(PROVIDER_IDS.filter(isKeyedProvider).map(async (provider) => ({
|
|
603
643
|
provider,
|
|
604
644
|
envName: PROVIDER_KEY_ENV[provider],
|
|
605
645
|
...keyStatus(provider, providerHome),
|
|
606
646
|
connection: await connectionCheck(provider, url.searchParams.get("check-connection") === provider),
|
|
607
647
|
ambient: (process.env[PROVIDER_KEY_ENV[provider]] ?? "") !== "",
|
|
608
|
-
mode: readAuthMode(provider, providerHome),
|
|
648
|
+
mode: readAuthMode(provider, providerHome) === "api-key" ? "api-key" : "subscription",
|
|
609
649
|
subscriptionCapable: SUBSCRIPTION_CAPABLE[provider],
|
|
610
650
|
})));
|
|
611
651
|
const telegramConfigured = loadBotToken(process.env, options.telegramTokenFile) !== null;
|
|
@@ -624,7 +664,12 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
|
|
|
624
664
|
}, who.role === "approver" && csrf !== "" ? (() => {
|
|
625
665
|
// v87: the mail server flows send email through; the password is never shown back.
|
|
626
666
|
// v89: where Inbox triggers read it, and a connected Google account (its client secret is never shown back either).
|
|
627
|
-
const email = readEmailSettings(options.configDir ?? null), google =
|
|
667
|
+
const email = readEmailSettings(options.configDir ?? null), google = (() => { try {
|
|
668
|
+
return readGoogleMail(options.configDir ?? null);
|
|
669
|
+
}
|
|
670
|
+
catch {
|
|
671
|
+
return null;
|
|
672
|
+
} })(), origin = consoleOrigin(request.headers.host);
|
|
628
673
|
const googleView = { connected: googleConnected(options.configDir ?? null)?.address ?? null, clientId: google?.clientId ?? "", redirect: origin === null ? null : `${origin}${GOOGLE_CALLBACK}` };
|
|
629
674
|
return email === null ? { set: false, host: "", port: 587, secure: false, user: "", from: "", imapHost: "", imapPort: 993, google: googleView }
|
|
630
675
|
: { set: true, host: email.host, port: email.port, secure: email.secure, user: email.user, from: email.from, imapHost: email.imap?.host ?? "", imapPort: email.imap?.port ?? 993, google: googleView };
|
|
@@ -650,6 +695,18 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
|
|
|
650
695
|
return redirect(response, "/work");
|
|
651
696
|
}
|
|
652
697
|
// The update notice is dismissed per browser and per version: a newer release shows it again.
|
|
698
|
+
// Cockpit B2: Forget — nothing verified on this session before now counts as fresh (a CSRF-checked post, like every act).
|
|
699
|
+
async function forgetPassword(ctx) {
|
|
700
|
+
const { who, response, posted } = ctx;
|
|
701
|
+
const body = readForm(posted, CONSOLE_FORMS.forgetPassword);
|
|
702
|
+
if (who.via === "cookie") {
|
|
703
|
+
who.session.freshFloor = Date.now();
|
|
704
|
+
delete who.session.passwordAt;
|
|
705
|
+
delete who.session.ssoReauthAt;
|
|
706
|
+
delete who.session.ssoNowAt;
|
|
707
|
+
}
|
|
708
|
+
return redirect(response, safeReturn(body.get("return") ?? "/"));
|
|
709
|
+
}
|
|
653
710
|
async function updatesDismiss(ctx) {
|
|
654
711
|
const { who, response, posted } = ctx;
|
|
655
712
|
const body = readForm(posted, CONSOLE_FORMS.updatesDismiss);
|
|
@@ -802,6 +859,51 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
|
|
|
802
859
|
store.bumpWake();
|
|
803
860
|
return back("said", changed.before === changed.after ? "Nothing changed." : `Saved. Up to ${n} at once.`);
|
|
804
861
|
}
|
|
862
|
+
// Settings → Project → Live view (live-detail.ts): an approver for the project shows commands with their password,
|
|
863
|
+
// or hides them in one step; the ledger keeps each change. It applies from the next run.
|
|
864
|
+
async function projectLiveView(ctx) {
|
|
865
|
+
const { who, response, now, posted } = ctx;
|
|
866
|
+
const body = readForm(posted, CONSOLE_FORMS.projectLiveView);
|
|
867
|
+
const repo = body.get("repo") ?? "";
|
|
868
|
+
if (!consoleProjects().includes(repo) || !visible(repo))
|
|
869
|
+
return refuse(response, who, 403, "That project is outside your access.", "/settings/project");
|
|
870
|
+
if (who.via !== "cookie" || who.role !== "approver" || store.isDemo() || !store.accountCanAccess(who.name, repo))
|
|
871
|
+
return refuse(response, who, 403, "An approver for this project changes the live view.", "/settings/project");
|
|
872
|
+
const back = (key, words) => redirect(response, `/settings/project?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}`);
|
|
873
|
+
const act = body.get("act");
|
|
874
|
+
if (act === "off")
|
|
875
|
+
return back("said", setLiveCommands(store, { repo, commands: false, by: who.name, via: "the console" }, now).said);
|
|
876
|
+
if (act !== "on")
|
|
877
|
+
return back("problem", "That change isn't available.");
|
|
878
|
+
// Single use, for this person and exactly this change: a replayed or copied form changes nothing.
|
|
879
|
+
if (!consumeApprovalNonce(body.get("nonce") ?? "", who.name, LIVE_VIEW_CONSENT, liveViewConsentDigest(repo)))
|
|
880
|
+
return back("problem", "That form was already used or is out of date. Try again from this page.");
|
|
881
|
+
if (!authenticateApprover(who, body.get("password") ?? "", repo).ok)
|
|
882
|
+
return back("problem", "Enter your Toolroll password to show commands.");
|
|
883
|
+
return back("said", setLiveCommands(store, { repo, commands: true, by: who.name, via: "the console" }, now).said);
|
|
884
|
+
}
|
|
885
|
+
// Settings → Project → Network (egress.ts): what the project's builds may reach. An approver for the project changes it
|
|
886
|
+
// with their password typed now; the ledger keeps before → after. New scopes use it; approved ones keep theirs.
|
|
887
|
+
async function projectNetwork(ctx) {
|
|
888
|
+
const { who, response, now, posted } = ctx;
|
|
889
|
+
const body = readForm(posted, CONSOLE_FORMS.projectNetwork);
|
|
890
|
+
const repo = body.get("repo") ?? "";
|
|
891
|
+
if (!consoleProjects().includes(repo) || !visible(repo))
|
|
892
|
+
return refuse(response, who, 403, "That project is outside your access.", "/settings/project");
|
|
893
|
+
if (who.via !== "cookie" || who.role !== "approver" || store.isDemo() || !store.accountCanAccess(who.name, repo))
|
|
894
|
+
return refuse(response, who, 403, "An approver for this project changes what its builds may reach.", "/settings/project");
|
|
895
|
+
const back = (key, words) => redirect(response, `/settings/project?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}#network`);
|
|
896
|
+
const hosts = (body.get("hosts") ?? "").split(/[\s,]+/).filter(Boolean).map(normalizeHost);
|
|
897
|
+
if (hosts.some(one => one === null))
|
|
898
|
+
return back("problem", "A host is a name like api.stripe.com or *.stripe.com: no address, port or path. Nothing changed.");
|
|
899
|
+
if (hosts.length > MAX_NETWORK_HOSTS)
|
|
900
|
+
return back("problem", `A project lists at most ${MAX_NETWORK_HOSTS} hosts. Nothing changed.`);
|
|
901
|
+
if (!authenticateCredential(who, body.get("password") ?? "").ok)
|
|
902
|
+
return back("problem", "Enter your Toolroll password to change what builds may reach.");
|
|
903
|
+
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);
|
|
904
|
+
const same = JSON.stringify(changed.before) === JSON.stringify(changed.after);
|
|
905
|
+
return back("said", same ? "Nothing changed." : "Saved. Tasks scoped from now on use it; tasks already approved keep the network they were approved with.");
|
|
906
|
+
}
|
|
805
907
|
// Delete a project, in two steps: its name, then what goes and the password. An instance operator; the ledger keeps who and what.
|
|
806
908
|
async function projectDelete(ctx) {
|
|
807
909
|
const { response, now, posted } = ctx;
|
|
@@ -911,7 +1013,8 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
|
|
|
911
1013
|
const found = await providerFor(before.issuer);
|
|
912
1014
|
return found.ok ? back("said", `${before.label} answers: it's ready for people to sign in.`) : back("problem", found.said);
|
|
913
1015
|
}
|
|
914
|
-
|
|
1016
|
+
// Sign-in methods and the identity provider are the login itself: a credential act, a password typed now.
|
|
1017
|
+
if (!authenticateCredential(who, body.get("password") ?? "").ok)
|
|
915
1018
|
return back("problem", "Enter your Toolroll password to change sign-in.");
|
|
916
1019
|
if (action === "remove") {
|
|
917
1020
|
removeSsoSettings(options.configDir);
|
|
@@ -1397,6 +1500,8 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
|
|
|
1397
1500
|
const secret = body.get("secret") ?? "";
|
|
1398
1501
|
if (!tool.spec.secrets.some(one => one.name === secret))
|
|
1399
1502
|
return back("problem", "Choose one of this tool's secrets.");
|
|
1503
|
+
if (signedIn(tool.spec))
|
|
1504
|
+
return back("problem", `${name} is connected by signing in. Sign in again from Connect to renew it.`, `#tool-${name}`);
|
|
1400
1505
|
setToolSecret(repo, name, secret, body.get("value") ?? "", toolHome);
|
|
1401
1506
|
return back("said", `Saved ${secret} for ${name}. It is never shown again.`, `#tool-${name}`);
|
|
1402
1507
|
}
|
|
@@ -1603,7 +1708,7 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
|
|
|
1603
1708
|
const nonceKey = `project-instructions:${project}`;
|
|
1604
1709
|
if (step === "preview") {
|
|
1605
1710
|
const nonce = mintApprovalNonce(who.name, nonceKey, preview.fingerprint);
|
|
1606
|
-
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
|
|
1711
|
+
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") }));
|
|
1607
1712
|
}
|
|
1608
1713
|
if (who.via === "cookie" && !consumeApprovalNonce(body.get("nonce") ?? "", who.name, nonceKey, body.get("fingerprint") ?? ""))
|
|
1609
1714
|
return refuse(response, who, 409, "This instruction preview expired. Review it again.", "/control");
|
|
@@ -1647,7 +1752,8 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
|
|
|
1647
1752
|
savePrimary(dir, "slack");
|
|
1648
1753
|
return redirect(response, "/settings/slack");
|
|
1649
1754
|
}
|
|
1650
|
-
|
|
1755
|
+
// Connecting or pairing opens a way to act from Slack: a credential act. Unpairing or disconnecting only closes one.
|
|
1756
|
+
if (!(action === "connect" || action === "pair" ? authenticateCredential(who, body.get("password") ?? "") : authenticateApprover(who, body.get("password") ?? "")).ok)
|
|
1651
1757
|
return show("Enter your Toolroll password to change Slack access.", 403);
|
|
1652
1758
|
const generation = store.accountOf(who.name).generation;
|
|
1653
1759
|
if (action === "connect") {
|
|
@@ -1750,6 +1856,11 @@ ${postForm("/settings/chat-approval/save", html `\
|
|
|
1750
1856
|
return show("Submit one value for each field.");
|
|
1751
1857
|
if (botId === null)
|
|
1752
1858
|
return show("Connect the Telegram bot first.", 409);
|
|
1859
|
+
// Pairing a phone is a credential act (a typed password, always); unpairing only closes a way in.
|
|
1860
|
+
if (op === "unpair" && authenticateApprover(who, body.get("password") ?? "").ok) {
|
|
1861
|
+
store.unpairTelegram(botId, who.name, now);
|
|
1862
|
+
return redirect(response, "/settings/telegram");
|
|
1863
|
+
}
|
|
1753
1864
|
const pairingIdentity = authenticateAccount(store, who.name, body.get("password") ?? "");
|
|
1754
1865
|
if (!pairingIdentity.ok || pairingIdentity.role !== "approver" || pairingIdentity.generation !== who.session.generation)
|
|
1755
1866
|
return show("Enter your Toolroll password to change your phone pairing.", 403);
|
|
@@ -1780,7 +1891,7 @@ ${postForm("/settings/chat-approval/save", html `\
|
|
|
1780
1891
|
savePrimary(dir, "teams");
|
|
1781
1892
|
return redirect(response, "/settings/teams");
|
|
1782
1893
|
}
|
|
1783
|
-
if (!authenticateApprover(who, body.get("password") ?? "").ok)
|
|
1894
|
+
if (!(action === "connect" || action === "pair" ? authenticateCredential(who, body.get("password") ?? "") : authenticateApprover(who, body.get("password") ?? "")).ok)
|
|
1784
1895
|
return show("Enter your Toolroll password to change Teams access.", 403);
|
|
1785
1896
|
const generation = store.accountOf(who.name).generation;
|
|
1786
1897
|
if (action === "connect") {
|
|
@@ -1837,7 +1948,7 @@ ${postForm("/settings/chat-approval/save", html `\
|
|
|
1837
1948
|
savePrimary(dir, "discord");
|
|
1838
1949
|
return redirect(response, "/settings/discord");
|
|
1839
1950
|
}
|
|
1840
|
-
if (!authenticateApprover(who, body.get("password") ?? "").ok)
|
|
1951
|
+
if (!(action === "connect" || action === "pair" ? authenticateCredential(who, body.get("password") ?? "") : authenticateApprover(who, body.get("password") ?? "")).ok)
|
|
1841
1952
|
return show("Enter your Toolroll password to change Discord access.", 403);
|
|
1842
1953
|
const generation = store.accountOf(who.name).generation;
|
|
1843
1954
|
if (action === "connect") {
|
|
@@ -1976,6 +2087,8 @@ ${postForm("/settings/chat-approval/save", html `\
|
|
|
1976
2087
|
const provider = body.get("provider") ?? "";
|
|
1977
2088
|
if (!isProviderId(provider))
|
|
1978
2089
|
return refuse(response, who, 400, "unknown provider", "/settings");
|
|
2090
|
+
if (!isKeyedProvider(provider))
|
|
2091
|
+
return refuse(response, who, 400, `${ACP_AGENTS[provider].name} signs in its own way — Toolroll keeps no key for it`, "/settings");
|
|
1979
2092
|
if (op === "clear") {
|
|
1980
2093
|
const cleared = clearProviderKey(provider, providerHome);
|
|
1981
2094
|
const clearedMode = readAuthMode(provider, providerHome);
|
|
@@ -2159,7 +2272,8 @@ ${modeWords(terms)
|
|
|
2159
2272
|
return refuse(response, who, 409, "the terms moved while you were reading — read them again", "/mode");
|
|
2160
2273
|
}
|
|
2161
2274
|
const token = body.get("token") ?? "";
|
|
2162
|
-
|
|
2275
|
+
// A mode that lets a chat app approve opens a way to act: a credential act. Any other mode is an ordinary one.
|
|
2276
|
+
if (!(body.get("chat-approve") === "1" ? authenticateCredential(who, token) : authenticateApprover(who, token)).ok) {
|
|
2163
2277
|
return refuse(response, who, 403, "signing a mode takes your password, typed again", "/mode");
|
|
2164
2278
|
}
|
|
2165
2279
|
store.signMode({
|
|
@@ -2345,6 +2459,8 @@ ${modeWords(terms)
|
|
|
2345
2459
|
"settings.backups": backupsPage,
|
|
2346
2460
|
"settings.data": dataPage,
|
|
2347
2461
|
"settings.sign-in": signInPage,
|
|
2462
|
+
"settings.passwordless": passwordlessPage,
|
|
2463
|
+
"settings.passwordless-send": passwordlessSave,
|
|
2348
2464
|
"settings.lead": leadPage,
|
|
2349
2465
|
"settings.teams": teamsPage,
|
|
2350
2466
|
"settings.discord": discordPage,
|
|
@@ -2358,12 +2474,15 @@ ${modeWords(terms)
|
|
|
2358
2474
|
"settings.page": settingsHome,
|
|
2359
2475
|
"provider.resume": providerResume,
|
|
2360
2476
|
"settings.updates-dismiss": updatesDismiss,
|
|
2477
|
+
"session.forget-password": forgetPassword,
|
|
2361
2478
|
"settings.updates-checks": updatesChecks,
|
|
2362
2479
|
"settings.skills-revise": skillsRevise,
|
|
2363
2480
|
"settings.skills-import": skillsAct("import"),
|
|
2364
2481
|
"settings.skills-change": skillsAct("change"),
|
|
2365
2482
|
"spend.budget": spendBudget,
|
|
2366
2483
|
"settings.project-concurrency": projectConcurrency,
|
|
2484
|
+
"settings.project-live-view": projectLiveView,
|
|
2485
|
+
"settings.project-network": projectNetwork,
|
|
2367
2486
|
"settings.project-delete": projectDelete,
|
|
2368
2487
|
"settings.policy-send": policySave,
|
|
2369
2488
|
"settings.approval-send": approvalSave,
|
|
@@ -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;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { approvalProofOf, freshlyApprovable, freshnessFor, withFreshness } from "../scope.js";
|
|
2
|
+
import { liveAttemptOf, steerControlsFor } from "../steer-controls.js";
|
|
3
|
+
import { originOf } from "../task-source.js";
|
|
1
4
|
import { taskBaseOf } from "../task-base.js";
|
|
2
5
|
import { assignmentPresentationOf, historicalAssessmentReason } from '../assignment-presentation.js';
|
|
3
|
-
import { html, joinHtml } from "../html.js";
|
|
6
|
+
import { html, htmlString, joinHtml } from "../html.js";
|
|
4
7
|
import { assignmentStatusOf, assignmentWithEvidence } from '../assignment-ui.js';
|
|
5
8
|
import { assignmentOf } from '../assignment.js';
|
|
6
9
|
import { knowledgeContextHtml } from "../knowledge-ui.js";
|
|
@@ -18,7 +21,7 @@ import { taskFingerprint } from "../task-live.js";
|
|
|
18
21
|
import { pullRequestFactOf } from '../task-status.js';
|
|
19
22
|
import { openWorkDecisionOf } from "../work-summary.js";
|
|
20
23
|
import { learningHtml } from "../workspace-ui.js";
|
|
21
|
-
import { randomBytes } from "node:crypto";
|
|
24
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
22
25
|
import {} from "node:http";
|
|
23
26
|
import { agentChoicesFor, routeOfTask } from "../agentconfig.js";
|
|
24
27
|
import { rulesSummary } from "../approval-rules-ui.js";
|
|
@@ -41,6 +44,8 @@ import {} from "./render-pages.js";
|
|
|
41
44
|
import { completionReceiptView, diffFileAnchor, evidenceLinksFor, followUpsFor, orderChangedFiles, parseReviewDiff, proofBundleView, receiptStatusOf, withinSignedTouches } from "./render-results.js";
|
|
42
45
|
import { approvalFormDigest, consentDoorOf, nonceHashOf, publicationFactsOf, resumeCeremonyPage, resumeDigestOf, taskBody, taskPage } from "./render-tasks.js";
|
|
43
46
|
import {} from "./session.js";
|
|
47
|
+
import { RESULT_REVIEW_SCRIPT } from "../result-review.js";
|
|
48
|
+
import { reviewEvidenceScript } from "./render-results.js";
|
|
44
49
|
import { budgetHoldWords, budgetLabel, monthOf } from "../spend.js";
|
|
45
50
|
import {} from "../store.js";
|
|
46
51
|
import { taskControlOf } from "../task-control.js";
|
|
@@ -367,6 +372,8 @@ export function createTaskViews(runtime) {
|
|
|
367
372
|
repairChain: completion !== null ? null : runtime.store.repairChainForDraft(taskId),
|
|
368
373
|
repo: ref?.repo ?? null,
|
|
369
374
|
filedVia: runtime.store.filedViaOf(taskId),
|
|
375
|
+
// v126: where the work came from (a flow, an outside event), from what was kept when it was filed.
|
|
376
|
+
origin: (() => { const repos = runtime.workAccess().repos; return originOf(runtime.store, taskId, repo => repos === null || repos.includes(repo)); })(),
|
|
370
377
|
coordinator: (() => {
|
|
371
378
|
const who = runtime.store.coordinatorProvenanceOf(taskId);
|
|
372
379
|
if (who === null)
|
|
@@ -485,6 +492,31 @@ export function createTaskViews(runtime) {
|
|
|
485
492
|
steering: ref === null ? [] : runtime.store.listSteerNotes(ref.id),
|
|
486
493
|
base: taskBaseOf(runtime.store, taskId),
|
|
487
494
|
steeringLive: ref !== null && runtime.store.steeringLiveBuild(ref.id, now) !== null,
|
|
495
|
+
steeringRun: ref === null ? null : runtime.store.steeringLiveBuild(ref.id, now),
|
|
496
|
+
approvalProof: (() => {
|
|
497
|
+
// The proof of the approval shown: its approver, its exact sealed digest (approvedDigest === digest when approved).
|
|
498
|
+
const scope = ref === null ? null : runtime.store.getScope(ref.externalId);
|
|
499
|
+
return scope === null || scope.approvedBy === null || scope.approvedDigest === null ? null
|
|
500
|
+
: approvalProofOf(runtime.store, ref.externalId, scope.approvedBy, scope.approvedDigest);
|
|
501
|
+
})(),
|
|
502
|
+
// Cockpit B2: whether this person's remembered password may approve these exact terms (the same rule approve()
|
|
503
|
+
// applies), and since when.
|
|
504
|
+
approvalFresh: (() => {
|
|
505
|
+
const scope = ref === null ? null : runtime.store.getScope(ref.externalId);
|
|
506
|
+
if (scope === null || who?.via !== "cookie")
|
|
507
|
+
return null;
|
|
508
|
+
const fresh = withFreshness("approve-standard", () => freshnessFor(who.name, Date.now(), true));
|
|
509
|
+
if (fresh === null)
|
|
510
|
+
return null;
|
|
511
|
+
const gate = runtime.store.approvalGate(ref.externalId, who.name, "person");
|
|
512
|
+
return freshlyApprovable(runtime.store, ref.externalId, scope, gate.verdict === "seal" && !gate.protectedWork) ? new Date(fresh.at).toISOString() : null;
|
|
513
|
+
})(),
|
|
514
|
+
steerRecent: ref === null ? [] : runtime.store.runsFor(ref.id).filter(one => one.outcome !== null).map(one => one.id).sort((a, b) => b - a).slice(0, 5),
|
|
515
|
+
// The running attempt's composer, for an approver on a browser session (steer-controls.ts).
|
|
516
|
+
steerComposer: (() => {
|
|
517
|
+
const run = ref === null ? null : liveAttemptOf(runtime.store, ref.id, now);
|
|
518
|
+
return run === null ? null : steerControlsFor(runtime.store, ref.externalId, run, now, who?.role === "approver");
|
|
519
|
+
})(),
|
|
488
520
|
// "publishes as" reads ONLY publicationGrantFor(repo) — the grant
|
|
489
521
|
// the publisher would act under — never listGrants(), which is
|
|
490
522
|
// dispatch authority (slice 1c).
|
|
@@ -584,6 +616,7 @@ export function createTaskViews(runtime) {
|
|
|
584
616
|
deliverable: view.deliverable ?? "branch",
|
|
585
617
|
revision: view.revision ?? null,
|
|
586
618
|
coordinator: view.coordinator ?? null,
|
|
619
|
+
origin: view.origin ?? null,
|
|
587
620
|
repairChain: view.repairChain ?? null,
|
|
588
621
|
},
|
|
589
622
|
decisions: (view?.decisions ?? runtime.store.decisionsForTask(ref.id))
|
|
@@ -690,7 +723,53 @@ export function createTaskViews(runtime) {
|
|
|
690
723
|
return null;
|
|
691
724
|
return { action: `/r/${runId}/checks`, level: followUps.full ? "full" : "quick", returnTo };
|
|
692
725
|
}
|
|
693
|
-
|
|
726
|
+
/**
|
|
727
|
+
* One page per task (docs/plans/one-page-task-and-agent.md): the tab the address named, the Result tab's result with
|
|
728
|
+
* every result attempt to pick from, and every link that would open a result page of this family (its /review deep
|
|
729
|
+
* link, a build's /r/<n>) pointed at this page's Result tab instead — the person stays on the task.
|
|
730
|
+
*/
|
|
731
|
+
function onePageView(view, family, runs, extras) {
|
|
732
|
+
const root = family?.root.id ?? view.id;
|
|
733
|
+
const current = family?.current.id ?? view.id;
|
|
734
|
+
const versions = new Set(family?.versions.map(one => one.id) ?? [view.id]);
|
|
735
|
+
const results = new Map(runs.filter(run => runIsTaskResult(run) && (extras.admissible === undefined || extras.admissible.has(run.id))).map(run => [run.id, run.taskId]));
|
|
736
|
+
const tabHref = (taskId, run, section, hash) => `${taskHref(root)}?${taskId !== current ? `version=${encodeURIComponent(taskId)}&` : ""}tab=result${run === null ? "" : `&run=${run}`}${section === null || section === "summary" ? "" : `§ion=${section}`}${hash}`;
|
|
737
|
+
const toTab = (href) => {
|
|
738
|
+
const review = /^\/review\?result=([A-Za-z0-9._~%-]{1,200})(?:&run=([0-9]{1,15}))?(?:&tab=(summary|changes|checks))?(#[A-Za-z0-9_-]{1,120})?$/.exec(href);
|
|
739
|
+
if (review !== null) {
|
|
740
|
+
const taskId = decodeURIComponent(review[1]);
|
|
741
|
+
if (versions.has(taskId))
|
|
742
|
+
return tabHref(taskId, review[2] === undefined ? null : Number(review[2]), review[3] ?? null, review[4] ?? "");
|
|
743
|
+
}
|
|
744
|
+
const record = /^\/r\/([0-9]{1,15})$/.exec(href);
|
|
745
|
+
if (record !== null && results.has(Number(record[1])))
|
|
746
|
+
return tabHref(results.get(Number(record[1])), Number(record[1]), null, "");
|
|
747
|
+
return href;
|
|
748
|
+
};
|
|
749
|
+
const link = (one) => one === null ? null : { ...one, href: one.href === null ? null : toTab(one.href) };
|
|
750
|
+
const status = view.status === null ? null : { ...view.status, action: view.status.action === null ? null : link(view.status.action),
|
|
751
|
+
status: { ...view.status.status, details: view.status.status.details.map(one => ({ ...one, href: one.href === null ? null : toTab(one.href), action: one.action === null ? null : link(one.action) })) } };
|
|
752
|
+
const selected = extras.result?.selected ?? null;
|
|
753
|
+
const attempts = [...results.entries()].sort((a, b) => b[0] - a[0]).map(([id, taskId]) => {
|
|
754
|
+
const run = runs.find(one => one.id === id);
|
|
755
|
+
const outcome = run.outcome === "built" ? "result" : run.outcome === "no-change" ? "no change" : run.outcome === null ? "never finished" : run.outcome;
|
|
756
|
+
return { label: `${run.role === "scout" ? "Report" : "Build"} #${id} · ${outcome}`, href: tabHref(taskId, id, null, ""), current: selected?.build === id };
|
|
757
|
+
});
|
|
758
|
+
return {
|
|
759
|
+
...view,
|
|
760
|
+
status,
|
|
761
|
+
...(view.failure == null ? {} : { failure: { ...view.failure, link: link(view.failure.link) } }),
|
|
762
|
+
...(view.earlier == null ? {} : { earlier: { ...view.earlier, attempts: view.earlier.attempts.map(one => ({ ...one, href: toTab(one.href) })) } }),
|
|
763
|
+
...(view.thread === undefined ? {} : { thread: view.thread.map(one => ({ ...one, link: link(one.link) })) }),
|
|
764
|
+
// The previous result's fold moves to the Result tab when there is one.
|
|
765
|
+
sections: selected === null ? view.sections : view.sections.filter(one => one.id !== "result"),
|
|
766
|
+
tab: extras.tab ?? "activity",
|
|
767
|
+
result: selected === null ? null : { selected, current: extras.result.current, attempts,
|
|
768
|
+
// Fresh per-render request tokens (32 hex) are not what it shows: left out, so an unchanged result keeps its stamp.
|
|
769
|
+
revision: createHash("sha256").update(JSON.stringify(selected).replace(/\b[a-f0-9]{32}\b/g, "")).digest("hex").slice(0, 16) },
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
function taskScreen(response, who, taskId, problem, status, scopeDraft, cancelDraft, editPlan = false, extras = {}) {
|
|
694
773
|
const admittedFamily = runtime.familyOf(taskId);
|
|
695
774
|
if (admittedFamily?.problem != null)
|
|
696
775
|
return runtime.sendScreen(response, status, screen(admittedFamily.root.title, html `<h1>${admittedFamily.root.title}</h1><p>${admittedFamily.current.state}</p><p class="problem" data-history-problem>${admittedFamily.problem}</p>`, { chrome: runtime.chromeFor(admittedFamily.root.repo, "tasks") }));
|
|
@@ -738,10 +817,21 @@ export function createTaskViews(runtime) {
|
|
|
738
817
|
catch {
|
|
739
818
|
return null;
|
|
740
819
|
} })() } } : {}) } : page.workspace?.view;
|
|
820
|
+
// A refusal that keeps a draft (a scope edit, an armed cancel) or asks for the plan editor reopens on Plan, where the
|
|
821
|
+
// draft and its error are; anything else on Activity unless the address named a tab.
|
|
822
|
+
const tab = extras.tab ?? (scopeDraft !== undefined || cancelDraft !== undefined || editPlan ? "plan" : "activity");
|
|
823
|
+
const oneTaskView = taskView?.kind === "task" ? onePageView(taskView, family, presentedData.family?.runs ?? data.runs.map(run => ({ ...run, taskId: data.task.id })), { ...extras, tab }) : taskView;
|
|
824
|
+
// The Result tab's no-script page, under the task's own (the same HTML its result page serves), and its script.
|
|
825
|
+
if (extras.result != null) {
|
|
826
|
+
page.body = html `${page.body}<section class="task-result-tab" id="task-result" aria-label="Result">${extras.result.html}</section>`;
|
|
827
|
+
// The result's own script, as its result page ships it (a password form there keeps it too: Request changes and
|
|
828
|
+
// the line notes need it).
|
|
829
|
+
page.functional = { script: `${page.functional?.script ?? ""}${reviewEvidenceScript()}${RESULT_REVIEW_SCRIPT}`, fetches: page.functional?.fetches ?? false };
|
|
830
|
+
}
|
|
741
831
|
if (docked !== null)
|
|
742
|
-
page.workspace = { ...page.workspace, ...(
|
|
743
|
-
else if (
|
|
744
|
-
page.workspace = { ...page.workspace, view:
|
|
832
|
+
page.workspace = { ...page.workspace, ...(oneTaskView === undefined ? {} : { view: oneTaskView }), conversation: docked, pageHtml: page.body };
|
|
833
|
+
else if (oneTaskView !== undefined && page.workspace !== undefined)
|
|
834
|
+
page.workspace = { ...page.workspace, view: oneTaskView };
|
|
745
835
|
page.refreshSeconds = runtime.liveRefreshSeconds();
|
|
746
836
|
return runtime.sendScreen(response, status, page);
|
|
747
837
|
}
|