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/operate.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { ACP_AGENTS, ACP_PROVIDER_IDS, isAcpProvider, isKeyedProvider } from "./acp-agents.js";
|
|
2
|
+
import { inspectAcpAgent } from "./acp-inspect.js";
|
|
3
|
+
import { runAcpChecks } from "./acp-check.js";
|
|
4
|
+
import { verifyLaunch } from "./acp-attest.js";
|
|
1
5
|
import { notificationClaimHeld, claimNotifications, finalizeNotification, notificationDestination } from "./notification-delivery.js";
|
|
2
6
|
import { adapterPolicy } from "./server/route-policy.js";
|
|
3
7
|
import { UNSENT_REPLY_MS } from "./telegram-settings.js";
|
|
@@ -52,6 +56,7 @@ import { readHooksBase, scheduleFromWords } from "./flow-triggers.js";
|
|
|
52
56
|
import { sendSubagentSummaries } from "./subagent-admin.js";
|
|
53
57
|
import { runRequestedUndos, sendSubagentWeeklies } from "./subagent-week.js";
|
|
54
58
|
import { refreshConnections } from "./mcp-connect.js";
|
|
59
|
+
import { SECRETS_FIRST, startToolSecrets, toolSecretStorage } from "./project-tools.js";
|
|
55
60
|
import { followDiscord } from "./discord.js";
|
|
56
61
|
import { followTeams } from "./teams.js";
|
|
57
62
|
import { followSlack } from "./slack.js";
|
|
@@ -94,6 +99,7 @@ import { basename, dirname, join, resolve } from "node:path";
|
|
|
94
99
|
import { releaseStalledUpdate, waitingUpdate } from "./toolroll-update.js";
|
|
95
100
|
import { openStore, openStoreNoMigrate, CONCURRENT_WRITER_WAIT_MS, isDatabaseBusy, databasePath, BUILT_IN, DEFAULT_ACTOR, parseCapabilityKey, verifiedAuthor, isDigestTime, RESULT_SCREENSHOTS, } from "./store.js";
|
|
96
101
|
import { createHash, randomBytes, randomInt, randomUUID } from "node:crypto";
|
|
102
|
+
import { claimInstance, clearInstance, openInstanceSocket } from "./console-instance.js";
|
|
97
103
|
import { authorizePlanUnderMode } from "./plan-auto.js";
|
|
98
104
|
import { ghDispatchAdapter, mirrorTaskId, syncPass } from "./sync.js";
|
|
99
105
|
import { LIVE_FILE_BOUND, LIVE_RETAIN_MS, sweepLiveLogs } from "./live.js";
|
|
@@ -104,6 +110,7 @@ import { chmodSync, closeSync, constants as fsConstants, existsSync, fstatSync,
|
|
|
104
110
|
import { BRANCH_PREFIX, envTwins, envValue, existingOrFirst, taskBranches } from "./names.js";
|
|
105
111
|
import { claimActor, currentActor, withActor } from "./actor.js";
|
|
106
112
|
import { parseLeadToken } from "./api-tokens.js";
|
|
113
|
+
import { MAX_NETWORK_HOSTS, normalizeHost } from "./egress.js";
|
|
107
114
|
import { CLI_PASSWORD_SOURCE, withPasswordSource } from "./sign-in-guard.js";
|
|
108
115
|
import { admissionRecorded, admitProject, commandLineActor } from "./project-admission.js";
|
|
109
116
|
import { leadClaim, leadSay, noteLeadWork } from "./lead-voice.js";
|
|
@@ -181,7 +188,7 @@ import { register, authenticate, heartbeat as heartbeatRunner, isAlive, recoverD
|
|
|
181
188
|
import { mintCoordinator, revokeCoordinator, listCoordinators } from "./coordinator.js";
|
|
182
189
|
import { serveMcp } from "./mcp.js";
|
|
183
190
|
import { createInterface } from "node:readline";
|
|
184
|
-
import { propose, approve, addApprover, authenticateAccount, authenticateApprover as passwordApprover, describeScope, approvalOf, hashToken as hashApproverToken, fileAndSealUnderMode, modeFilingCoverage, acceptanceLinesToInput, parseAcceptanceCriteria, splitAcceptanceRubric, rubricIsPlaceholder, isCommitSha } from "./scope.js";
|
|
191
|
+
import { propose, approve, addApprover, authenticateAccount, authenticateApprover as passwordApprover, describeScope, approvalOf, hashToken as hashApproverToken, fileAndSealUnderMode, modeFilingCoverage, acceptanceLinesToInput, parseAcceptanceCriteria, splitAcceptanceRubric, rubricIsPlaceholder, isCommitSha, credentialProof, rememberedLogin } from "./scope.js";
|
|
185
192
|
import { CHAT_APPROVE_ALL, presetTerms, modeTermsJson, modeDigestOf, modeTermsFromJson, modeWords, MODE_MAX_DAYS } from "./modes.js";
|
|
186
193
|
import { WorktreePool } from "./worktree.js";
|
|
187
194
|
import { baseStandingWords, freshBaseFor, taskBaseOf } from "./task-base.js";
|
|
@@ -250,6 +257,11 @@ export const OPERATE_HELP = `toolroll — operating the queue
|
|
|
250
257
|
toolroll project show show the current project and credential reference
|
|
251
258
|
toolroll project rules --repo <p> a project's approval rules; an instance operator changes them with
|
|
252
259
|
--not-requester on|off, --protect none|project, --protect-paths "a/**,b/**"
|
|
260
|
+
toolroll project network --repo <p> [--allow h,…] [--remove h,…] [--open|--allowlist] [--local any|none]
|
|
261
|
+
what the project's builds may reach on the network; an approver changes it
|
|
262
|
+
(new scopes seal it; approved ones keep theirs)
|
|
263
|
+
toolroll config unconfined-agents [allow|refuse]
|
|
264
|
+
whether builds run where this machine can't confine them (instance operator)
|
|
253
265
|
toolroll project concurrency [<n>] --repo <p>
|
|
254
266
|
how many of a project's tasks build at once (default 2, never past the
|
|
255
267
|
worker's capacity); an approver changes it with <n>
|
|
@@ -751,7 +763,8 @@ export function parseOperateArgs(argv, ownValues = new Set()) {
|
|
|
751
763
|
}
|
|
752
764
|
/** Route a native command and return its exit code. Its password source cannot be selected by an HTTP request. */
|
|
753
765
|
export async function runOperate(command, argv, write, options = {}) {
|
|
754
|
-
|
|
766
|
+
// Each command gets its own remembered-login holder (Passwordless: "cli-login" is this command's, never the next one's).
|
|
767
|
+
return withPasswordSource(CLI_PASSWORD_SOURCE, () => rememberedLogin.run({ login: null }, () => runOperateLocal(command, argv, write, options)));
|
|
755
768
|
}
|
|
756
769
|
async function runOperateLocal(command, argv, write, options = {}) {
|
|
757
770
|
const parsed = parseOperateArgs(argv, command === "flows" ? FLOWS_VALUE_FLAGS : undefined);
|
|
@@ -815,6 +828,9 @@ async function runOperateLocal(command, argv, write, options = {}) {
|
|
|
815
828
|
catch (error) {
|
|
816
829
|
return databaseFailure(write, json, command, file, error);
|
|
817
830
|
}
|
|
831
|
+
// Toolroll's folder for the secrets it holds, beside this database: made before any build can run.
|
|
832
|
+
if (file !== ":memory:")
|
|
833
|
+
startToolSecrets(store, file, () => new Date(), { eager: SECRETS_FIRST.has(command) });
|
|
818
834
|
// The lead (a lead token, as --token or TOOLROLL_LEAD_TOKEN) acts for its owner in single commands; its work
|
|
819
835
|
// pings nobody. Anyone else who signs in inside one of these commands is who acted. Services name nobody.
|
|
820
836
|
const presentedFlag = text(flags, "token");
|
|
@@ -1141,6 +1157,8 @@ async function dispatch(command, positional, flags, context) {
|
|
|
1141
1157
|
return projectDeleteCommand(positional, flags, context);
|
|
1142
1158
|
if (positional[0] === "concurrency")
|
|
1143
1159
|
return projectConcurrencyCommand(positional, flags, context);
|
|
1160
|
+
if (positional[0] === "network")
|
|
1161
|
+
return projectNetworkCommand(positional, flags, context);
|
|
1144
1162
|
if (positional[0] === "checks")
|
|
1145
1163
|
return projectChecksCommand(positional, flags, context);
|
|
1146
1164
|
if (positional[0] === "demo")
|
|
@@ -1219,6 +1237,8 @@ async function dispatch(command, positional, flags, context) {
|
|
|
1219
1237
|
case "flows":
|
|
1220
1238
|
return flowsCommand(positional, flags, context);
|
|
1221
1239
|
case "config":
|
|
1240
|
+
if (positional[0] === "unconfined-agents")
|
|
1241
|
+
return unconfinedAgentsCommand(positional, flags, context);
|
|
1222
1242
|
return configCommand(positional, flags, context);
|
|
1223
1243
|
case "chat":
|
|
1224
1244
|
return chatCommand(flags, context);
|
|
@@ -1239,6 +1259,8 @@ async function dispatch(command, positional, flags, context) {
|
|
|
1239
1259
|
});
|
|
1240
1260
|
case "keys":
|
|
1241
1261
|
return keysCommand(positional, flags, context);
|
|
1262
|
+
case "agents":
|
|
1263
|
+
return agentsCommand(positional, flags, context);
|
|
1242
1264
|
case "setup":
|
|
1243
1265
|
return setupCommand(positional, flags, context);
|
|
1244
1266
|
case "verify":
|
|
@@ -1484,6 +1506,9 @@ function claimCommand(positional, flags, context) {
|
|
|
1484
1506
|
if (result.reason === "coordinator-filed") {
|
|
1485
1507
|
return fail(write, json, "claim", "coordinator-filed", "a coordinator filed this — nothing runs until the operator signs its scope", EXIT.refused);
|
|
1486
1508
|
}
|
|
1509
|
+
if (result.reason === "sweep-limit") {
|
|
1510
|
+
return fail(write, json, "claim", "sweep-limit", `its sweep already has ${result.limit} running at once — it starts when one finishes`, EXIT.refused, { sweep: result.sweep, limit: result.limit });
|
|
1511
|
+
}
|
|
1487
1512
|
return fail(write, json, "claim", result.reason, `held by ${result.by} until ${result.until}`, EXIT.refused, { holder: result.by, until: result.until });
|
|
1488
1513
|
}
|
|
1489
1514
|
// Taking a task is what makes it running; leaving that to the caller would
|
|
@@ -1602,7 +1627,7 @@ async function runnerCommand(positional, flags, context) {
|
|
|
1602
1627
|
if (acting === null) {
|
|
1603
1628
|
return fail(write, json, "runner register", "usage", "`runner register <name> --repo <path> --as <you> --token <t>` — minting runner authority is an operator act", EXIT.usage);
|
|
1604
1629
|
}
|
|
1605
|
-
const authenticated =
|
|
1630
|
+
const authenticated = credentialApprover(store, acting.name, acting.token);
|
|
1606
1631
|
if (!authenticated.ok) {
|
|
1607
1632
|
return fail(write, json, "runner register", authenticated.reason, describeApproveFailure(authenticated.reason, name), EXIT.refused);
|
|
1608
1633
|
}
|
|
@@ -1728,7 +1753,7 @@ async function runnerCommand(positional, flags, context) {
|
|
|
1728
1753
|
if (acting === null) {
|
|
1729
1754
|
return fail(write, json, "runner bind", "usage", "`runner bind <name> --repo <path> --as <you> --token <t>` — binding repositories is an operator act", EXIT.usage);
|
|
1730
1755
|
}
|
|
1731
|
-
const authenticated =
|
|
1756
|
+
const authenticated = credentialApprover(store, acting.name, acting.token);
|
|
1732
1757
|
if (!authenticated.ok) {
|
|
1733
1758
|
return fail(write, json, "runner bind", authenticated.reason, describeApproveFailure(authenticated.reason, name), EXIT.refused);
|
|
1734
1759
|
}
|
|
@@ -1814,7 +1839,7 @@ async function coordinatorCommand(positional, flags, context) {
|
|
|
1814
1839
|
if (acting === null) {
|
|
1815
1840
|
return fail(write, json, "coordinator mint", "usage", "`coordinator mint <name> --repo <path> --as <you> --token <t>` — minting filing authority is an operator act", EXIT.usage);
|
|
1816
1841
|
}
|
|
1817
|
-
const authenticated =
|
|
1842
|
+
const authenticated = credentialApprover(store, acting.name, acting.token);
|
|
1818
1843
|
if (!authenticated.ok) {
|
|
1819
1844
|
return fail(write, json, "coordinator mint", authenticated.reason, describeApproveFailure(authenticated.reason, name), EXIT.refused);
|
|
1820
1845
|
}
|
|
@@ -1940,6 +1965,7 @@ async function mcpCommand(file, flags, write, json) {
|
|
|
1940
1965
|
if (!door.ok)
|
|
1941
1966
|
return said(door.message);
|
|
1942
1967
|
const store = door.store;
|
|
1968
|
+
startToolSecrets(store, file, () => new Date(), { eager: false });
|
|
1943
1969
|
for (const suffix of ["", "-wal", "-shm"]) {
|
|
1944
1970
|
try {
|
|
1945
1971
|
chmodSync(`${file}${suffix}`, 0o600);
|
|
@@ -2287,6 +2313,17 @@ async function tickCommand(flags, context) {
|
|
|
2287
2313
|
root: pool,
|
|
2288
2314
|
...(context.gitRunner === undefined ? {} : { runner: context.gitRunner }),
|
|
2289
2315
|
});
|
|
2316
|
+
// ACP agent checks (acp-check.ts) run in a watch's own pass, under its lease, before anything is dispatched — one
|
|
2317
|
+
// at a time per runner. Only a watch's full pass runs them (never a build lane, never a bare tick).
|
|
2318
|
+
const watchOwner = text(flags, "incarnation");
|
|
2319
|
+
if (watchOwner !== undefined && !buildsOnly) {
|
|
2320
|
+
await runAcpChecks({
|
|
2321
|
+
store, runner, token, pool: worktrees, clock,
|
|
2322
|
+
watch: { repo: canonicalProject(repo) ?? resolve(repo), owner: watchOwner },
|
|
2323
|
+
progress: line => { if (!json)
|
|
2324
|
+
write(line); },
|
|
2325
|
+
});
|
|
2326
|
+
}
|
|
2290
2327
|
// Where a first attempt's branch grows from, resolved once per pass. A
|
|
2291
2328
|
// detached HEAD refuses the pass rather than guessing: an unattended commit
|
|
2292
2329
|
// onto "wherever the operator happened to be" is not a default.
|
|
@@ -2611,6 +2648,20 @@ async function tickCommand(flags, context) {
|
|
|
2611
2648
|
: [store.getScope(id)?.approvedProfile?.provider].filter((one) => one !== null && one !== undefined);
|
|
2612
2649
|
let unattestedLane = null;
|
|
2613
2650
|
for (const candidate of new Set(skipProviders)) {
|
|
2651
|
+
// An ACP agent is admitted by its own check on this runner (absence is denial), read here the way the gateway reads it.
|
|
2652
|
+
if (isAcpProvider(candidate)) {
|
|
2653
|
+
const checked = store.acpAttestationFor(candidate, runner);
|
|
2654
|
+
const verified = checked === null ? null : verifyLaunch(checked.launch);
|
|
2655
|
+
if (checked === null) {
|
|
2656
|
+
unattestedLane = `${ACP_AGENTS[candidate].name} isn't checked on ${runner} — run \`toolroll agents check ${candidate}\` there`;
|
|
2657
|
+
break;
|
|
2658
|
+
}
|
|
2659
|
+
if (verified !== null && !verified.ok) {
|
|
2660
|
+
unattestedLane = verified.problem;
|
|
2661
|
+
break;
|
|
2662
|
+
}
|
|
2663
|
+
continue;
|
|
2664
|
+
}
|
|
2614
2665
|
if (attestationOf(candidate) === null)
|
|
2615
2666
|
continue;
|
|
2616
2667
|
let verdict = attestedThisPass.get(candidate);
|
|
@@ -2641,7 +2692,9 @@ async function tickCommand(flags, context) {
|
|
|
2641
2692
|
// budget is raised (the 100% alert went out; the task page and the spend page say why).
|
|
2642
2693
|
// Budgets are dollars: work that runs only on subscriptions passes (see Store.budgetGate).
|
|
2643
2694
|
// Each provider as it bills now.
|
|
2644
|
-
|
|
2695
|
+
// An ACP agent bills as its declaration for the exact model it would run on this runner.
|
|
2696
|
+
const sealedModel = store.getScope(id)?.approvedProfile?.model ?? null;
|
|
2697
|
+
const budgeted = budgetHold(ref.id, [...new Set([spec.provider, ...skipProviders])].map(provider => isAcpProvider(provider) ? store.agentFor(provider, provider === spec.provider ? spec.model ?? sealedModel : sealedModel, runner) : store.agentsFor([provider])[0]));
|
|
2645
2698
|
if (budgeted.over !== null) {
|
|
2646
2699
|
dispatched.push({ id, outcome: "skipped", reason: "budget", detail: budgetWords(budgeted) });
|
|
2647
2700
|
continue;
|
|
@@ -3895,6 +3948,7 @@ async function startConsole(options) {
|
|
|
3895
3948
|
...(options.registryPath === undefined ? {} : { registryPath: options.registryPath }),
|
|
3896
3949
|
...(options.upConsole === undefined ? {} : { upConsole: options.upConsole }),
|
|
3897
3950
|
...(options.editorLinks === undefined ? {} : { editorLinks: options.editorLinks }),
|
|
3951
|
+
...(options.instanceSecret === undefined ? {} : { instanceSecret: options.instanceSecret }),
|
|
3898
3952
|
});
|
|
3899
3953
|
await new Promise((resolve, reject) => {
|
|
3900
3954
|
server.once("error", reject);
|
|
@@ -4030,6 +4084,8 @@ async function requeueTask(positional, flags, context) {
|
|
|
4030
4084
|
claimed: `a runner holds ${id} right now — wait for the attempt to end, or stop it`,
|
|
4031
4085
|
"accepted-result": `${id}'s last result was accepted; change it through a revision, not a rerun`,
|
|
4032
4086
|
published: `${id}'s last result is published; revise it through its pull request`,
|
|
4087
|
+
// Only an Approve & build again asks for an exact build; the CLI never does.
|
|
4088
|
+
moved: `${id} changed since its page was read; read it again`,
|
|
4033
4089
|
};
|
|
4034
4090
|
return fail(write, json, "task requeue", result.reason, why[result.reason], EXIT.refused);
|
|
4035
4091
|
}
|
|
@@ -4312,6 +4368,13 @@ async function providersCommand(flags, context) {
|
|
|
4312
4368
|
// that serializes four of them reads as a hang (Phase 3).
|
|
4313
4369
|
const probed = await Promise.all(PROVIDER_IDS.map(async (id) => {
|
|
4314
4370
|
const facts = inspectionOf(id);
|
|
4371
|
+
// An ACP agent is looked at, never run (acp-inspect.ts): its version is its package's, read; whether it may
|
|
4372
|
+
// run is its conformance check.
|
|
4373
|
+
if (isAcpProvider(id)) {
|
|
4374
|
+
const seen = inspectAcpAgent(id);
|
|
4375
|
+
const version = { code: seen.found ? 0 : 127, stdout: seen.found ? (seen.package?.version ?? "") : "", stderr: "", notFound: !seen.found, timedOut: false };
|
|
4376
|
+
return { id, facts, version, installed: seen.found, identity: seen.found ? `not checked on this computer — run \`toolroll agents check ${id}\`` : null };
|
|
4377
|
+
}
|
|
4315
4378
|
// A version probe carries no key (Codex gemini verify, finding 2).
|
|
4316
4379
|
const version = await probe(facts.binary, ["--version"], { timeoutMs: 5_000, omitEnv: ALL_CREDENTIAL_ENV });
|
|
4317
4380
|
const installed = version.code === 0 && !version.notFound;
|
|
@@ -4368,7 +4431,7 @@ async function providersCommand(flags, context) {
|
|
|
4368
4431
|
if (runnerName === undefined || runnerToken === undefined) {
|
|
4369
4432
|
return fail(write, json, "providers", "usage", "`toolroll providers --report --runner <name> --token <t>` records readiness under that runner", EXIT.usage);
|
|
4370
4433
|
}
|
|
4371
|
-
const observed = await observeProviderReadiness((file, args, options) => probe(file, args, { timeoutMs: options?.timeoutMs ?? 5_000, ...(options?.omitEnv === undefined ? {} : { omitEnv: options.omitEnv }) }));
|
|
4434
|
+
const observed = await observeProviderReadiness((file, args, options) => probe(file, args, { timeoutMs: options?.timeoutMs ?? 5_000, ...(options?.omitEnv === undefined ? {} : { omitEnv: options.omitEnv }) }), process.env, PROVIDER_IDS, provider => store.acpAttestationFor(provider, runnerName));
|
|
4372
4435
|
const reported = reportProviderReadinessAuthed(store, { name: runnerName, token: runnerToken, observations: observed }, context.clock());
|
|
4373
4436
|
if (!reported.ok) {
|
|
4374
4437
|
return fail(write, json, "providers", reported.reason, describeAuth(reported.reason, runnerName), EXIT.refused);
|
|
@@ -4439,6 +4502,83 @@ async function providersCommand(flags, context) {
|
|
|
4439
4502
|
* own — these are 0600 files under the operator's home, and whoever can
|
|
4440
4503
|
* write them already owns the machine's spend.
|
|
4441
4504
|
*/
|
|
4505
|
+
/**
|
|
4506
|
+
* `toolroll agents …` — the coding agents Toolroll runs over ACP (acp-agents.ts): what is installed here, which are
|
|
4507
|
+
* checked on which runner, and how each checked model is billed. An agent runs only once its conformance check passed
|
|
4508
|
+
* on that runner (acp-check.ts); asking for one, or declaring a model billed by the agent's own plan, takes a password.
|
|
4509
|
+
*
|
|
4510
|
+
* agents each agent: installed, checked, billing
|
|
4511
|
+
* agents check <agent> --model <m> [--runner r] ask the runner's watch to check it (password)
|
|
4512
|
+
* agents revoke <agent> [--runner r] the agent stops running there
|
|
4513
|
+
* agents billing <agent> <model> plan|metered [--runner r]
|
|
4514
|
+
*/
|
|
4515
|
+
async function agentsCommand(positional, flags, context) {
|
|
4516
|
+
const { store, write, json, clock } = context;
|
|
4517
|
+
const [action = "list", provider, ...rest] = positional;
|
|
4518
|
+
const runners = store.listRunners().filter(one => one.retiredAt === null).map(one => one.name);
|
|
4519
|
+
const runnerGiven = text(flags, "runner");
|
|
4520
|
+
const runner = runnerGiven ?? (runners.length === 1 ? runners[0] : undefined);
|
|
4521
|
+
if (action === "list") {
|
|
4522
|
+
const rows = ACP_PROVIDER_IDS.map(id => {
|
|
4523
|
+
const seen = inspectAcpAgent(id);
|
|
4524
|
+
const checked = runners.flatMap(name => { const one = store.acpAttestationFor(id, name); return one === null ? [] : [{ runner: name, agent: one.agent, models: one.models, at: one.recordedAt }]; });
|
|
4525
|
+
const latest = store.acpChecksFor(id, 1)[0] ?? null;
|
|
4526
|
+
return { provider: id, name: ACP_AGENTS[id].name, installed: seen.found ? seen.realpath : null, checked, latestCheck: latest === null ? null : { id: latest.id, state: latest.state, model: latest.model, result: latest.result } };
|
|
4527
|
+
});
|
|
4528
|
+
if (json) {
|
|
4529
|
+
write(envelopeJson({ ok: true, command: "agents", agents: rows }));
|
|
4530
|
+
return EXIT.ok;
|
|
4531
|
+
}
|
|
4532
|
+
for (const row of rows) {
|
|
4533
|
+
write(` ${row.name.padEnd(10)} ${row.installed === null ? "not installed" : row.installed}`);
|
|
4534
|
+
for (const one of row.checked)
|
|
4535
|
+
write(` checked on ${one.runner}: ${one.agent.name} ${one.agent.version}, ${one.models.length} models (${one.at.slice(0, 10)})`);
|
|
4536
|
+
if (row.latestCheck !== null && row.latestCheck.state !== "passed")
|
|
4537
|
+
write(` check #${row.latestCheck.id} (${row.latestCheck.model}): ${row.latestCheck.state}${row.latestCheck.result === null ? "" : ` — ${row.latestCheck.result}`}`);
|
|
4538
|
+
}
|
|
4539
|
+
write(" → `agents check <agent> --model <id>` asks this runner's watch to check one; it runs on the watch's next pass");
|
|
4540
|
+
return EXIT.ok;
|
|
4541
|
+
}
|
|
4542
|
+
if (!["check", "revoke", "billing"].includes(action))
|
|
4543
|
+
return fail(write, json, "agents", "usage", "`toolroll agents [check|revoke|billing]`", EXIT.usage);
|
|
4544
|
+
if (provider === undefined || !isAcpProvider(provider))
|
|
4545
|
+
return fail(write, json, `agents ${action}`, "usage", `which agent? ${ACP_PROVIDER_IDS.join(", ")}`, EXIT.usage);
|
|
4546
|
+
if (runner === undefined || !runners.includes(runner))
|
|
4547
|
+
return fail(write, json, `agents ${action}`, "usage", `which runner? --runner ${runners.join(" | ") || "<name>"}`, EXIT.usage);
|
|
4548
|
+
const acting = await askCredentials(flags, context);
|
|
4549
|
+
if (acting === null)
|
|
4550
|
+
return fail(write, json, `agents ${action}`, "usage", "this takes your name and password — `--as <you>` and the hidden prompt", EXIT.usage);
|
|
4551
|
+
const verified = verifyApproverByPassword(store, acting.name, acting.token, store.knownRepos().filter(repo => store.accountCanAccess(acting.name, repo)));
|
|
4552
|
+
if (!verified.ok)
|
|
4553
|
+
return fail(write, json, `agents ${action}`, "unauthenticated", "that is not an approver, or the password does not match", EXIT.refused);
|
|
4554
|
+
// Which agents a runner admits, and how they bill, hold for every project the runner serves: an operator's call.
|
|
4555
|
+
if (!store.isInstanceOperator(verified.who.name))
|
|
4556
|
+
return fail(write, json, `agents ${action}`, "forbidden", "only an installation operator can check, revoke or set billing for a runner's agents", EXIT.refused);
|
|
4557
|
+
const name = ACP_AGENTS[provider].name;
|
|
4558
|
+
const now = clock();
|
|
4559
|
+
if (action === "revoke") {
|
|
4560
|
+
const revoked = store.revokeAcpAttestation(provider, runner, verified.who.name, now);
|
|
4561
|
+
return succeed(write, json, "agents revoke", { provider, runner, revoked }, () => [revoked ? `${name} no longer runs on ${runner}; check it again to bring it back.` : `${name} wasn't checked on ${runner}.`]);
|
|
4562
|
+
}
|
|
4563
|
+
if (action === "billing") {
|
|
4564
|
+
const [model, kind] = rest;
|
|
4565
|
+
if (model === undefined || !validModelId(model) || (kind !== "plan" && kind !== "metered"))
|
|
4566
|
+
return fail(write, json, "agents billing", "usage", "`agents billing <agent> <model> plan|metered`", EXIT.usage);
|
|
4567
|
+
const revision = store.declareAcpBilling({ provider, runner, model, kind, by: verified.who.name }, now);
|
|
4568
|
+
return succeed(write, json, "agents billing", { provider, runner, model, kind, revision }, () => [kind === "plan"
|
|
4569
|
+
? `${name} on ${model} (${runner}) is billed by ${name}'s own plan: its runs count as $0, unmeasured, and monthly budgets don't hold them. Runs already started keep the terms they started with.`
|
|
4570
|
+
: `${name} on ${model} (${runner}) is metered: its cost is unknown, so capped work and hard budgets hold it.`]);
|
|
4571
|
+
}
|
|
4572
|
+
// check
|
|
4573
|
+
const model = text(flags, "model");
|
|
4574
|
+
if (model === undefined || !validModelId(model))
|
|
4575
|
+
return fail(write, json, "agents check", "usage", "`agents check <agent> --model <id>` — a model the agent offers (its own list shows after the check)", EXIT.usage);
|
|
4576
|
+
const id = store.requestAcpCheck({ provider, runner, model, by: verified.who.name }, now);
|
|
4577
|
+
return succeed(write, json, "agents check", { provider, runner, model, request: id }, () => [
|
|
4578
|
+
`Check #${id} asked for: ${name} on ${model}, on ${runner}. Its watch runs it on its next pass (two short runs, in the fence; the model's own spend).`,
|
|
4579
|
+
"→ `toolroll agents` shows how it went.",
|
|
4580
|
+
]);
|
|
4581
|
+
}
|
|
4442
4582
|
async function keysCommand(positional, flags, context) {
|
|
4443
4583
|
const { write, json } = context;
|
|
4444
4584
|
const [action, provider, ...rest] = positional;
|
|
@@ -4446,7 +4586,8 @@ async function keysCommand(positional, flags, context) {
|
|
|
4446
4586
|
return fail(write, json, "keys", "usage", `unknown \`keys ${action ?? ""}\` — try ${KEYS_ACTIONS.join(", ")}`, EXIT.usage);
|
|
4447
4587
|
}
|
|
4448
4588
|
if (action === "status") {
|
|
4449
|
-
|
|
4589
|
+
// ACP agents sign in their own way: Toolroll keeps no key for them, so they aren't listed here.
|
|
4590
|
+
const rows = PROVIDER_IDS.filter(isKeyedProvider).map(one => ({
|
|
4450
4591
|
provider: one,
|
|
4451
4592
|
envName: PROVIDER_KEY_ENV[one],
|
|
4452
4593
|
...keyStatus(one),
|
|
@@ -4470,7 +4611,10 @@ async function keysCommand(positional, flags, context) {
|
|
|
4470
4611
|
return EXIT.ok;
|
|
4471
4612
|
}
|
|
4472
4613
|
if (provider === undefined || !isProviderId(provider)) {
|
|
4473
|
-
return fail(write, json, `keys ${action}`, "usage", `which provider? ${PROVIDER_IDS.join(", ")}`, EXIT.usage);
|
|
4614
|
+
return fail(write, json, `keys ${action}`, "usage", `which provider? ${PROVIDER_IDS.filter(isKeyedProvider).join(", ")}`, EXIT.usage);
|
|
4615
|
+
}
|
|
4616
|
+
if (!isKeyedProvider(provider)) {
|
|
4617
|
+
return fail(write, json, `keys ${action}`, "refused", `${ACP_AGENTS[provider].name} signs in its own way (\`${ACP_AGENTS[provider].signIn}\`) — Toolroll keeps no key for it`, EXIT.refused);
|
|
4474
4618
|
}
|
|
4475
4619
|
if (action === "clear") {
|
|
4476
4620
|
const cleared = clearProviderKey(provider);
|
|
@@ -4575,7 +4719,8 @@ async function peopleCommand(positional, flags, context) {
|
|
|
4575
4719
|
if (acting === null) {
|
|
4576
4720
|
return fail(write, json, `people ${action}`, "usage", `\`toolroll people ${action} … --as <you> --token <t>\``, EXIT.usage);
|
|
4577
4721
|
}
|
|
4578
|
-
|
|
4722
|
+
// Inviting and widening access are credential acts; revoking only narrows.
|
|
4723
|
+
const authenticated = (action === "revoke" ? authenticateApprover(store, acting.name, acting.token) : credentialApprover(store, acting.name, acting.token));
|
|
4579
4724
|
if (!authenticated.ok) {
|
|
4580
4725
|
return fail(write, json, `people ${action}`, authenticated.reason, describeApproveFailure(authenticated.reason, acting.name), EXIT.refused);
|
|
4581
4726
|
}
|
|
@@ -4642,7 +4787,8 @@ async function chatApprovalCommand(positional, flags, context) {
|
|
|
4642
4787
|
const acting = await askCredentials(flags, context);
|
|
4643
4788
|
if (acting === null)
|
|
4644
4789
|
return fail(write, json, command, "unauthenticated", "Approving from chat is your own setting: pass --as <you> --token <your password> (or sign in once with toolroll up).", EXIT.refused);
|
|
4645
|
-
|
|
4790
|
+
// Turning approving from chat on (or changing its limits) is a credential act; showing or turning it off is not.
|
|
4791
|
+
const authed = (action === "on" ? credentialApprover(store, acting.name, acting.token) : authenticateApprover(store, acting.name, acting.token));
|
|
4646
4792
|
if (!authed.ok)
|
|
4647
4793
|
return fail(write, json, command, "unauthenticated", "That name and password aren't an approver's. Nothing was changed.", EXIT.refused);
|
|
4648
4794
|
const given = text(flags, "repo");
|
|
@@ -4705,7 +4851,8 @@ async function modeCommand(positional, flags, context) {
|
|
|
4705
4851
|
if (acting === null) {
|
|
4706
4852
|
return fail(write, json, "mode", "usage", "signing or revoking a mode takes `--as <you> --token <t>`", EXIT.usage);
|
|
4707
4853
|
}
|
|
4708
|
-
|
|
4854
|
+
// Signing a mode that lets a chat app approve is a credential act; any other mode signing or revoking is ordinary.
|
|
4855
|
+
const authed = (flag(flags, "chat-approve") ? credentialApprover(store, acting.name, acting.token) : authenticateApprover(store, acting.name, acting.token));
|
|
4709
4856
|
if (!authed.ok) {
|
|
4710
4857
|
return fail(write, json, "mode", authed.reason, "that is not an active approver, or the token does not match", EXIT.refused);
|
|
4711
4858
|
}
|
|
@@ -6811,7 +6958,7 @@ async function resolveUpApprover(context, flags, canPrompt) {
|
|
|
6811
6958
|
return { approver: selected, approvers: names, verified, passwordFile, mintedPassword: null, notes };
|
|
6812
6959
|
}
|
|
6813
6960
|
/** The platform browser opener — detached, silent, never load-bearing. */
|
|
6814
|
-
function openBrowser(url) {
|
|
6961
|
+
export function openBrowser(url) {
|
|
6815
6962
|
// TOOLROLL_BROWSER_COMMAND names another program to open it with (it gets the address as its one argument).
|
|
6816
6963
|
const chosen = envValue(process.env, "BROWSER_COMMAND");
|
|
6817
6964
|
const command = chosen !== undefined && chosen !== "" ? [chosen, url]
|
|
@@ -7177,9 +7324,12 @@ async function upCommand(flags, context) {
|
|
|
7177
7324
|
// proved set on each request, so its project rail and unified chat move
|
|
7178
7325
|
// with the live builder instead of freezing at process start.
|
|
7179
7326
|
const activeRepos = new Set(repos);
|
|
7327
|
+
// Cockpit B1: this start's instance secret, minted once it is listening; the console answers its challenges with it.
|
|
7328
|
+
let instanceSecret = null;
|
|
7180
7329
|
let console_;
|
|
7181
7330
|
try {
|
|
7182
7331
|
console_ = await startConsole({
|
|
7332
|
+
instanceSecret: () => instanceSecret,
|
|
7183
7333
|
context,
|
|
7184
7334
|
host: hostFlag,
|
|
7185
7335
|
port,
|
|
@@ -7437,7 +7587,7 @@ async function upCommand(flags, context) {
|
|
|
7437
7587
|
const observeReadiness = () => {
|
|
7438
7588
|
if (context.agentRunner !== undefined)
|
|
7439
7589
|
return;
|
|
7440
|
-
void observeProviderReadiness((file, args, options) => run(file, args, { timeoutMs: options?.timeoutMs ?? 5_000, ...(options?.omitEnv === undefined ? {} : { omitEnv: options.omitEnv }) }))
|
|
7590
|
+
void observeProviderReadiness((file, args, options) => run(file, args, { timeoutMs: options?.timeoutMs ?? 5_000, ...(options?.omitEnv === undefined ? {} : { omitEnv: options.omitEnv }) }), process.env, PROVIDER_IDS, provider => store.acpAttestationFor(provider, runnerName))
|
|
7441
7591
|
.then(observed => {
|
|
7442
7592
|
if (!stopping)
|
|
7443
7593
|
reportProviderReadinessAuthed(store, { name: runnerName, token: runnerToken, observations: observed }, clock());
|
|
@@ -7469,6 +7619,21 @@ async function upCommand(flags, context) {
|
|
|
7469
7619
|
login: { account: approverPlan2.passwordFile === null ? approverPlan2.approver : loginAccount(approverPlan2.passwordFile) ?? approverPlan2.approver, file: approverPlan2.passwordFile },
|
|
7470
7620
|
});
|
|
7471
7621
|
recordUpConsoleUrl(dirname(context.databaseFile), url, clock());
|
|
7622
|
+
// Cockpit B1: who this console is, for bare `toolroll` to prove before it attaches: a fresh secret in the database
|
|
7623
|
+
// (behind the agent fence), and a discovery-only socket beside it. A socket that can't open only means attaching
|
|
7624
|
+
// isn't offered; the console runs either way.
|
|
7625
|
+
// The socket is taken FIRST: only the console that owns it publishes a record. A second console on the same database
|
|
7626
|
+
// (another port) never replaces the first one's record; it runs, but isn't attached to.
|
|
7627
|
+
let mintedSecret = null;
|
|
7628
|
+
const instanceSocket = await openInstanceSocket(context.databaseFile, { pid: process.pid, build: packageVersion(), database: context.databaseFile, port: console_.port }, () => mintedSecret);
|
|
7629
|
+
if (instanceSocket.ok) {
|
|
7630
|
+
mintedSecret = await claimInstance(store, context.databaseFile, { pid: process.pid, port: console_.port, build: packageVersion(), socket: instanceSocket.name }, clock());
|
|
7631
|
+
instanceSecret = mintedSecret;
|
|
7632
|
+
if (mintedSecret === null)
|
|
7633
|
+
progress("attach: another console is running on this database — `toolroll` in a repository opens that one");
|
|
7634
|
+
}
|
|
7635
|
+
else
|
|
7636
|
+
progress(`attach: ${instanceSocket.problem} — \`toolroll\` in a repository won't find this console`);
|
|
7472
7637
|
if (fatal === null) {
|
|
7473
7638
|
if (json) {
|
|
7474
7639
|
write(envelopeJson({
|
|
@@ -7536,7 +7701,15 @@ async function upCommand(flags, context) {
|
|
|
7536
7701
|
process.removeListener("SIGINT", stop);
|
|
7537
7702
|
process.removeListener("SIGTERM", stop);
|
|
7538
7703
|
retireRunnerIfCurrent(store, runnerName, runnerToken, clock());
|
|
7539
|
-
|
|
7704
|
+
if (instanceSocket.ok)
|
|
7705
|
+
instanceSocket.close();
|
|
7706
|
+
if (mintedSecret !== null)
|
|
7707
|
+
clearInstance(store, mintedSecret);
|
|
7708
|
+
let closeProblem = null;
|
|
7709
|
+
await new Promise(done => console_.server.close(error => { closeProblem = error ?? null; done(); }));
|
|
7710
|
+
// A live terminal that couldn't be proved stopped (or any other close failure) is said, never "stopped".
|
|
7711
|
+
if (closeProblem !== null && fatal === null)
|
|
7712
|
+
fatal = closeProblem.message;
|
|
7540
7713
|
if (fatal !== null) {
|
|
7541
7714
|
// The startup envelope (when json) already went out; the exit code is
|
|
7542
7715
|
// the health signal (finding 11/20) — never a second envelope.
|
|
@@ -7666,7 +7839,7 @@ async function bridgeCommand(positional, flags, context) {
|
|
|
7666
7839
|
return fail(write, json, `bridge ${action}`, "usage", "`--as <you> --token <your password>` — pairing hands your authority to a chat, so it takes your credential", EXIT.usage);
|
|
7667
7840
|
}
|
|
7668
7841
|
const { name: asWho, token } = acting;
|
|
7669
|
-
const authenticated = authenticateApprover(store, asWho, token);
|
|
7842
|
+
const authenticated = (action === "pair" ? credentialApprover(store, asWho, token) : authenticateApprover(store, asWho, token));
|
|
7670
7843
|
if (!authenticated.ok) {
|
|
7671
7844
|
return fail(write, json, `bridge ${action}`, authenticated.reason, describeApproveFailure(authenticated.reason, asWho), EXIT.refused);
|
|
7672
7845
|
}
|
|
@@ -8480,7 +8653,8 @@ async function integrationsCommand(positional, flags, context) {
|
|
|
8480
8653
|
const counts = { connected: 0, "not-set-up": 0, broken: 0 };
|
|
8481
8654
|
for (const one of list)
|
|
8482
8655
|
counts[one.state] += 1;
|
|
8483
|
-
|
|
8656
|
+
const secrets = toolSecretStorage();
|
|
8657
|
+
return succeed(context.write, context.json, command, { integrations: list, counts, secrets: { kind: secrets.kind, words: secrets.words, ...(secrets.problem === undefined ? {} : { problem: secrets.problem }) } }, () => renderIntegrations(list, secrets));
|
|
8484
8658
|
}
|
|
8485
8659
|
async function waitTask(positional, flags, context) {
|
|
8486
8660
|
const command = "task wait";
|
|
@@ -9445,7 +9619,7 @@ async function leadCommand(positional, flags, context) {
|
|
|
9445
9619
|
if (currentActor()?.lead === true)
|
|
9446
9620
|
return fail(write, json, command, "refused", "A lead cannot mint lead tokens. The person it acts for runs this with their password.", EXIT.refused);
|
|
9447
9621
|
const acting = await askCredentials(flags, context);
|
|
9448
|
-
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
9622
|
+
const verified = acting === null ? null : (flags.has("revoke") ? authenticateApprover(store, acting.name, acting.token) : credentialApprover(store, acting.name, acting.token));
|
|
9449
9623
|
if (acting === null || verified === null || !verified.ok) {
|
|
9450
9624
|
return fail(write, json, command, "unauthenticated", "A lead token is minted behind your password: pass --as and --token (or use the remembered login).", EXIT.refused);
|
|
9451
9625
|
}
|
|
@@ -10359,6 +10533,10 @@ function credentialsFrom(flags, context) {
|
|
|
10359
10533
|
if (remembered !== null && (name === undefined || name === remembered.name)) {
|
|
10360
10534
|
name = remembered.name;
|
|
10361
10535
|
token = remembered.password;
|
|
10536
|
+
// This command proves with the remembered login: under Passwordless that reads as "cli-login", never as typed.
|
|
10537
|
+
const holder = rememberedLogin.getStore();
|
|
10538
|
+
if (holder !== undefined)
|
|
10539
|
+
holder.login = { name, token };
|
|
10362
10540
|
}
|
|
10363
10541
|
}
|
|
10364
10542
|
return { name, token };
|
|
@@ -10379,6 +10557,12 @@ async function askCredentials(flags, context) {
|
|
|
10379
10557
|
* which scope.ts's authenticateAccount takes as that person (a viewer for a read token) and never checks as a
|
|
10380
10558
|
* password; these add the token's own project limit.
|
|
10381
10559
|
*/
|
|
10560
|
+
/** A credential act from the CLI (docs/plans/passwordless.md §5): a password typed for this command; with Passwordless on,
|
|
10561
|
+
* never the remembered login. */
|
|
10562
|
+
function credentialApprover(store, by, token, repo) {
|
|
10563
|
+
const proved = authenticateApprover(store, by, token, repo);
|
|
10564
|
+
return !proved.ok || credentialProof(proved.method) ? proved : { ok: false, reason: "not-an-approver" };
|
|
10565
|
+
}
|
|
10382
10566
|
function authenticateApprover(store, by, token, repo) {
|
|
10383
10567
|
const remote = activeRemote();
|
|
10384
10568
|
if (remote !== null && repo !== undefined && !remote.allows(repo))
|
|
@@ -10728,6 +10912,86 @@ async function projectRulesCommand(positional, flags, context) {
|
|
|
10728
10912
|
context.store.setApprovalRules(repo, next, acting.name, context.clock());
|
|
10729
10913
|
return succeed(context.write, context.json, command, { repo, rules: next }, () => [`${repo}: ${rulesSummary(next)}`]);
|
|
10730
10914
|
}
|
|
10915
|
+
/**
|
|
10916
|
+
* `project network --repo <p> [--allow h,…] [--remove h,…] [--open|--allowlist] [--local any|none]`: what the project's
|
|
10917
|
+
* builds may reach (egress.ts). New scopes are written with it and their approvals seal it; scopes already approved
|
|
10918
|
+
* keep the network they were approved with. Widening or narrowing it is an approver's act with a password typed now,
|
|
10919
|
+
* kept in the ledger as before → after.
|
|
10920
|
+
*/
|
|
10921
|
+
async function projectNetworkCommand(positional, flags, context) {
|
|
10922
|
+
const command = "project network";
|
|
10923
|
+
const allowed = new Set(["repo", "allow", "remove", "open", "allowlist", "local", "as", "token", "token-file", "token-env", "db", "json"]);
|
|
10924
|
+
for (const name of flags.keys())
|
|
10925
|
+
if (!allowed.has(name))
|
|
10926
|
+
return fail(context.write, context.json, command, "usage", `--${name} is not a project network option.`, EXIT.usage);
|
|
10927
|
+
const repoFlag = text(flags, "repo");
|
|
10928
|
+
const local = flags.get("local");
|
|
10929
|
+
if (positional.length > 1 || repoFlag === undefined || (flags.has("open") && flags.has("allowlist")) || (local !== undefined && local !== "any" && local !== "none")) {
|
|
10930
|
+
return fail(context.write, context.json, command, "usage", "Use project network --repo <project path> [--allow host,…] [--remove host,…] [--open | --allowlist] [--local any|none].", EXIT.usage);
|
|
10931
|
+
}
|
|
10932
|
+
const registered = await loadRepos(registryPathOf(context)).catch(() => ({ error: "unreadable" }));
|
|
10933
|
+
const known = [...new Set([...context.store.knownRepos(), ...context.store.listProjects().map(one => one.path), ...("error" in registered ? [] : registered.repos)])];
|
|
10934
|
+
const repo = known.find(one => one === repoFlag || one === resolve(repoFlag) || one === canonicalProject(repoFlag));
|
|
10935
|
+
if (repo === undefined)
|
|
10936
|
+
return fail(context.write, context.json, command, "not-found", "That isn't a project Toolroll knows.", EXIT.refused);
|
|
10937
|
+
const current = context.store.projectNetwork(repo);
|
|
10938
|
+
const words = (one) => [
|
|
10939
|
+
`${repo}: ${one.mode === "open" ? "builds may reach any host" : `builds reach their agent's provider, the package registries the project uses${one.hosts.length === 0 ? "" : `, and ${one.hosts.join(", ")}`}`}.`,
|
|
10940
|
+
one.loopback === "any" ? "On a Mac, builds may reach this Mac's local services (never Toolroll's own)." : "On a Mac, builds reach no local services but their own proxy and tools.",
|
|
10941
|
+
];
|
|
10942
|
+
const listOf = (name) => {
|
|
10943
|
+
const given = text(flags, name);
|
|
10944
|
+
if (given === undefined)
|
|
10945
|
+
return [];
|
|
10946
|
+
const hosts = given.split(",").map(one => one.trim()).filter(Boolean).map(normalizeHost);
|
|
10947
|
+
return hosts.some(one => one === null) ? null : hosts;
|
|
10948
|
+
};
|
|
10949
|
+
const add = listOf("allow"), remove = listOf("remove");
|
|
10950
|
+
if (add === null || remove === null)
|
|
10951
|
+
return fail(context.write, context.json, command, "usage", "A host is a name like api.stripe.com or *.stripe.com: no address, port or path.", EXIT.usage);
|
|
10952
|
+
const changing = add.length > 0 || remove.length > 0 || flags.has("open") || flags.has("allowlist") || local !== undefined;
|
|
10953
|
+
if (!changing)
|
|
10954
|
+
return succeed(context.write, context.json, command, { repo, network: current }, () => words(current));
|
|
10955
|
+
const next = {
|
|
10956
|
+
mode: flags.has("open") ? "open" : flags.has("allowlist") ? "allowlist" : current.mode,
|
|
10957
|
+
hosts: [...new Set([...current.hosts.filter(one => !remove.includes(one)), ...add])].sort(),
|
|
10958
|
+
loopback: local === "any" ? "any" : local === "none" ? "proxy-only" : current.loopback,
|
|
10959
|
+
};
|
|
10960
|
+
if (next.hosts.length > MAX_NETWORK_HOSTS)
|
|
10961
|
+
return fail(context.write, context.json, command, "usage", `A project lists at most ${MAX_NETWORK_HOSTS} hosts.`, EXIT.usage);
|
|
10962
|
+
const acting = await askCredentials(flags, context);
|
|
10963
|
+
const verified = acting === null ? null : credentialApprover(context.store, acting.name, acting.token, repo);
|
|
10964
|
+
if (acting === null || verified === null || !verified.ok || !context.store.accountCanAccess(acting.name, repo)) {
|
|
10965
|
+
return fail(context.write, context.json, command, "refused", "An approver for this project changes what its builds may reach, with their password: pass --as and --token.", EXIT.refused);
|
|
10966
|
+
}
|
|
10967
|
+
const changed = context.store.setProjectNetwork(repo, next, acting.name, context.clock());
|
|
10968
|
+
return succeed(context.write, context.json, command, { repo, before: changed.before, network: changed.after }, () => [
|
|
10969
|
+
...words(changed.after),
|
|
10970
|
+
"Scopes written from now on use this; scopes already approved keep the network they were approved with.",
|
|
10971
|
+
]);
|
|
10972
|
+
}
|
|
10973
|
+
/** `config unconfined-agents [allow|refuse]`: whether a build may run on a machine that can't confine it at the OS
|
|
10974
|
+
* (no macOS sandbox, no working bubblewrap). Refused unless an instance operator allows it, with a password. */
|
|
10975
|
+
async function unconfinedAgentsCommand(positional, flags, context) {
|
|
10976
|
+
const command = "config unconfined-agents";
|
|
10977
|
+
const { store, write, json } = context;
|
|
10978
|
+
const wanted = positional[1];
|
|
10979
|
+
if (positional.length > 2 || (wanted !== undefined && wanted !== "allow" && wanted !== "refuse")) {
|
|
10980
|
+
return fail(write, json, command, "usage", "Use config unconfined-agents [allow|refuse].", EXIT.usage);
|
|
10981
|
+
}
|
|
10982
|
+
const words = (allowed) => [allowed
|
|
10983
|
+
? "Builds run even where this machine can't confine them (each such run says \"Not confined\")."
|
|
10984
|
+
: "Builds this machine can't confine at the OS don't start."];
|
|
10985
|
+
if (wanted === undefined)
|
|
10986
|
+
return succeed(write, json, command, { allowed: store.unfencedAgentsAllowed() }, () => words(store.unfencedAgentsAllowed()));
|
|
10987
|
+
const acting = await askCredentials(flags, context);
|
|
10988
|
+
const verified = acting === null ? null : credentialApprover(store, acting.name, acting.token);
|
|
10989
|
+
if (acting === null || verified === null || !verified.ok || !store.isInstanceOperator(acting.name)) {
|
|
10990
|
+
return fail(write, json, command, "refused", "An instance operator changes this, with their password: pass --as and --token.", EXIT.refused);
|
|
10991
|
+
}
|
|
10992
|
+
store.setUnfencedAgentsAllowed(wanted === "allow", acting.name, context.clock());
|
|
10993
|
+
return succeed(write, json, command, { allowed: wanted === "allow" }, () => words(wanted === "allow"));
|
|
10994
|
+
}
|
|
10731
10995
|
/** `project concurrency [<n>] --repo <p>`: how many of a project's tasks build at once. Changing it is an approver's act. */
|
|
10732
10996
|
async function projectConcurrencyCommand(positional, flags, context) {
|
|
10733
10997
|
const command = "project concurrency";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Passwordless (docs/plans/passwordless.md): one installation setting. When it is on, signing in is the only
|
|
3
|
+
* password — a step-up is satisfied by the person's live browser session (scope.ts `proveAccountNow`, method
|
|
4
|
+
* "session") or, at this computer's CLI, by its remembered login. Credential acts (the plan's closed list) still
|
|
5
|
+
* take a typed password (`proveCredential`).
|
|
6
|
+
*
|
|
7
|
+
* Like the other policy settings it is an append-only ledger entry; the newest one wins; none means off.
|
|
8
|
+
*/
|
|
9
|
+
import type { Store } from "./store.js";
|
|
10
|
+
export declare const PASSWORDLESS_ACTION = "passwordless changed";
|
|
11
|
+
export type PasswordlessSetting = {
|
|
12
|
+
on: boolean;
|
|
13
|
+
setBy: string | null;
|
|
14
|
+
at: string | null;
|
|
15
|
+
};
|
|
16
|
+
/** Read live at every act: a change takes effect on the next one. */
|
|
17
|
+
export declare function passwordlessSetting(store: Store): PasswordlessSetting;
|
|
18
|
+
export declare const passwordlessOn: (store: Store) => boolean;
|
|
19
|
+
/** An operator's act (the caller proves it: on takes a typed password, off is one click). The ledger keeps before → after. */
|
|
20
|
+
export declare function setPasswordless(store: Store, on: boolean, by: string, now: Date): {
|
|
21
|
+
changed: boolean;
|
|
22
|
+
};
|
|
23
|
+
export declare const PASSWORDLESS_TRADE = "Anyone holding a signed-in browser \u2014 an unlocked laptop, a phone, a stolen session \u2014 can then do everything you can without a second check. Signing in, session limits (12 hours idle, 7 days), signing out and ending sessions remain your protection. Creating or widening a way in (passwords, sign-in methods, API tokens, connected apps, people, workers, chat pairing, webhook secrets) still asks for your password.";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const PASSWORDLESS_ACTION = "passwordless changed";
|
|
2
|
+
/** Read live at every act: a change takes effect on the next one. */
|
|
3
|
+
export function passwordlessSetting(store) {
|
|
4
|
+
// Fail closed: a setting that can't be read is off (a step-up then asks for the password, as it always did).
|
|
5
|
+
let row;
|
|
6
|
+
try {
|
|
7
|
+
row = store.handle.prepare("SELECT actor, at, outcome FROM action_ledger WHERE repo IS NULL AND task_id IS NULL AND action = ? ORDER BY id DESC LIMIT 1").get(PASSWORDLESS_ACTION);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return { on: false, setBy: null, at: null };
|
|
11
|
+
}
|
|
12
|
+
if (row === undefined || (row["outcome"] !== "on" && row["outcome"] !== "off"))
|
|
13
|
+
return { on: false, setBy: null, at: null };
|
|
14
|
+
return { on: row["outcome"] === "on", setBy: String(row["actor"]), at: String(row["at"]) };
|
|
15
|
+
}
|
|
16
|
+
export const passwordlessOn = (store) => passwordlessSetting(store).on;
|
|
17
|
+
/** An operator's act (the caller proves it: on takes a typed password, off is one click). The ledger keeps before → after. */
|
|
18
|
+
export function setPasswordless(store, on, by, now) {
|
|
19
|
+
if (passwordlessOn(store) === on)
|
|
20
|
+
return { changed: false };
|
|
21
|
+
store.recordAction({ at: now.toISOString(), actor: by, repo: null, taskId: null, runId: null, action: PASSWORDLESS_ACTION, outcome: on ? "on" : "off", source: "policy",
|
|
22
|
+
detail: on ? "Passwordless on: signing in is the only password" : "Passwordless off: step-ups ask for the password again" });
|
|
23
|
+
store.bumpWake();
|
|
24
|
+
return { changed: true };
|
|
25
|
+
}
|
|
26
|
+
export const PASSWORDLESS_TRADE = "Anyone holding a signed-in browser — an unlocked laptop, a phone, a stolen session — can then do everything you can without a second check. Signing in, session limits (12 hours idle, 7 days), signing out and ending sessions remain your protection. Creating or widening a way in (passwords, sign-in methods, API tokens, connected apps, people, workers, chat pairing, webhook secrets) still asks for your password.";
|