toolroll 0.9.63 → 0.9.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +108 -0
- package/README.md +33 -5
- package/dist/acp-agents.d.ts +43 -0
- package/dist/acp-agents.js +62 -0
- package/dist/acp-attest.d.ts +44 -0
- package/dist/acp-attest.js +269 -0
- package/dist/acp-check.d.ts +28 -0
- package/dist/acp-check.js +410 -0
- package/dist/acp-client.d.ts +163 -0
- package/dist/acp-client.js +738 -0
- package/dist/acp-inspect.d.ts +23 -0
- package/dist/acp-inspect.js +109 -0
- package/dist/agent-board.d.ts +19 -6
- package/dist/agent-board.js +30 -9
- package/dist/agent-fence.d.ts +117 -2
- package/dist/agent-fence.js +439 -12
- package/dist/agent-pulse.js +9 -2
- package/dist/agent-state-words.d.ts +19 -0
- package/dist/agent-state-words.js +20 -0
- package/dist/agent-state.d.ts +65 -0
- package/dist/agent-state.js +229 -0
- package/dist/agentconfig.js +39 -13
- package/dist/agents-board.d.ts +34 -2
- package/dist/agents-board.js +110 -1
- package/dist/approval-rules-ui.js +2 -2
- package/dist/assignment-delivery.d.ts +14 -0
- package/dist/assignment-delivery.js +13 -0
- package/dist/assignment.js +13 -0
- package/dist/attest.js +7 -0
- package/dist/backup-ui.js +2 -2
- package/dist/batch-checks.js +3 -2
- package/dist/browser/THIRD_PARTY_NOTICES.txt +94 -0
- package/dist/browser/terminal.css +37 -0
- package/dist/browser/terminal.js +228 -0
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +42 -38
- package/dist/browser-shell.js +6 -3
- package/dist/browser-workspace.d.ts +235 -1
- package/dist/browser-workspace.js +60 -0
- package/dist/builder.d.ts +10 -1
- package/dist/builder.js +87 -24
- package/dist/chat-actions.d.ts +13 -1
- package/dist/chat-actions.js +197 -15
- package/dist/chat-ask.d.ts +4 -0
- package/dist/chat-ask.js +14 -0
- package/dist/chat-channel.js +20 -0
- package/dist/chat-continuity.js +5 -4
- package/dist/chat-decide.d.ts +0 -14
- package/dist/chat-decide.js +4 -1
- package/dist/chat-delivery.d.ts +9 -0
- package/dist/chat-delivery.js +45 -6
- package/dist/chat-messages.d.ts +8 -0
- package/dist/chat-messages.js +36 -4
- package/dist/check-levels-ui.js +2 -2
- package/dist/checkout-proof.d.ts +28 -0
- package/dist/checkout-proof.js +62 -0
- package/dist/claim.d.ts +12 -0
- package/dist/claim.js +19 -0
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +4 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +21 -2
- package/dist/coding-ui.js +6 -4
- package/dist/console-instance.d.ts +124 -0
- package/dist/console-instance.js +337 -0
- package/dist/contracts/chat-actions.d.ts +16 -0
- package/dist/contracts/chat-actions.js +4 -0
- package/dist/contracts/chat-content.d.ts +19 -0
- package/dist/contracts/chat-content.js +6 -0
- package/dist/contracts/console-api.d.ts +13 -4
- package/dist/contracts/console-api.js +13 -4
- package/dist/contracts/flow-card.d.ts +4 -0
- package/dist/contracts/flow-card.js +3 -0
- package/dist/contracts/flow.d.ts +46 -0
- package/dist/contracts/flow.js +11 -2
- package/dist/contracts/lead-tools.d.ts +92 -0
- package/dist/contracts/lead-tools.js +13 -0
- package/dist/contracts/provider.d.ts +11 -1
- package/dist/contracts/provider.js +1 -1
- package/dist/contracts/route.d.ts +76 -1
- package/dist/contracts/route.js +1 -1
- package/dist/contracts/store-json.d.ts +2 -0
- package/dist/control-setup.d.ts +1 -1
- package/dist/control-ui.js +2 -2
- package/dist/converse.d.ts +16 -0
- package/dist/converse.js +32 -4
- package/dist/demo-chat.js +4 -4
- package/dist/demo.js +2 -2
- package/dist/desktop-update-gate.js +6 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/discord-chat.js +4 -1
- package/dist/doctor.js +1 -1
- package/dist/egress-bridge.d.ts +17 -0
- package/dist/egress-bridge.js +111 -0
- package/dist/egress.d.ts +119 -0
- package/dist/egress.js +423 -0
- package/dist/exec.d.ts +78 -0
- package/dist/exec.js +367 -9
- package/dist/export-ui.js +2 -2
- package/dist/flow-engine.js +24 -5
- package/dist/flow-gallery-ui.js +2 -2
- package/dist/flow-gallery.js +27 -8
- package/dist/flow-share.js +16 -5
- package/dist/flow-triggers.d.ts +77 -8
- package/dist/flow-triggers.js +517 -70
- package/dist/flows-cli.js +8 -2
- package/dist/flows-ui.d.ts +0 -2
- package/dist/flows-ui.js +52 -4
- package/dist/flows.d.ts +4 -0
- package/dist/flows.js +9 -1
- package/dist/html.d.ts +9 -0
- package/dist/html.js +11 -0
- package/dist/invoke.d.ts +35 -0
- package/dist/invoke.js +347 -28
- package/dist/keys.d.ts +18 -14
- package/dist/keys.js +5 -0
- package/dist/lead-capabilities.js +3 -2
- package/dist/lead-contract.js +2 -0
- package/dist/lead-doors.d.ts +2 -0
- package/dist/lead-doors.js +7 -0
- package/dist/lead-follow.d.ts +2 -0
- package/dist/lead-follow.js +58 -2
- package/dist/lead-progress.js +1 -0
- package/dist/lead-queue-runtime.d.ts +46 -0
- package/dist/lead-queue-runtime.js +191 -0
- package/dist/lead-queue.d.ts +104 -0
- package/dist/lead-queue.js +187 -0
- package/dist/lead-sweep.d.ts +67 -0
- package/dist/lead-sweep.js +99 -0
- package/dist/lead-tools.d.ts +22 -0
- package/dist/lead-tools.js +152 -1
- package/dist/lead.d.ts +13 -5
- package/dist/lead.js +89 -29
- package/dist/limits-ui.d.ts +8 -1
- package/dist/limits-ui.js +26 -0
- package/dist/live-bus.d.ts +8 -0
- package/dist/live-bus.js +40 -4
- package/dist/live-detail.d.ts +27 -0
- package/dist/live-detail.js +57 -0
- package/dist/live-notes.d.ts +20 -0
- package/dist/live-notes.js +47 -0
- package/dist/live-records.d.ts +140 -0
- package/dist/live-records.js +935 -0
- package/dist/live-terminal.d.ts +269 -0
- package/dist/live-terminal.js +686 -0
- package/dist/live.d.ts +38 -27
- package/dist/live.js +142 -124
- package/dist/mcp-oauth.d.ts +6 -3
- package/dist/mcp-oauth.js +8 -4
- package/dist/memory-pass.js +2 -2
- package/dist/mobile-viewport.d.ts +5 -1
- package/dist/mobile-viewport.js +18 -2
- package/dist/model-catalog.js +4 -0
- package/dist/monitoring-ui.js +2 -2
- package/dist/needs-you.d.ts +1 -1
- package/dist/needs-you.js +11 -4
- package/dist/operate.d.ts +4 -0
- package/dist/operate.js +335 -19
- package/dist/passwordless.d.ts +23 -0
- package/dist/passwordless.js +26 -0
- package/dist/peek-cli.d.ts +12 -4
- package/dist/peek-cli.js +19 -11
- package/dist/phase-routing.js +10 -3
- package/dist/planner-source.d.ts +3 -0
- package/dist/planner-source.js +14 -0
- package/dist/planner.d.ts +2 -1
- package/dist/planner.js +7 -1
- package/dist/policy-ui.js +2 -2
- package/dist/policy.d.ts +1 -20
- package/dist/policy.js +29 -2
- package/dist/project-delete-ui.d.ts +18 -0
- package/dist/project-delete-ui.js +60 -9
- package/dist/project-delete.js +2 -2
- package/dist/project-memory.d.ts +3 -1
- package/dist/project-memory.js +57 -25
- package/dist/project-tools.d.ts +5 -3
- package/dist/project-tools.js +12 -9
- package/dist/provider-auth.d.ts +4 -3
- package/dist/provider-auth.js +5 -4
- package/dist/provider-connection.d.ts +2 -1
- package/dist/provider-connection.js +5 -0
- package/dist/provider.d.ts +25 -2
- package/dist/provider.js +63 -9
- package/dist/pull-request-ui.js +2 -2
- package/dist/push.js +1 -1
- package/dist/quick-start.d.ts +24 -0
- package/dist/quick-start.js +127 -0
- package/dist/release-adapters.d.ts +6 -0
- package/dist/release-adapters.js +70 -3
- package/dist/release.d.ts +10 -1
- package/dist/release.js +34 -15
- package/dist/request-budget-ui.js +2 -2
- package/dist/result-follow-ups.js +3 -2
- package/dist/result-review.d.ts +1 -1
- package/dist/result-review.js +9 -3
- package/dist/retention-ui.js +2 -2
- package/dist/retention.js +10 -1
- package/dist/run-efficiency.d.ts +73 -0
- package/dist/run-efficiency.js +228 -0
- package/dist/runner.d.ts +5 -1
- package/dist/runner.js +22 -1
- package/dist/scope.d.ts +183 -5
- package/dist/scope.js +256 -31
- package/dist/scout-net.d.ts +4 -1
- package/dist/scout-net.js +37 -12
- package/dist/scout.d.ts +20 -6
- package/dist/scout.js +27 -3
- package/dist/serve.js +248 -40
- package/dist/server/chat.d.ts +1 -1
- package/dist/server/chat.js +122 -31
- package/dist/server/chrome.d.ts +16 -2
- package/dist/server/chrome.js +63 -11
- package/dist/server/console.css +107 -42
- package/dist/server/flows.js +38 -8
- package/dist/server/guards.d.ts +1 -0
- package/dist/server/guards.js +23 -9
- package/dist/server/http.d.ts +7 -0
- package/dist/server/http.js +3 -2
- package/dist/server/live.d.ts +34 -1
- package/dist/server/live.js +39 -17
- package/dist/server/pages-assets.js +5 -0
- package/dist/server/pages.js +90 -13
- package/dist/server/people-tokens.d.ts +3 -0
- package/dist/server/people-tokens.js +55 -17
- package/dist/server/remote-hooks.js +12 -6
- package/dist/server/remote.d.ts +2 -1
- package/dist/server/remote.js +14 -5
- package/dist/server/render-chat.d.ts +12 -3
- package/dist/server/render-chat.js +47 -16
- package/dist/server/render-pages.d.ts +32 -4
- package/dist/server/render-pages.js +83 -13
- package/dist/server/render-people.d.ts +3 -2
- package/dist/server/render-people.js +3 -2
- package/dist/server/render-results.d.ts +18 -2
- package/dist/server/render-results.js +37 -28
- package/dist/server/render-settings.js +6 -5
- package/dist/server/render-tasks.d.ts +35 -10
- package/dist/server/render-tasks.js +111 -27
- package/dist/server/request-context.d.ts +2 -0
- package/dist/server/request-context.js +2 -1
- package/dist/server/route-table.d.ts +3 -0
- package/dist/server/route-table.js +47 -5
- package/dist/server/runtime.d.ts +33 -2
- package/dist/server/session.d.ts +25 -1
- package/dist/server/session.js +18 -4
- package/dist/server/settings.js +142 -10
- package/dist/server/sweeps.d.ts +4 -0
- package/dist/server/sweeps.js +58 -0
- package/dist/server/task-views.d.ts +2 -1
- package/dist/server/task-views.js +96 -6
- package/dist/server/tasks.js +327 -78
- package/dist/server/terminals.d.ts +31 -0
- package/dist/server/terminals.js +365 -0
- package/dist/setup-guide.js +3 -2
- package/dist/slack-chat.js +4 -1
- package/dist/spend-ui.js +3 -3
- package/dist/spend.js +2 -1
- package/dist/steer-capabilities.d.ts +32 -0
- package/dist/steer-capabilities.js +60 -0
- package/dist/steer-controls.d.ts +14 -0
- package/dist/steer-controls.js +36 -0
- package/dist/steer-status.js +2 -2
- package/dist/storage-ui.js +4 -4
- package/dist/store-baseline.d.ts +1 -1
- package/dist/store-baseline.js +1 -1
- package/dist/store.d.ts +440 -15
- package/dist/store.js +1072 -52
- package/dist/subagent-desk.js +1 -1
- package/dist/summary.js +3 -0
- package/dist/task-source.d.ts +58 -0
- package/dist/task-source.js +93 -0
- package/dist/team-leads.js +1 -1
- package/dist/teams-chat.js +4 -1
- package/dist/teardown-git.d.ts +22 -0
- package/dist/teardown-git.js +136 -2
- package/dist/telegram-lead.d.ts +3 -0
- package/dist/telegram-lead.js +44 -7
- package/dist/telegram.d.ts +11 -2
- package/dist/telegram.js +51 -17
- package/dist/terminal-catalog.d.ts +11 -0
- package/dist/terminal-catalog.js +129 -0
- package/dist/text-limits.d.ts +2 -0
- package/dist/text-limits.js +2 -0
- package/dist/tool-broker.d.ts +14 -0
- package/dist/tool-broker.js +67 -2
- package/dist/toolroll-update-ui.js +2 -2
- package/dist/toolroll-update.js +13 -3
- package/dist/tools-ui.js +2 -2
- package/dist/unfenced-test-support.d.ts +2 -0
- package/dist/unfenced-test-support.js +11 -0
- package/dist/webhook-limits.d.ts +23 -0
- package/dist/webhook-limits.js +30 -0
- package/dist/webhook-readers.d.ts +56 -0
- package/dist/webhook-readers.js +138 -0
- package/dist/webhook-signing.d.ts +27 -0
- package/dist/webhook-signing.js +119 -0
- package/dist/work-index.d.ts +29 -0
- package/dist/work-index.js +16 -2
- package/dist/workspace-revision.js +3 -1
- package/dist/writes-mode.d.ts +9 -0
- package/dist/writes-mode.js +8 -0
- package/package.json +15 -2
package/dist/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";
|
|
@@ -95,6 +99,7 @@ import { basename, dirname, join, resolve } from "node:path";
|
|
|
95
99
|
import { releaseStalledUpdate, waitingUpdate } from "./toolroll-update.js";
|
|
96
100
|
import { openStore, openStoreNoMigrate, CONCURRENT_WRITER_WAIT_MS, isDatabaseBusy, databasePath, BUILT_IN, DEFAULT_ACTOR, parseCapabilityKey, verifiedAuthor, isDigestTime, RESULT_SCREENSHOTS, } from "./store.js";
|
|
97
101
|
import { createHash, randomBytes, randomInt, randomUUID } from "node:crypto";
|
|
102
|
+
import { claimInstance, clearInstance, openInstanceSocket } from "./console-instance.js";
|
|
98
103
|
import { authorizePlanUnderMode } from "./plan-auto.js";
|
|
99
104
|
import { ghDispatchAdapter, mirrorTaskId, syncPass } from "./sync.js";
|
|
100
105
|
import { LIVE_FILE_BOUND, LIVE_RETAIN_MS, sweepLiveLogs } from "./live.js";
|
|
@@ -105,6 +110,8 @@ import { chmodSync, closeSync, constants as fsConstants, existsSync, fstatSync,
|
|
|
105
110
|
import { BRANCH_PREFIX, envTwins, envValue, existingOrFirst, taskBranches } from "./names.js";
|
|
106
111
|
import { claimActor, currentActor, withActor } from "./actor.js";
|
|
107
112
|
import { parseLeadToken } from "./api-tokens.js";
|
|
113
|
+
import { MAX_NETWORK_HOSTS, normalizeHost } from "./egress.js";
|
|
114
|
+
import { isWritesMode, writesWords } from "./writes-mode.js";
|
|
108
115
|
import { CLI_PASSWORD_SOURCE, withPasswordSource } from "./sign-in-guard.js";
|
|
109
116
|
import { admissionRecorded, admitProject, commandLineActor } from "./project-admission.js";
|
|
110
117
|
import { leadClaim, leadSay, noteLeadWork } from "./lead-voice.js";
|
|
@@ -182,7 +189,7 @@ import { register, authenticate, heartbeat as heartbeatRunner, isAlive, recoverD
|
|
|
182
189
|
import { mintCoordinator, revokeCoordinator, listCoordinators } from "./coordinator.js";
|
|
183
190
|
import { serveMcp } from "./mcp.js";
|
|
184
191
|
import { createInterface } from "node:readline";
|
|
185
|
-
import { propose, approve, addApprover, authenticateAccount, authenticateApprover as passwordApprover, describeScope, approvalOf, hashToken as hashApproverToken, fileAndSealUnderMode, modeFilingCoverage, acceptanceLinesToInput, parseAcceptanceCriteria, splitAcceptanceRubric, rubricIsPlaceholder, isCommitSha } from "./scope.js";
|
|
192
|
+
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";
|
|
186
193
|
import { CHAT_APPROVE_ALL, presetTerms, modeTermsJson, modeDigestOf, modeTermsFromJson, modeWords, MODE_MAX_DAYS } from "./modes.js";
|
|
187
194
|
import { WorktreePool } from "./worktree.js";
|
|
188
195
|
import { baseStandingWords, freshBaseFor, taskBaseOf } from "./task-base.js";
|
|
@@ -251,6 +258,15 @@ export const OPERATE_HELP = `toolroll — operating the queue
|
|
|
251
258
|
toolroll project show show the current project and credential reference
|
|
252
259
|
toolroll project rules --repo <p> a project's approval rules; an instance operator changes them with
|
|
253
260
|
--not-requester on|off, --protect none|project, --protect-paths "a/**,b/**"
|
|
261
|
+
toolroll project network --repo <p> [--allow h,…] [--remove h,…] [--open|--allowlist] [--local any|none]
|
|
262
|
+
what the project's builds may reach on the network; an approver changes it
|
|
263
|
+
(new scopes seal it; approved ones keep theirs)
|
|
264
|
+
toolroll project writes --repo <p> [workspace|anywhere]
|
|
265
|
+
where the project's builds may write: only their checkout, caches and temp
|
|
266
|
+
(workspace, the default), or anywhere outside Toolroll's fenced folders;
|
|
267
|
+
an approver changes it (new scopes seal it; approved ones keep theirs)
|
|
268
|
+
toolroll config unconfined-agents [allow|refuse]
|
|
269
|
+
whether builds run where this machine can't confine them (instance operator)
|
|
254
270
|
toolroll project concurrency [<n>] --repo <p>
|
|
255
271
|
how many of a project's tasks build at once (default 2, never past the
|
|
256
272
|
worker's capacity); an approver changes it with <n>
|
|
@@ -677,6 +693,8 @@ export const OPERATE_BOOLEAN_FLAGS = new Set([
|
|
|
677
693
|
"no-open", "remove", "no-verify", "no-follow", "end", "report", "off", "tmux",
|
|
678
694
|
"self-heal", "plan-auto", "chat-approve", "full-access", "repair-auto", "review-retry-auto", "no-local",
|
|
679
695
|
"html", "csv", "alerts-only",
|
|
696
|
+
// project network: what its builds may reach (--open | --allowlist).
|
|
697
|
+
"open", "allowlist",
|
|
680
698
|
// v105: the full export.
|
|
681
699
|
"zip",
|
|
682
700
|
// Settings → Integrations: the last checks, without checking again.
|
|
@@ -698,6 +716,9 @@ export const OPERATE_BOOLEAN_FLAGS = new Set([
|
|
|
698
716
|
// task steer: pause the running Claude build, then send the note.
|
|
699
717
|
"now",
|
|
700
718
|
]);
|
|
719
|
+
/** `project network` reads --remove <hosts> and --local any|none as values, where every other verb's --remove and
|
|
720
|
+
* --local are switches. */
|
|
721
|
+
const PROJECT_NETWORK_VALUE_FLAGS = new Set(["remove", "local"]);
|
|
701
722
|
export function parseOperateArgs(argv, ownValues = new Set()) {
|
|
702
723
|
const positional = [];
|
|
703
724
|
const flags = new Map();
|
|
@@ -752,10 +773,11 @@ export function parseOperateArgs(argv, ownValues = new Set()) {
|
|
|
752
773
|
}
|
|
753
774
|
/** Route a native command and return its exit code. Its password source cannot be selected by an HTTP request. */
|
|
754
775
|
export async function runOperate(command, argv, write, options = {}) {
|
|
755
|
-
|
|
776
|
+
// Each command gets its own remembered-login holder (Passwordless: "cli-login" is this command's, never the next one's).
|
|
777
|
+
return withPasswordSource(CLI_PASSWORD_SOURCE, () => rememberedLogin.run({ login: null }, () => runOperateLocal(command, argv, write, options)));
|
|
756
778
|
}
|
|
757
779
|
async function runOperateLocal(command, argv, write, options = {}) {
|
|
758
|
-
const parsed = parseOperateArgs(argv, command
|
|
780
|
+
const parsed = parseOperateArgs(argv, ownValueFlags(command, argv));
|
|
759
781
|
// A parse error precedes the flags map, so JSON mode is read from the raw
|
|
760
782
|
// argv — the envelope contract holds even for the earliest refusal.
|
|
761
783
|
if ("error" in parsed)
|
|
@@ -967,7 +989,7 @@ export async function runOperateAs(argv, opts) {
|
|
|
967
989
|
const scope = REMOTE_SCOPES.get(row.invocation);
|
|
968
990
|
if (row.remote !== "yes" || scope === undefined)
|
|
969
991
|
return refuse("not-remote", REMOTE_MESSAGES.no);
|
|
970
|
-
const parsed = parseOperateArgs(rest, root
|
|
992
|
+
const parsed = parseOperateArgs(rest, ownValueFlags(root, rest));
|
|
971
993
|
if ("error" in parsed)
|
|
972
994
|
return refuse("usage", parsed.error);
|
|
973
995
|
// The parser's positionals must spell the same row (a value flag can't hide a subcommand).
|
|
@@ -1145,6 +1167,10 @@ async function dispatch(command, positional, flags, context) {
|
|
|
1145
1167
|
return projectDeleteCommand(positional, flags, context);
|
|
1146
1168
|
if (positional[0] === "concurrency")
|
|
1147
1169
|
return projectConcurrencyCommand(positional, flags, context);
|
|
1170
|
+
if (positional[0] === "network")
|
|
1171
|
+
return projectNetworkCommand(positional, flags, context);
|
|
1172
|
+
if (positional[0] === "writes")
|
|
1173
|
+
return projectWritesCommand(positional, flags, context);
|
|
1148
1174
|
if (positional[0] === "checks")
|
|
1149
1175
|
return projectChecksCommand(positional, flags, context);
|
|
1150
1176
|
if (positional[0] === "demo")
|
|
@@ -1223,6 +1249,8 @@ async function dispatch(command, positional, flags, context) {
|
|
|
1223
1249
|
case "flows":
|
|
1224
1250
|
return flowsCommand(positional, flags, context);
|
|
1225
1251
|
case "config":
|
|
1252
|
+
if (positional[0] === "unconfined-agents")
|
|
1253
|
+
return unconfinedAgentsCommand(positional, flags, context);
|
|
1226
1254
|
return configCommand(positional, flags, context);
|
|
1227
1255
|
case "chat":
|
|
1228
1256
|
return chatCommand(flags, context);
|
|
@@ -1243,6 +1271,8 @@ async function dispatch(command, positional, flags, context) {
|
|
|
1243
1271
|
});
|
|
1244
1272
|
case "keys":
|
|
1245
1273
|
return keysCommand(positional, flags, context);
|
|
1274
|
+
case "agents":
|
|
1275
|
+
return agentsCommand(positional, flags, context);
|
|
1246
1276
|
case "setup":
|
|
1247
1277
|
return setupCommand(positional, flags, context);
|
|
1248
1278
|
case "verify":
|
|
@@ -1488,6 +1518,9 @@ function claimCommand(positional, flags, context) {
|
|
|
1488
1518
|
if (result.reason === "coordinator-filed") {
|
|
1489
1519
|
return fail(write, json, "claim", "coordinator-filed", "a coordinator filed this — nothing runs until the operator signs its scope", EXIT.refused);
|
|
1490
1520
|
}
|
|
1521
|
+
if (result.reason === "sweep-limit") {
|
|
1522
|
+
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 });
|
|
1523
|
+
}
|
|
1491
1524
|
return fail(write, json, "claim", result.reason, `held by ${result.by} until ${result.until}`, EXIT.refused, { holder: result.by, until: result.until });
|
|
1492
1525
|
}
|
|
1493
1526
|
// Taking a task is what makes it running; leaving that to the caller would
|
|
@@ -1606,7 +1639,7 @@ async function runnerCommand(positional, flags, context) {
|
|
|
1606
1639
|
if (acting === null) {
|
|
1607
1640
|
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);
|
|
1608
1641
|
}
|
|
1609
|
-
const authenticated =
|
|
1642
|
+
const authenticated = credentialApprover(store, acting.name, acting.token);
|
|
1610
1643
|
if (!authenticated.ok) {
|
|
1611
1644
|
return fail(write, json, "runner register", authenticated.reason, describeApproveFailure(authenticated.reason, name), EXIT.refused);
|
|
1612
1645
|
}
|
|
@@ -1732,7 +1765,7 @@ async function runnerCommand(positional, flags, context) {
|
|
|
1732
1765
|
if (acting === null) {
|
|
1733
1766
|
return fail(write, json, "runner bind", "usage", "`runner bind <name> --repo <path> --as <you> --token <t>` — binding repositories is an operator act", EXIT.usage);
|
|
1734
1767
|
}
|
|
1735
|
-
const authenticated =
|
|
1768
|
+
const authenticated = credentialApprover(store, acting.name, acting.token);
|
|
1736
1769
|
if (!authenticated.ok) {
|
|
1737
1770
|
return fail(write, json, "runner bind", authenticated.reason, describeApproveFailure(authenticated.reason, name), EXIT.refused);
|
|
1738
1771
|
}
|
|
@@ -1818,7 +1851,7 @@ async function coordinatorCommand(positional, flags, context) {
|
|
|
1818
1851
|
if (acting === null) {
|
|
1819
1852
|
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);
|
|
1820
1853
|
}
|
|
1821
|
-
const authenticated =
|
|
1854
|
+
const authenticated = credentialApprover(store, acting.name, acting.token);
|
|
1822
1855
|
if (!authenticated.ok) {
|
|
1823
1856
|
return fail(write, json, "coordinator mint", authenticated.reason, describeApproveFailure(authenticated.reason, name), EXIT.refused);
|
|
1824
1857
|
}
|
|
@@ -2292,6 +2325,17 @@ async function tickCommand(flags, context) {
|
|
|
2292
2325
|
root: pool,
|
|
2293
2326
|
...(context.gitRunner === undefined ? {} : { runner: context.gitRunner }),
|
|
2294
2327
|
});
|
|
2328
|
+
// ACP agent checks (acp-check.ts) run in a watch's own pass, under its lease, before anything is dispatched — one
|
|
2329
|
+
// at a time per runner. Only a watch's full pass runs them (never a build lane, never a bare tick).
|
|
2330
|
+
const watchOwner = text(flags, "incarnation");
|
|
2331
|
+
if (watchOwner !== undefined && !buildsOnly) {
|
|
2332
|
+
await runAcpChecks({
|
|
2333
|
+
store, runner, token, pool: worktrees, clock,
|
|
2334
|
+
watch: { repo: canonicalProject(repo) ?? resolve(repo), owner: watchOwner },
|
|
2335
|
+
progress: line => { if (!json)
|
|
2336
|
+
write(line); },
|
|
2337
|
+
});
|
|
2338
|
+
}
|
|
2295
2339
|
// Where a first attempt's branch grows from, resolved once per pass. A
|
|
2296
2340
|
// detached HEAD refuses the pass rather than guessing: an unattended commit
|
|
2297
2341
|
// onto "wherever the operator happened to be" is not a default.
|
|
@@ -2616,6 +2660,20 @@ async function tickCommand(flags, context) {
|
|
|
2616
2660
|
: [store.getScope(id)?.approvedProfile?.provider].filter((one) => one !== null && one !== undefined);
|
|
2617
2661
|
let unattestedLane = null;
|
|
2618
2662
|
for (const candidate of new Set(skipProviders)) {
|
|
2663
|
+
// An ACP agent is admitted by its own check on this runner (absence is denial), read here the way the gateway reads it.
|
|
2664
|
+
if (isAcpProvider(candidate)) {
|
|
2665
|
+
const checked = store.acpAttestationFor(candidate, runner);
|
|
2666
|
+
const verified = checked === null ? null : verifyLaunch(checked.launch);
|
|
2667
|
+
if (checked === null) {
|
|
2668
|
+
unattestedLane = `${ACP_AGENTS[candidate].name} isn't checked on ${runner} — run \`toolroll agents check ${candidate}\` there`;
|
|
2669
|
+
break;
|
|
2670
|
+
}
|
|
2671
|
+
if (verified !== null && !verified.ok) {
|
|
2672
|
+
unattestedLane = verified.problem;
|
|
2673
|
+
break;
|
|
2674
|
+
}
|
|
2675
|
+
continue;
|
|
2676
|
+
}
|
|
2619
2677
|
if (attestationOf(candidate) === null)
|
|
2620
2678
|
continue;
|
|
2621
2679
|
let verdict = attestedThisPass.get(candidate);
|
|
@@ -2646,7 +2704,9 @@ async function tickCommand(flags, context) {
|
|
|
2646
2704
|
// budget is raised (the 100% alert went out; the task page and the spend page say why).
|
|
2647
2705
|
// Budgets are dollars: work that runs only on subscriptions passes (see Store.budgetGate).
|
|
2648
2706
|
// Each provider as it bills now.
|
|
2649
|
-
|
|
2707
|
+
// An ACP agent bills as its declaration for the exact model it would run on this runner.
|
|
2708
|
+
const sealedModel = store.getScope(id)?.approvedProfile?.model ?? null;
|
|
2709
|
+
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]));
|
|
2650
2710
|
if (budgeted.over !== null) {
|
|
2651
2711
|
dispatched.push({ id, outcome: "skipped", reason: "budget", detail: budgetWords(budgeted) });
|
|
2652
2712
|
continue;
|
|
@@ -3900,6 +3960,7 @@ async function startConsole(options) {
|
|
|
3900
3960
|
...(options.registryPath === undefined ? {} : { registryPath: options.registryPath }),
|
|
3901
3961
|
...(options.upConsole === undefined ? {} : { upConsole: options.upConsole }),
|
|
3902
3962
|
...(options.editorLinks === undefined ? {} : { editorLinks: options.editorLinks }),
|
|
3963
|
+
...(options.instanceSecret === undefined ? {} : { instanceSecret: options.instanceSecret }),
|
|
3903
3964
|
});
|
|
3904
3965
|
await new Promise((resolve, reject) => {
|
|
3905
3966
|
server.once("error", reject);
|
|
@@ -4035,6 +4096,8 @@ async function requeueTask(positional, flags, context) {
|
|
|
4035
4096
|
claimed: `a runner holds ${id} right now — wait for the attempt to end, or stop it`,
|
|
4036
4097
|
"accepted-result": `${id}'s last result was accepted; change it through a revision, not a rerun`,
|
|
4037
4098
|
published: `${id}'s last result is published; revise it through its pull request`,
|
|
4099
|
+
// Only an Approve & build again asks for an exact build; the CLI never does.
|
|
4100
|
+
moved: `${id} changed since its page was read; read it again`,
|
|
4038
4101
|
};
|
|
4039
4102
|
return fail(write, json, "task requeue", result.reason, why[result.reason], EXIT.refused);
|
|
4040
4103
|
}
|
|
@@ -4317,6 +4380,13 @@ async function providersCommand(flags, context) {
|
|
|
4317
4380
|
// that serializes four of them reads as a hang (Phase 3).
|
|
4318
4381
|
const probed = await Promise.all(PROVIDER_IDS.map(async (id) => {
|
|
4319
4382
|
const facts = inspectionOf(id);
|
|
4383
|
+
// An ACP agent is looked at, never run (acp-inspect.ts): its version is its package's, read; whether it may
|
|
4384
|
+
// run is its conformance check.
|
|
4385
|
+
if (isAcpProvider(id)) {
|
|
4386
|
+
const seen = inspectAcpAgent(id);
|
|
4387
|
+
const version = { code: seen.found ? 0 : 127, stdout: seen.found ? (seen.package?.version ?? "") : "", stderr: "", notFound: !seen.found, timedOut: false };
|
|
4388
|
+
return { id, facts, version, installed: seen.found, identity: seen.found ? `not checked on this computer — run \`toolroll agents check ${id}\`` : null };
|
|
4389
|
+
}
|
|
4320
4390
|
// A version probe carries no key (Codex gemini verify, finding 2).
|
|
4321
4391
|
const version = await probe(facts.binary, ["--version"], { timeoutMs: 5_000, omitEnv: ALL_CREDENTIAL_ENV });
|
|
4322
4392
|
const installed = version.code === 0 && !version.notFound;
|
|
@@ -4373,7 +4443,7 @@ async function providersCommand(flags, context) {
|
|
|
4373
4443
|
if (runnerName === undefined || runnerToken === undefined) {
|
|
4374
4444
|
return fail(write, json, "providers", "usage", "`toolroll providers --report --runner <name> --token <t>` records readiness under that runner", EXIT.usage);
|
|
4375
4445
|
}
|
|
4376
|
-
const observed = await observeProviderReadiness((file, args, options) => probe(file, args, { timeoutMs: options?.timeoutMs ?? 5_000, ...(options?.omitEnv === undefined ? {} : { omitEnv: options.omitEnv }) }));
|
|
4446
|
+
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));
|
|
4377
4447
|
const reported = reportProviderReadinessAuthed(store, { name: runnerName, token: runnerToken, observations: observed }, context.clock());
|
|
4378
4448
|
if (!reported.ok) {
|
|
4379
4449
|
return fail(write, json, "providers", reported.reason, describeAuth(reported.reason, runnerName), EXIT.refused);
|
|
@@ -4444,6 +4514,83 @@ async function providersCommand(flags, context) {
|
|
|
4444
4514
|
* own — these are 0600 files under the operator's home, and whoever can
|
|
4445
4515
|
* write them already owns the machine's spend.
|
|
4446
4516
|
*/
|
|
4517
|
+
/**
|
|
4518
|
+
* `toolroll agents …` — the coding agents Toolroll runs over ACP (acp-agents.ts): what is installed here, which are
|
|
4519
|
+
* checked on which runner, and how each checked model is billed. An agent runs only once its conformance check passed
|
|
4520
|
+
* on that runner (acp-check.ts); asking for one, or declaring a model billed by the agent's own plan, takes a password.
|
|
4521
|
+
*
|
|
4522
|
+
* agents each agent: installed, checked, billing
|
|
4523
|
+
* agents check <agent> --model <m> [--runner r] ask the runner's watch to check it (password)
|
|
4524
|
+
* agents revoke <agent> [--runner r] the agent stops running there
|
|
4525
|
+
* agents billing <agent> <model> plan|metered [--runner r]
|
|
4526
|
+
*/
|
|
4527
|
+
async function agentsCommand(positional, flags, context) {
|
|
4528
|
+
const { store, write, json, clock } = context;
|
|
4529
|
+
const [action = "list", provider, ...rest] = positional;
|
|
4530
|
+
const runners = store.listRunners().filter(one => one.retiredAt === null).map(one => one.name);
|
|
4531
|
+
const runnerGiven = text(flags, "runner");
|
|
4532
|
+
const runner = runnerGiven ?? (runners.length === 1 ? runners[0] : undefined);
|
|
4533
|
+
if (action === "list") {
|
|
4534
|
+
const rows = ACP_PROVIDER_IDS.map(id => {
|
|
4535
|
+
const seen = inspectAcpAgent(id);
|
|
4536
|
+
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 }]; });
|
|
4537
|
+
const latest = store.acpChecksFor(id, 1)[0] ?? null;
|
|
4538
|
+
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 } };
|
|
4539
|
+
});
|
|
4540
|
+
if (json) {
|
|
4541
|
+
write(envelopeJson({ ok: true, command: "agents", agents: rows }));
|
|
4542
|
+
return EXIT.ok;
|
|
4543
|
+
}
|
|
4544
|
+
for (const row of rows) {
|
|
4545
|
+
write(` ${row.name.padEnd(10)} ${row.installed === null ? "not installed" : row.installed}`);
|
|
4546
|
+
for (const one of row.checked)
|
|
4547
|
+
write(` checked on ${one.runner}: ${one.agent.name} ${one.agent.version}, ${one.models.length} models (${one.at.slice(0, 10)})`);
|
|
4548
|
+
if (row.latestCheck !== null && row.latestCheck.state !== "passed")
|
|
4549
|
+
write(` check #${row.latestCheck.id} (${row.latestCheck.model}): ${row.latestCheck.state}${row.latestCheck.result === null ? "" : ` — ${row.latestCheck.result}`}`);
|
|
4550
|
+
}
|
|
4551
|
+
write(" → `agents check <agent> --model <id>` asks this runner's watch to check one; it runs on the watch's next pass");
|
|
4552
|
+
return EXIT.ok;
|
|
4553
|
+
}
|
|
4554
|
+
if (!["check", "revoke", "billing"].includes(action))
|
|
4555
|
+
return fail(write, json, "agents", "usage", "`toolroll agents [check|revoke|billing]`", EXIT.usage);
|
|
4556
|
+
if (provider === undefined || !isAcpProvider(provider))
|
|
4557
|
+
return fail(write, json, `agents ${action}`, "usage", `which agent? ${ACP_PROVIDER_IDS.join(", ")}`, EXIT.usage);
|
|
4558
|
+
if (runner === undefined || !runners.includes(runner))
|
|
4559
|
+
return fail(write, json, `agents ${action}`, "usage", `which runner? --runner ${runners.join(" | ") || "<name>"}`, EXIT.usage);
|
|
4560
|
+
const acting = await askCredentials(flags, context);
|
|
4561
|
+
if (acting === null)
|
|
4562
|
+
return fail(write, json, `agents ${action}`, "usage", "this takes your name and password — `--as <you>` and the hidden prompt", EXIT.usage);
|
|
4563
|
+
const verified = verifyApproverByPassword(store, acting.name, acting.token, store.knownRepos().filter(repo => store.accountCanAccess(acting.name, repo)));
|
|
4564
|
+
if (!verified.ok)
|
|
4565
|
+
return fail(write, json, `agents ${action}`, "unauthenticated", "that is not an approver, or the password does not match", EXIT.refused);
|
|
4566
|
+
// Which agents a runner admits, and how they bill, hold for every project the runner serves: an operator's call.
|
|
4567
|
+
if (!store.isInstanceOperator(verified.who.name))
|
|
4568
|
+
return fail(write, json, `agents ${action}`, "forbidden", "only an installation operator can check, revoke or set billing for a runner's agents", EXIT.refused);
|
|
4569
|
+
const name = ACP_AGENTS[provider].name;
|
|
4570
|
+
const now = clock();
|
|
4571
|
+
if (action === "revoke") {
|
|
4572
|
+
const revoked = store.revokeAcpAttestation(provider, runner, verified.who.name, now);
|
|
4573
|
+
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}.`]);
|
|
4574
|
+
}
|
|
4575
|
+
if (action === "billing") {
|
|
4576
|
+
const [model, kind] = rest;
|
|
4577
|
+
if (model === undefined || !validModelId(model) || (kind !== "plan" && kind !== "metered"))
|
|
4578
|
+
return fail(write, json, "agents billing", "usage", "`agents billing <agent> <model> plan|metered`", EXIT.usage);
|
|
4579
|
+
const revision = store.declareAcpBilling({ provider, runner, model, kind, by: verified.who.name }, now);
|
|
4580
|
+
return succeed(write, json, "agents billing", { provider, runner, model, kind, revision }, () => [kind === "plan"
|
|
4581
|
+
? `${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.`
|
|
4582
|
+
: `${name} on ${model} (${runner}) is metered: its cost is unknown, so capped work and hard budgets hold it.`]);
|
|
4583
|
+
}
|
|
4584
|
+
// check
|
|
4585
|
+
const model = text(flags, "model");
|
|
4586
|
+
if (model === undefined || !validModelId(model))
|
|
4587
|
+
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);
|
|
4588
|
+
const id = store.requestAcpCheck({ provider, runner, model, by: verified.who.name }, now);
|
|
4589
|
+
return succeed(write, json, "agents check", { provider, runner, model, request: id }, () => [
|
|
4590
|
+
`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).`,
|
|
4591
|
+
"→ `toolroll agents` shows how it went.",
|
|
4592
|
+
]);
|
|
4593
|
+
}
|
|
4447
4594
|
async function keysCommand(positional, flags, context) {
|
|
4448
4595
|
const { write, json } = context;
|
|
4449
4596
|
const [action, provider, ...rest] = positional;
|
|
@@ -4451,7 +4598,8 @@ async function keysCommand(positional, flags, context) {
|
|
|
4451
4598
|
return fail(write, json, "keys", "usage", `unknown \`keys ${action ?? ""}\` — try ${KEYS_ACTIONS.join(", ")}`, EXIT.usage);
|
|
4452
4599
|
}
|
|
4453
4600
|
if (action === "status") {
|
|
4454
|
-
|
|
4601
|
+
// ACP agents sign in their own way: Toolroll keeps no key for them, so they aren't listed here.
|
|
4602
|
+
const rows = PROVIDER_IDS.filter(isKeyedProvider).map(one => ({
|
|
4455
4603
|
provider: one,
|
|
4456
4604
|
envName: PROVIDER_KEY_ENV[one],
|
|
4457
4605
|
...keyStatus(one),
|
|
@@ -4475,7 +4623,10 @@ async function keysCommand(positional, flags, context) {
|
|
|
4475
4623
|
return EXIT.ok;
|
|
4476
4624
|
}
|
|
4477
4625
|
if (provider === undefined || !isProviderId(provider)) {
|
|
4478
|
-
return fail(write, json, `keys ${action}`, "usage", `which provider? ${PROVIDER_IDS.join(", ")}`, EXIT.usage);
|
|
4626
|
+
return fail(write, json, `keys ${action}`, "usage", `which provider? ${PROVIDER_IDS.filter(isKeyedProvider).join(", ")}`, EXIT.usage);
|
|
4627
|
+
}
|
|
4628
|
+
if (!isKeyedProvider(provider)) {
|
|
4629
|
+
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);
|
|
4479
4630
|
}
|
|
4480
4631
|
if (action === "clear") {
|
|
4481
4632
|
const cleared = clearProviderKey(provider);
|
|
@@ -4580,7 +4731,8 @@ async function peopleCommand(positional, flags, context) {
|
|
|
4580
4731
|
if (acting === null) {
|
|
4581
4732
|
return fail(write, json, `people ${action}`, "usage", `\`toolroll people ${action} … --as <you> --token <t>\``, EXIT.usage);
|
|
4582
4733
|
}
|
|
4583
|
-
|
|
4734
|
+
// Inviting and widening access are credential acts; revoking only narrows.
|
|
4735
|
+
const authenticated = (action === "revoke" ? authenticateApprover(store, acting.name, acting.token) : credentialApprover(store, acting.name, acting.token));
|
|
4584
4736
|
if (!authenticated.ok) {
|
|
4585
4737
|
return fail(write, json, `people ${action}`, authenticated.reason, describeApproveFailure(authenticated.reason, acting.name), EXIT.refused);
|
|
4586
4738
|
}
|
|
@@ -4647,7 +4799,8 @@ async function chatApprovalCommand(positional, flags, context) {
|
|
|
4647
4799
|
const acting = await askCredentials(flags, context);
|
|
4648
4800
|
if (acting === null)
|
|
4649
4801
|
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);
|
|
4650
|
-
|
|
4802
|
+
// Turning approving from chat on (or changing its limits) is a credential act; showing or turning it off is not.
|
|
4803
|
+
const authed = (action === "on" ? credentialApprover(store, acting.name, acting.token) : authenticateApprover(store, acting.name, acting.token));
|
|
4651
4804
|
if (!authed.ok)
|
|
4652
4805
|
return fail(write, json, command, "unauthenticated", "That name and password aren't an approver's. Nothing was changed.", EXIT.refused);
|
|
4653
4806
|
const given = text(flags, "repo");
|
|
@@ -4710,7 +4863,8 @@ async function modeCommand(positional, flags, context) {
|
|
|
4710
4863
|
if (acting === null) {
|
|
4711
4864
|
return fail(write, json, "mode", "usage", "signing or revoking a mode takes `--as <you> --token <t>`", EXIT.usage);
|
|
4712
4865
|
}
|
|
4713
|
-
|
|
4866
|
+
// Signing a mode that lets a chat app approve is a credential act; any other mode signing or revoking is ordinary.
|
|
4867
|
+
const authed = (flag(flags, "chat-approve") ? credentialApprover(store, acting.name, acting.token) : authenticateApprover(store, acting.name, acting.token));
|
|
4714
4868
|
if (!authed.ok) {
|
|
4715
4869
|
return fail(write, json, "mode", authed.reason, "that is not an active approver, or the token does not match", EXIT.refused);
|
|
4716
4870
|
}
|
|
@@ -6816,7 +6970,7 @@ async function resolveUpApprover(context, flags, canPrompt) {
|
|
|
6816
6970
|
return { approver: selected, approvers: names, verified, passwordFile, mintedPassword: null, notes };
|
|
6817
6971
|
}
|
|
6818
6972
|
/** The platform browser opener — detached, silent, never load-bearing. */
|
|
6819
|
-
function openBrowser(url) {
|
|
6973
|
+
export function openBrowser(url) {
|
|
6820
6974
|
// TOOLROLL_BROWSER_COMMAND names another program to open it with (it gets the address as its one argument).
|
|
6821
6975
|
const chosen = envValue(process.env, "BROWSER_COMMAND");
|
|
6822
6976
|
const command = chosen !== undefined && chosen !== "" ? [chosen, url]
|
|
@@ -7182,9 +7336,12 @@ async function upCommand(flags, context) {
|
|
|
7182
7336
|
// proved set on each request, so its project rail and unified chat move
|
|
7183
7337
|
// with the live builder instead of freezing at process start.
|
|
7184
7338
|
const activeRepos = new Set(repos);
|
|
7339
|
+
// Cockpit B1: this start's instance secret, minted once it is listening; the console answers its challenges with it.
|
|
7340
|
+
let instanceSecret = null;
|
|
7185
7341
|
let console_;
|
|
7186
7342
|
try {
|
|
7187
7343
|
console_ = await startConsole({
|
|
7344
|
+
instanceSecret: () => instanceSecret,
|
|
7188
7345
|
context,
|
|
7189
7346
|
host: hostFlag,
|
|
7190
7347
|
port,
|
|
@@ -7442,7 +7599,7 @@ async function upCommand(flags, context) {
|
|
|
7442
7599
|
const observeReadiness = () => {
|
|
7443
7600
|
if (context.agentRunner !== undefined)
|
|
7444
7601
|
return;
|
|
7445
|
-
void observeProviderReadiness((file, args, options) => run(file, args, { timeoutMs: options?.timeoutMs ?? 5_000, ...(options?.omitEnv === undefined ? {} : { omitEnv: options.omitEnv }) }))
|
|
7602
|
+
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))
|
|
7446
7603
|
.then(observed => {
|
|
7447
7604
|
if (!stopping)
|
|
7448
7605
|
reportProviderReadinessAuthed(store, { name: runnerName, token: runnerToken, observations: observed }, clock());
|
|
@@ -7474,6 +7631,21 @@ async function upCommand(flags, context) {
|
|
|
7474
7631
|
login: { account: approverPlan2.passwordFile === null ? approverPlan2.approver : loginAccount(approverPlan2.passwordFile) ?? approverPlan2.approver, file: approverPlan2.passwordFile },
|
|
7475
7632
|
});
|
|
7476
7633
|
recordUpConsoleUrl(dirname(context.databaseFile), url, clock());
|
|
7634
|
+
// Cockpit B1: who this console is, for bare `toolroll` to prove before it attaches: a fresh secret in the database
|
|
7635
|
+
// (behind the agent fence), and a discovery-only socket beside it. A socket that can't open only means attaching
|
|
7636
|
+
// isn't offered; the console runs either way.
|
|
7637
|
+
// The socket is taken FIRST: only the console that owns it publishes a record. A second console on the same database
|
|
7638
|
+
// (another port) never replaces the first one's record; it runs, but isn't attached to.
|
|
7639
|
+
let mintedSecret = null;
|
|
7640
|
+
const instanceSocket = await openInstanceSocket(context.databaseFile, { pid: process.pid, build: packageVersion(), database: context.databaseFile, port: console_.port }, () => mintedSecret);
|
|
7641
|
+
if (instanceSocket.ok) {
|
|
7642
|
+
mintedSecret = await claimInstance(store, context.databaseFile, { pid: process.pid, port: console_.port, build: packageVersion(), socket: instanceSocket.name }, clock());
|
|
7643
|
+
instanceSecret = mintedSecret;
|
|
7644
|
+
if (mintedSecret === null)
|
|
7645
|
+
progress("attach: another console is running on this database — `toolroll` in a repository opens that one");
|
|
7646
|
+
}
|
|
7647
|
+
else
|
|
7648
|
+
progress(`attach: ${instanceSocket.problem} — \`toolroll\` in a repository won't find this console`);
|
|
7477
7649
|
if (fatal === null) {
|
|
7478
7650
|
if (json) {
|
|
7479
7651
|
write(envelopeJson({
|
|
@@ -7541,7 +7713,15 @@ async function upCommand(flags, context) {
|
|
|
7541
7713
|
process.removeListener("SIGINT", stop);
|
|
7542
7714
|
process.removeListener("SIGTERM", stop);
|
|
7543
7715
|
retireRunnerIfCurrent(store, runnerName, runnerToken, clock());
|
|
7544
|
-
|
|
7716
|
+
if (instanceSocket.ok)
|
|
7717
|
+
instanceSocket.close();
|
|
7718
|
+
if (mintedSecret !== null)
|
|
7719
|
+
clearInstance(store, mintedSecret);
|
|
7720
|
+
let closeProblem = null;
|
|
7721
|
+
await new Promise(done => console_.server.close(error => { closeProblem = error ?? null; done(); }));
|
|
7722
|
+
// A live terminal that couldn't be proved stopped (or any other close failure) is said, never "stopped".
|
|
7723
|
+
if (closeProblem !== null && fatal === null)
|
|
7724
|
+
fatal = closeProblem.message;
|
|
7545
7725
|
if (fatal !== null) {
|
|
7546
7726
|
// The startup envelope (when json) already went out; the exit code is
|
|
7547
7727
|
// the health signal (finding 11/20) — never a second envelope.
|
|
@@ -7671,7 +7851,7 @@ async function bridgeCommand(positional, flags, context) {
|
|
|
7671
7851
|
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);
|
|
7672
7852
|
}
|
|
7673
7853
|
const { name: asWho, token } = acting;
|
|
7674
|
-
const authenticated = authenticateApprover(store, asWho, token);
|
|
7854
|
+
const authenticated = (action === "pair" ? credentialApprover(store, asWho, token) : authenticateApprover(store, asWho, token));
|
|
7675
7855
|
if (!authenticated.ok) {
|
|
7676
7856
|
return fail(write, json, `bridge ${action}`, authenticated.reason, describeApproveFailure(authenticated.reason, asWho), EXIT.refused);
|
|
7677
7857
|
}
|
|
@@ -9451,7 +9631,7 @@ async function leadCommand(positional, flags, context) {
|
|
|
9451
9631
|
if (currentActor()?.lead === true)
|
|
9452
9632
|
return fail(write, json, command, "refused", "A lead cannot mint lead tokens. The person it acts for runs this with their password.", EXIT.refused);
|
|
9453
9633
|
const acting = await askCredentials(flags, context);
|
|
9454
|
-
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
9634
|
+
const verified = acting === null ? null : (flags.has("revoke") ? authenticateApprover(store, acting.name, acting.token) : credentialApprover(store, acting.name, acting.token));
|
|
9455
9635
|
if (acting === null || verified === null || !verified.ok) {
|
|
9456
9636
|
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);
|
|
9457
9637
|
}
|
|
@@ -10365,6 +10545,10 @@ function credentialsFrom(flags, context) {
|
|
|
10365
10545
|
if (remembered !== null && (name === undefined || name === remembered.name)) {
|
|
10366
10546
|
name = remembered.name;
|
|
10367
10547
|
token = remembered.password;
|
|
10548
|
+
// This command proves with the remembered login: under Passwordless that reads as "cli-login", never as typed.
|
|
10549
|
+
const holder = rememberedLogin.getStore();
|
|
10550
|
+
if (holder !== undefined)
|
|
10551
|
+
holder.login = { name, token };
|
|
10368
10552
|
}
|
|
10369
10553
|
}
|
|
10370
10554
|
return { name, token };
|
|
@@ -10385,6 +10569,12 @@ async function askCredentials(flags, context) {
|
|
|
10385
10569
|
* which scope.ts's authenticateAccount takes as that person (a viewer for a read token) and never checks as a
|
|
10386
10570
|
* password; these add the token's own project limit.
|
|
10387
10571
|
*/
|
|
10572
|
+
/** A credential act from the CLI (docs/plans/passwordless.md §5): a password typed for this command; with Passwordless on,
|
|
10573
|
+
* never the remembered login. */
|
|
10574
|
+
function credentialApprover(store, by, token, repo) {
|
|
10575
|
+
const proved = authenticateApprover(store, by, token, repo);
|
|
10576
|
+
return !proved.ok || credentialProof(proved.method) ? proved : { ok: false, reason: "not-an-approver" };
|
|
10577
|
+
}
|
|
10388
10578
|
function authenticateApprover(store, by, token, repo) {
|
|
10389
10579
|
const remote = activeRemote();
|
|
10390
10580
|
if (remote !== null && repo !== undefined && !remote.allows(repo))
|
|
@@ -10734,6 +10924,123 @@ async function projectRulesCommand(positional, flags, context) {
|
|
|
10734
10924
|
context.store.setApprovalRules(repo, next, acting.name, context.clock());
|
|
10735
10925
|
return succeed(context.write, context.json, command, { repo, rules: next }, () => [`${repo}: ${rulesSummary(next)}`]);
|
|
10736
10926
|
}
|
|
10927
|
+
/**
|
|
10928
|
+
* `project network --repo <p> [--allow h,…] [--remove h,…] [--open|--allowlist] [--local any|none]`: what the project's
|
|
10929
|
+
* builds may reach (egress.ts). New scopes are written with it and their approvals seal it; scopes already approved
|
|
10930
|
+
* keep the network they were approved with. Widening or narrowing it is an approver's act with a password typed now,
|
|
10931
|
+
* kept in the ledger as before → after.
|
|
10932
|
+
*/
|
|
10933
|
+
async function projectNetworkCommand(positional, flags, context) {
|
|
10934
|
+
const command = "project network";
|
|
10935
|
+
const allowed = new Set(["repo", "allow", "remove", "open", "allowlist", "local", "as", "token", "token-file", "token-env", "db", "json"]);
|
|
10936
|
+
for (const name of flags.keys())
|
|
10937
|
+
if (!allowed.has(name))
|
|
10938
|
+
return fail(context.write, context.json, command, "usage", `--${name} is not a project network option.`, EXIT.usage);
|
|
10939
|
+
const repoFlag = text(flags, "repo");
|
|
10940
|
+
const local = flags.get("local");
|
|
10941
|
+
if (positional.length > 1 || repoFlag === undefined || (flags.has("open") && flags.has("allowlist")) || (local !== undefined && local !== "any" && local !== "none")) {
|
|
10942
|
+
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);
|
|
10943
|
+
}
|
|
10944
|
+
const registered = await loadRepos(registryPathOf(context)).catch(() => ({ error: "unreadable" }));
|
|
10945
|
+
const known = [...new Set([...context.store.knownRepos(), ...context.store.listProjects().map(one => one.path), ...("error" in registered ? [] : registered.repos)])];
|
|
10946
|
+
const repo = known.find(one => one === repoFlag || one === resolve(repoFlag) || one === canonicalProject(repoFlag));
|
|
10947
|
+
if (repo === undefined)
|
|
10948
|
+
return fail(context.write, context.json, command, "not-found", "That isn't a project Toolroll knows.", EXIT.refused);
|
|
10949
|
+
const current = context.store.projectNetwork(repo);
|
|
10950
|
+
const words = (one) => [
|
|
10951
|
+
`${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(", ")}`}`}.`,
|
|
10952
|
+
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.",
|
|
10953
|
+
];
|
|
10954
|
+
const listOf = (name) => {
|
|
10955
|
+
const given = text(flags, name);
|
|
10956
|
+
if (given === undefined)
|
|
10957
|
+
return [];
|
|
10958
|
+
const hosts = given.split(",").map(one => one.trim()).filter(Boolean).map(normalizeHost);
|
|
10959
|
+
return hosts.some(one => one === null) ? null : hosts;
|
|
10960
|
+
};
|
|
10961
|
+
const add = listOf("allow"), remove = listOf("remove");
|
|
10962
|
+
if (add === null || remove === null)
|
|
10963
|
+
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);
|
|
10964
|
+
const changing = add.length > 0 || remove.length > 0 || flags.has("open") || flags.has("allowlist") || local !== undefined;
|
|
10965
|
+
if (!changing)
|
|
10966
|
+
return succeed(context.write, context.json, command, { repo, network: current }, () => words(current));
|
|
10967
|
+
const next = {
|
|
10968
|
+
mode: flags.has("open") ? "open" : flags.has("allowlist") ? "allowlist" : current.mode,
|
|
10969
|
+
hosts: [...new Set([...current.hosts.filter(one => !remove.includes(one)), ...add])].sort(),
|
|
10970
|
+
loopback: local === "any" ? "any" : local === "none" ? "proxy-only" : current.loopback,
|
|
10971
|
+
};
|
|
10972
|
+
if (next.hosts.length > MAX_NETWORK_HOSTS)
|
|
10973
|
+
return fail(context.write, context.json, command, "usage", `A project lists at most ${MAX_NETWORK_HOSTS} hosts.`, EXIT.usage);
|
|
10974
|
+
const acting = await askCredentials(flags, context);
|
|
10975
|
+
const verified = acting === null ? null : credentialApprover(context.store, acting.name, acting.token, repo);
|
|
10976
|
+
if (acting === null || verified === null || !verified.ok || !context.store.accountCanAccess(acting.name, repo)) {
|
|
10977
|
+
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);
|
|
10978
|
+
}
|
|
10979
|
+
const changed = context.store.setProjectNetwork(repo, next, acting.name, context.clock());
|
|
10980
|
+
return succeed(context.write, context.json, command, { repo, before: changed.before, network: changed.after }, () => [
|
|
10981
|
+
...words(changed.after),
|
|
10982
|
+
"Scopes written from now on use this; scopes already approved keep the network they were approved with.",
|
|
10983
|
+
]);
|
|
10984
|
+
}
|
|
10985
|
+
/**
|
|
10986
|
+
* `project writes --repo <p> [workspace|anywhere]`: where the project's builds may write (writes-mode.ts). `workspace`:
|
|
10987
|
+
* only their checkout and its Git data, their caches, their temp folder and the provider state their CLI needs;
|
|
10988
|
+
* `anywhere`: anywhere outside Toolroll's fenced folders. New scopes seal it; approved ones keep theirs. Changing it is
|
|
10989
|
+
* an approver's act with a password typed now, kept in the ledger as before → after.
|
|
10990
|
+
*/
|
|
10991
|
+
async function projectWritesCommand(positional, flags, context) {
|
|
10992
|
+
const command = "project writes";
|
|
10993
|
+
const allowed = new Set(["repo", "as", "token", "token-file", "token-env", "db", "json"]);
|
|
10994
|
+
for (const name of flags.keys())
|
|
10995
|
+
if (!allowed.has(name))
|
|
10996
|
+
return fail(context.write, context.json, command, "usage", `--${name} is not a project writes option.`, EXIT.usage);
|
|
10997
|
+
const repoFlag = text(flags, "repo");
|
|
10998
|
+
const wanted = positional[1];
|
|
10999
|
+
if (positional.length > 2 || repoFlag === undefined || (wanted !== undefined && !isWritesMode(wanted))) {
|
|
11000
|
+
return fail(context.write, context.json, command, "usage", "Use project writes --repo <project path> [workspace | anywhere].", EXIT.usage);
|
|
11001
|
+
}
|
|
11002
|
+
const registered = await loadRepos(registryPathOf(context)).catch(() => ({ error: "unreadable" }));
|
|
11003
|
+
const known = [...new Set([...context.store.knownRepos(), ...context.store.listProjects().map(one => one.path), ...("error" in registered ? [] : registered.repos)])];
|
|
11004
|
+
const repo = known.find(one => one === repoFlag || one === resolve(repoFlag) || one === canonicalProject(repoFlag));
|
|
11005
|
+
if (repo === undefined)
|
|
11006
|
+
return fail(context.write, context.json, command, "not-found", "That isn't a project Toolroll knows.", EXIT.refused);
|
|
11007
|
+
const words = (one) => [`${repo}: builds write ${writesWords(one)}.`];
|
|
11008
|
+
const current = context.store.projectWrites(repo);
|
|
11009
|
+
if (wanted === undefined)
|
|
11010
|
+
return succeed(context.write, context.json, command, { repo, writes: current }, () => words(current));
|
|
11011
|
+
const acting = await askCredentials(flags, context);
|
|
11012
|
+
const verified = acting === null ? null : credentialApprover(context.store, acting.name, acting.token, repo);
|
|
11013
|
+
if (acting === null || verified === null || !verified.ok || !context.store.accountCanAccess(acting.name, repo)) {
|
|
11014
|
+
return fail(context.write, context.json, command, "refused", "An approver for this project changes where its builds may write, with their password: pass --as and --token.", EXIT.refused);
|
|
11015
|
+
}
|
|
11016
|
+
const changed = context.store.setProjectWrites(repo, wanted, acting.name, context.clock());
|
|
11017
|
+
return succeed(context.write, context.json, command, { repo, before: changed.before, writes: changed.after }, () => [
|
|
11018
|
+
...words(changed.after),
|
|
11019
|
+
"Scopes written from now on use this; scopes already approved keep what they were approved with.",
|
|
11020
|
+
]);
|
|
11021
|
+
}
|
|
11022
|
+
/** `config unconfined-agents [allow|refuse]`: whether a build may run on a machine that can't confine it at the OS
|
|
11023
|
+
* (no macOS sandbox, no working bubblewrap). Refused unless an instance operator allows it, with a password. */
|
|
11024
|
+
async function unconfinedAgentsCommand(positional, flags, context) {
|
|
11025
|
+
const command = "config unconfined-agents";
|
|
11026
|
+
const { store, write, json } = context;
|
|
11027
|
+
const wanted = positional[1];
|
|
11028
|
+
if (positional.length > 2 || (wanted !== undefined && wanted !== "allow" && wanted !== "refuse")) {
|
|
11029
|
+
return fail(write, json, command, "usage", "Use config unconfined-agents [allow|refuse].", EXIT.usage);
|
|
11030
|
+
}
|
|
11031
|
+
const words = (allowed) => [allowed
|
|
11032
|
+
? "Builds run even where this machine can't confine them (each such run says \"Not confined\")."
|
|
11033
|
+
: "Builds this machine can't confine at the OS don't start."];
|
|
11034
|
+
if (wanted === undefined)
|
|
11035
|
+
return succeed(write, json, command, { allowed: store.unfencedAgentsAllowed() }, () => words(store.unfencedAgentsAllowed()));
|
|
11036
|
+
const acting = await askCredentials(flags, context);
|
|
11037
|
+
const verified = acting === null ? null : credentialApprover(store, acting.name, acting.token);
|
|
11038
|
+
if (acting === null || verified === null || !verified.ok || !store.isInstanceOperator(acting.name)) {
|
|
11039
|
+
return fail(write, json, command, "refused", "An instance operator changes this, with their password: pass --as and --token.", EXIT.refused);
|
|
11040
|
+
}
|
|
11041
|
+
store.setUnfencedAgentsAllowed(wanted === "allow", acting.name, context.clock());
|
|
11042
|
+
return succeed(write, json, command, { allowed: wanted === "allow" }, () => words(wanted === "allow"));
|
|
11043
|
+
}
|
|
10737
11044
|
/** `project concurrency [<n>] --repo <p>`: how many of a project's tasks build at once. Changing it is an approver's act. */
|
|
10738
11045
|
async function projectConcurrencyCommand(positional, flags, context) {
|
|
10739
11046
|
const command = "project concurrency";
|
|
@@ -11618,3 +11925,12 @@ function databaseFailure(write, json, command, file, error) {
|
|
|
11618
11925
|
return fail(write, json, command, "database", describe(error), EXIT.failed);
|
|
11619
11926
|
return fail(write, json, command, "database-busy", `The database ${file} stayed busy for ${CONCURRENT_WRITER_WAIT_MS / 1_000} seconds. Wait for the other Toolroll process to finish, then try again.`, EXIT.failed);
|
|
11620
11927
|
}
|
|
11928
|
+
// Last in the file, so the password tripwire's function positions (password-freshness.test.ts) stay as reviewed.
|
|
11929
|
+
/** The value flags a verb reads under a global switch's name, for parseOperateArgs. */
|
|
11930
|
+
export function ownValueFlags(command, argv) {
|
|
11931
|
+
if (command === "flows")
|
|
11932
|
+
return FLOWS_VALUE_FLAGS;
|
|
11933
|
+
if (command === "project" && argv.find(arg => !arg.startsWith("-")) === "network")
|
|
11934
|
+
return PROJECT_NETWORK_VALUE_FLAGS;
|
|
11935
|
+
return undefined;
|
|
11936
|
+
}
|