toolroll 0.9.63 → 0.9.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/README.md +15 -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 +50 -2
- package/dist/agent-fence.js +157 -10
- 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 +235 -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 +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 +12 -4
- package/dist/contracts/console-api.js +12 -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 +17 -0
- package/dist/invoke.js +217 -21
- 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 +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-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 +2 -0
- package/dist/operate.js +275 -17
- 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 +12 -0
- package/dist/project-delete-ui.js +41 -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.d.ts +3 -0
- package/dist/release.js +14 -8
- 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 +170 -5
- 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 +247 -40
- 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 +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 +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 +46 -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 +121 -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 +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-ui.js +4 -4
- package/dist/store-baseline.d.ts +1 -1
- package/dist/store-baseline.js +1 -1
- package/dist/store.d.ts +430 -15
- package/dist/store.js +1041 -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/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 +2 -0
- package/dist/tool-broker.js +1 -0
- package/dist/toolroll-update-ui.js +2 -2
- package/dist/toolroll-update.js +13 -3
- package/dist/tools-ui.js +2 -2
- 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/package.json +11 -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,7 @@ 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";
|
|
108
114
|
import { CLI_PASSWORD_SOURCE, withPasswordSource } from "./sign-in-guard.js";
|
|
109
115
|
import { admissionRecorded, admitProject, commandLineActor } from "./project-admission.js";
|
|
110
116
|
import { leadClaim, leadSay, noteLeadWork } from "./lead-voice.js";
|
|
@@ -182,7 +188,7 @@ import { register, authenticate, heartbeat as heartbeatRunner, isAlive, recoverD
|
|
|
182
188
|
import { mintCoordinator, revokeCoordinator, listCoordinators } from "./coordinator.js";
|
|
183
189
|
import { serveMcp } from "./mcp.js";
|
|
184
190
|
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";
|
|
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";
|
|
186
192
|
import { CHAT_APPROVE_ALL, presetTerms, modeTermsJson, modeDigestOf, modeTermsFromJson, modeWords, MODE_MAX_DAYS } from "./modes.js";
|
|
187
193
|
import { WorktreePool } from "./worktree.js";
|
|
188
194
|
import { baseStandingWords, freshBaseFor, taskBaseOf } from "./task-base.js";
|
|
@@ -251,6 +257,11 @@ export const OPERATE_HELP = `toolroll — operating the queue
|
|
|
251
257
|
toolroll project show show the current project and credential reference
|
|
252
258
|
toolroll project rules --repo <p> a project's approval rules; an instance operator changes them with
|
|
253
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)
|
|
254
265
|
toolroll project concurrency [<n>] --repo <p>
|
|
255
266
|
how many of a project's tasks build at once (default 2, never past the
|
|
256
267
|
worker's capacity); an approver changes it with <n>
|
|
@@ -752,7 +763,8 @@ export function parseOperateArgs(argv, ownValues = new Set()) {
|
|
|
752
763
|
}
|
|
753
764
|
/** Route a native command and return its exit code. Its password source cannot be selected by an HTTP request. */
|
|
754
765
|
export async function runOperate(command, argv, write, options = {}) {
|
|
755
|
-
|
|
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)));
|
|
756
768
|
}
|
|
757
769
|
async function runOperateLocal(command, argv, write, options = {}) {
|
|
758
770
|
const parsed = parseOperateArgs(argv, command === "flows" ? FLOWS_VALUE_FLAGS : undefined);
|
|
@@ -1145,6 +1157,8 @@ async function dispatch(command, positional, flags, context) {
|
|
|
1145
1157
|
return projectDeleteCommand(positional, flags, context);
|
|
1146
1158
|
if (positional[0] === "concurrency")
|
|
1147
1159
|
return projectConcurrencyCommand(positional, flags, context);
|
|
1160
|
+
if (positional[0] === "network")
|
|
1161
|
+
return projectNetworkCommand(positional, flags, context);
|
|
1148
1162
|
if (positional[0] === "checks")
|
|
1149
1163
|
return projectChecksCommand(positional, flags, context);
|
|
1150
1164
|
if (positional[0] === "demo")
|
|
@@ -1223,6 +1237,8 @@ async function dispatch(command, positional, flags, context) {
|
|
|
1223
1237
|
case "flows":
|
|
1224
1238
|
return flowsCommand(positional, flags, context);
|
|
1225
1239
|
case "config":
|
|
1240
|
+
if (positional[0] === "unconfined-agents")
|
|
1241
|
+
return unconfinedAgentsCommand(positional, flags, context);
|
|
1226
1242
|
return configCommand(positional, flags, context);
|
|
1227
1243
|
case "chat":
|
|
1228
1244
|
return chatCommand(flags, context);
|
|
@@ -1243,6 +1259,8 @@ async function dispatch(command, positional, flags, context) {
|
|
|
1243
1259
|
});
|
|
1244
1260
|
case "keys":
|
|
1245
1261
|
return keysCommand(positional, flags, context);
|
|
1262
|
+
case "agents":
|
|
1263
|
+
return agentsCommand(positional, flags, context);
|
|
1246
1264
|
case "setup":
|
|
1247
1265
|
return setupCommand(positional, flags, context);
|
|
1248
1266
|
case "verify":
|
|
@@ -1488,6 +1506,9 @@ function claimCommand(positional, flags, context) {
|
|
|
1488
1506
|
if (result.reason === "coordinator-filed") {
|
|
1489
1507
|
return fail(write, json, "claim", "coordinator-filed", "a coordinator filed this — nothing runs until the operator signs its scope", EXIT.refused);
|
|
1490
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
|
+
}
|
|
1491
1512
|
return fail(write, json, "claim", result.reason, `held by ${result.by} until ${result.until}`, EXIT.refused, { holder: result.by, until: result.until });
|
|
1492
1513
|
}
|
|
1493
1514
|
// Taking a task is what makes it running; leaving that to the caller would
|
|
@@ -1606,7 +1627,7 @@ async function runnerCommand(positional, flags, context) {
|
|
|
1606
1627
|
if (acting === null) {
|
|
1607
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);
|
|
1608
1629
|
}
|
|
1609
|
-
const authenticated =
|
|
1630
|
+
const authenticated = credentialApprover(store, acting.name, acting.token);
|
|
1610
1631
|
if (!authenticated.ok) {
|
|
1611
1632
|
return fail(write, json, "runner register", authenticated.reason, describeApproveFailure(authenticated.reason, name), EXIT.refused);
|
|
1612
1633
|
}
|
|
@@ -1732,7 +1753,7 @@ async function runnerCommand(positional, flags, context) {
|
|
|
1732
1753
|
if (acting === null) {
|
|
1733
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);
|
|
1734
1755
|
}
|
|
1735
|
-
const authenticated =
|
|
1756
|
+
const authenticated = credentialApprover(store, acting.name, acting.token);
|
|
1736
1757
|
if (!authenticated.ok) {
|
|
1737
1758
|
return fail(write, json, "runner bind", authenticated.reason, describeApproveFailure(authenticated.reason, name), EXIT.refused);
|
|
1738
1759
|
}
|
|
@@ -1818,7 +1839,7 @@ async function coordinatorCommand(positional, flags, context) {
|
|
|
1818
1839
|
if (acting === null) {
|
|
1819
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);
|
|
1820
1841
|
}
|
|
1821
|
-
const authenticated =
|
|
1842
|
+
const authenticated = credentialApprover(store, acting.name, acting.token);
|
|
1822
1843
|
if (!authenticated.ok) {
|
|
1823
1844
|
return fail(write, json, "coordinator mint", authenticated.reason, describeApproveFailure(authenticated.reason, name), EXIT.refused);
|
|
1824
1845
|
}
|
|
@@ -2292,6 +2313,17 @@ async function tickCommand(flags, context) {
|
|
|
2292
2313
|
root: pool,
|
|
2293
2314
|
...(context.gitRunner === undefined ? {} : { runner: context.gitRunner }),
|
|
2294
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
|
+
}
|
|
2295
2327
|
// Where a first attempt's branch grows from, resolved once per pass. A
|
|
2296
2328
|
// detached HEAD refuses the pass rather than guessing: an unattended commit
|
|
2297
2329
|
// onto "wherever the operator happened to be" is not a default.
|
|
@@ -2616,6 +2648,20 @@ async function tickCommand(flags, context) {
|
|
|
2616
2648
|
: [store.getScope(id)?.approvedProfile?.provider].filter((one) => one !== null && one !== undefined);
|
|
2617
2649
|
let unattestedLane = null;
|
|
2618
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
|
+
}
|
|
2619
2665
|
if (attestationOf(candidate) === null)
|
|
2620
2666
|
continue;
|
|
2621
2667
|
let verdict = attestedThisPass.get(candidate);
|
|
@@ -2646,7 +2692,9 @@ async function tickCommand(flags, context) {
|
|
|
2646
2692
|
// budget is raised (the 100% alert went out; the task page and the spend page say why).
|
|
2647
2693
|
// Budgets are dollars: work that runs only on subscriptions passes (see Store.budgetGate).
|
|
2648
2694
|
// Each provider as it bills now.
|
|
2649
|
-
|
|
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]));
|
|
2650
2698
|
if (budgeted.over !== null) {
|
|
2651
2699
|
dispatched.push({ id, outcome: "skipped", reason: "budget", detail: budgetWords(budgeted) });
|
|
2652
2700
|
continue;
|
|
@@ -3900,6 +3948,7 @@ async function startConsole(options) {
|
|
|
3900
3948
|
...(options.registryPath === undefined ? {} : { registryPath: options.registryPath }),
|
|
3901
3949
|
...(options.upConsole === undefined ? {} : { upConsole: options.upConsole }),
|
|
3902
3950
|
...(options.editorLinks === undefined ? {} : { editorLinks: options.editorLinks }),
|
|
3951
|
+
...(options.instanceSecret === undefined ? {} : { instanceSecret: options.instanceSecret }),
|
|
3903
3952
|
});
|
|
3904
3953
|
await new Promise((resolve, reject) => {
|
|
3905
3954
|
server.once("error", reject);
|
|
@@ -4035,6 +4084,8 @@ async function requeueTask(positional, flags, context) {
|
|
|
4035
4084
|
claimed: `a runner holds ${id} right now — wait for the attempt to end, or stop it`,
|
|
4036
4085
|
"accepted-result": `${id}'s last result was accepted; change it through a revision, not a rerun`,
|
|
4037
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`,
|
|
4038
4089
|
};
|
|
4039
4090
|
return fail(write, json, "task requeue", result.reason, why[result.reason], EXIT.refused);
|
|
4040
4091
|
}
|
|
@@ -4317,6 +4368,13 @@ async function providersCommand(flags, context) {
|
|
|
4317
4368
|
// that serializes four of them reads as a hang (Phase 3).
|
|
4318
4369
|
const probed = await Promise.all(PROVIDER_IDS.map(async (id) => {
|
|
4319
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
|
+
}
|
|
4320
4378
|
// A version probe carries no key (Codex gemini verify, finding 2).
|
|
4321
4379
|
const version = await probe(facts.binary, ["--version"], { timeoutMs: 5_000, omitEnv: ALL_CREDENTIAL_ENV });
|
|
4322
4380
|
const installed = version.code === 0 && !version.notFound;
|
|
@@ -4373,7 +4431,7 @@ async function providersCommand(flags, context) {
|
|
|
4373
4431
|
if (runnerName === undefined || runnerToken === undefined) {
|
|
4374
4432
|
return fail(write, json, "providers", "usage", "`toolroll providers --report --runner <name> --token <t>` records readiness under that runner", EXIT.usage);
|
|
4375
4433
|
}
|
|
4376
|
-
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));
|
|
4377
4435
|
const reported = reportProviderReadinessAuthed(store, { name: runnerName, token: runnerToken, observations: observed }, context.clock());
|
|
4378
4436
|
if (!reported.ok) {
|
|
4379
4437
|
return fail(write, json, "providers", reported.reason, describeAuth(reported.reason, runnerName), EXIT.refused);
|
|
@@ -4444,6 +4502,83 @@ async function providersCommand(flags, context) {
|
|
|
4444
4502
|
* own — these are 0600 files under the operator's home, and whoever can
|
|
4445
4503
|
* write them already owns the machine's spend.
|
|
4446
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
|
+
}
|
|
4447
4582
|
async function keysCommand(positional, flags, context) {
|
|
4448
4583
|
const { write, json } = context;
|
|
4449
4584
|
const [action, provider, ...rest] = positional;
|
|
@@ -4451,7 +4586,8 @@ async function keysCommand(positional, flags, context) {
|
|
|
4451
4586
|
return fail(write, json, "keys", "usage", `unknown \`keys ${action ?? ""}\` — try ${KEYS_ACTIONS.join(", ")}`, EXIT.usage);
|
|
4452
4587
|
}
|
|
4453
4588
|
if (action === "status") {
|
|
4454
|
-
|
|
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 => ({
|
|
4455
4591
|
provider: one,
|
|
4456
4592
|
envName: PROVIDER_KEY_ENV[one],
|
|
4457
4593
|
...keyStatus(one),
|
|
@@ -4475,7 +4611,10 @@ async function keysCommand(positional, flags, context) {
|
|
|
4475
4611
|
return EXIT.ok;
|
|
4476
4612
|
}
|
|
4477
4613
|
if (provider === undefined || !isProviderId(provider)) {
|
|
4478
|
-
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);
|
|
4479
4618
|
}
|
|
4480
4619
|
if (action === "clear") {
|
|
4481
4620
|
const cleared = clearProviderKey(provider);
|
|
@@ -4580,7 +4719,8 @@ async function peopleCommand(positional, flags, context) {
|
|
|
4580
4719
|
if (acting === null) {
|
|
4581
4720
|
return fail(write, json, `people ${action}`, "usage", `\`toolroll people ${action} … --as <you> --token <t>\``, EXIT.usage);
|
|
4582
4721
|
}
|
|
4583
|
-
|
|
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));
|
|
4584
4724
|
if (!authenticated.ok) {
|
|
4585
4725
|
return fail(write, json, `people ${action}`, authenticated.reason, describeApproveFailure(authenticated.reason, acting.name), EXIT.refused);
|
|
4586
4726
|
}
|
|
@@ -4647,7 +4787,8 @@ async function chatApprovalCommand(positional, flags, context) {
|
|
|
4647
4787
|
const acting = await askCredentials(flags, context);
|
|
4648
4788
|
if (acting === null)
|
|
4649
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);
|
|
4650
|
-
|
|
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));
|
|
4651
4792
|
if (!authed.ok)
|
|
4652
4793
|
return fail(write, json, command, "unauthenticated", "That name and password aren't an approver's. Nothing was changed.", EXIT.refused);
|
|
4653
4794
|
const given = text(flags, "repo");
|
|
@@ -4710,7 +4851,8 @@ async function modeCommand(positional, flags, context) {
|
|
|
4710
4851
|
if (acting === null) {
|
|
4711
4852
|
return fail(write, json, "mode", "usage", "signing or revoking a mode takes `--as <you> --token <t>`", EXIT.usage);
|
|
4712
4853
|
}
|
|
4713
|
-
|
|
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));
|
|
4714
4856
|
if (!authed.ok) {
|
|
4715
4857
|
return fail(write, json, "mode", authed.reason, "that is not an active approver, or the token does not match", EXIT.refused);
|
|
4716
4858
|
}
|
|
@@ -6816,7 +6958,7 @@ async function resolveUpApprover(context, flags, canPrompt) {
|
|
|
6816
6958
|
return { approver: selected, approvers: names, verified, passwordFile, mintedPassword: null, notes };
|
|
6817
6959
|
}
|
|
6818
6960
|
/** The platform browser opener — detached, silent, never load-bearing. */
|
|
6819
|
-
function openBrowser(url) {
|
|
6961
|
+
export function openBrowser(url) {
|
|
6820
6962
|
// TOOLROLL_BROWSER_COMMAND names another program to open it with (it gets the address as its one argument).
|
|
6821
6963
|
const chosen = envValue(process.env, "BROWSER_COMMAND");
|
|
6822
6964
|
const command = chosen !== undefined && chosen !== "" ? [chosen, url]
|
|
@@ -7182,9 +7324,12 @@ async function upCommand(flags, context) {
|
|
|
7182
7324
|
// proved set on each request, so its project rail and unified chat move
|
|
7183
7325
|
// with the live builder instead of freezing at process start.
|
|
7184
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;
|
|
7185
7329
|
let console_;
|
|
7186
7330
|
try {
|
|
7187
7331
|
console_ = await startConsole({
|
|
7332
|
+
instanceSecret: () => instanceSecret,
|
|
7188
7333
|
context,
|
|
7189
7334
|
host: hostFlag,
|
|
7190
7335
|
port,
|
|
@@ -7442,7 +7587,7 @@ async function upCommand(flags, context) {
|
|
|
7442
7587
|
const observeReadiness = () => {
|
|
7443
7588
|
if (context.agentRunner !== undefined)
|
|
7444
7589
|
return;
|
|
7445
|
-
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))
|
|
7446
7591
|
.then(observed => {
|
|
7447
7592
|
if (!stopping)
|
|
7448
7593
|
reportProviderReadinessAuthed(store, { name: runnerName, token: runnerToken, observations: observed }, clock());
|
|
@@ -7474,6 +7619,21 @@ async function upCommand(flags, context) {
|
|
|
7474
7619
|
login: { account: approverPlan2.passwordFile === null ? approverPlan2.approver : loginAccount(approverPlan2.passwordFile) ?? approverPlan2.approver, file: approverPlan2.passwordFile },
|
|
7475
7620
|
});
|
|
7476
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`);
|
|
7477
7637
|
if (fatal === null) {
|
|
7478
7638
|
if (json) {
|
|
7479
7639
|
write(envelopeJson({
|
|
@@ -7541,7 +7701,15 @@ async function upCommand(flags, context) {
|
|
|
7541
7701
|
process.removeListener("SIGINT", stop);
|
|
7542
7702
|
process.removeListener("SIGTERM", stop);
|
|
7543
7703
|
retireRunnerIfCurrent(store, runnerName, runnerToken, clock());
|
|
7544
|
-
|
|
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;
|
|
7545
7713
|
if (fatal !== null) {
|
|
7546
7714
|
// The startup envelope (when json) already went out; the exit code is
|
|
7547
7715
|
// the health signal (finding 11/20) — never a second envelope.
|
|
@@ -7671,7 +7839,7 @@ async function bridgeCommand(positional, flags, context) {
|
|
|
7671
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);
|
|
7672
7840
|
}
|
|
7673
7841
|
const { name: asWho, token } = acting;
|
|
7674
|
-
const authenticated = authenticateApprover(store, asWho, token);
|
|
7842
|
+
const authenticated = (action === "pair" ? credentialApprover(store, asWho, token) : authenticateApprover(store, asWho, token));
|
|
7675
7843
|
if (!authenticated.ok) {
|
|
7676
7844
|
return fail(write, json, `bridge ${action}`, authenticated.reason, describeApproveFailure(authenticated.reason, asWho), EXIT.refused);
|
|
7677
7845
|
}
|
|
@@ -9451,7 +9619,7 @@ async function leadCommand(positional, flags, context) {
|
|
|
9451
9619
|
if (currentActor()?.lead === true)
|
|
9452
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);
|
|
9453
9621
|
const acting = await askCredentials(flags, context);
|
|
9454
|
-
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));
|
|
9455
9623
|
if (acting === null || verified === null || !verified.ok) {
|
|
9456
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);
|
|
9457
9625
|
}
|
|
@@ -10365,6 +10533,10 @@ function credentialsFrom(flags, context) {
|
|
|
10365
10533
|
if (remembered !== null && (name === undefined || name === remembered.name)) {
|
|
10366
10534
|
name = remembered.name;
|
|
10367
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 };
|
|
10368
10540
|
}
|
|
10369
10541
|
}
|
|
10370
10542
|
return { name, token };
|
|
@@ -10385,6 +10557,12 @@ async function askCredentials(flags, context) {
|
|
|
10385
10557
|
* which scope.ts's authenticateAccount takes as that person (a viewer for a read token) and never checks as a
|
|
10386
10558
|
* password; these add the token's own project limit.
|
|
10387
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
|
+
}
|
|
10388
10566
|
function authenticateApprover(store, by, token, repo) {
|
|
10389
10567
|
const remote = activeRemote();
|
|
10390
10568
|
if (remote !== null && repo !== undefined && !remote.allows(repo))
|
|
@@ -10734,6 +10912,86 @@ async function projectRulesCommand(positional, flags, context) {
|
|
|
10734
10912
|
context.store.setApprovalRules(repo, next, acting.name, context.clock());
|
|
10735
10913
|
return succeed(context.write, context.json, command, { repo, rules: next }, () => [`${repo}: ${rulesSummary(next)}`]);
|
|
10736
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
|
+
}
|
|
10737
10995
|
/** `project concurrency [<n>] --repo <p>`: how many of a project's tasks build at once. Changing it is an approver's act. */
|
|
10738
10996
|
async function projectConcurrencyCommand(positional, flags, context) {
|
|
10739
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.";
|
package/dist/peek-cli.d.ts
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* One pane per open run: its task, worker, role, stage word, elapsed
|
|
5
5
|
* clock, and the tail of its live transcript (the same per-run file the
|
|
6
|
-
* console's run page follows:
|
|
7
|
-
*
|
|
8
|
-
* shapes redacted at write). Digits focus one pane, `a` shows all, `q`
|
|
6
|
+
* console's run page follows: what the agent said and each step it took —
|
|
7
|
+
* a command's text only where its project shows commands, never file
|
|
8
|
+
* contents, credential shapes redacted at write and again at read). Digits focus one pane, `a` shows all, `q`
|
|
9
9
|
* leaves. No dependency: the screen is the alternate buffer and a
|
|
10
10
|
* handful of escape sequences. `--tmux` opens a real tmux session with one
|
|
11
11
|
* window per run for people who live there. Outside a TTY (or with
|
|
@@ -36,8 +36,16 @@ type Tail = {
|
|
|
36
36
|
offset: number;
|
|
37
37
|
lines: string[];
|
|
38
38
|
state: PeekPane["transcript"];
|
|
39
|
+
gen?: string;
|
|
40
|
+
inKey?: boolean;
|
|
41
|
+
keyTail?: string;
|
|
39
42
|
};
|
|
40
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Advance one run's tail from its offset; `replaced` resets visibly. The
|
|
45
|
+
* offset always moves by the bytes read, even when nothing in them shows
|
|
46
|
+
* (a finished step, a skipped line), and an offset is only ever used
|
|
47
|
+
* against the file it came from.
|
|
48
|
+
*/
|
|
41
49
|
export declare function advanceTail(root: string, runId: number, tail: Tail): Tail;
|
|
42
50
|
/** The stage word for a pane: the machine's own phase, or what precedes it. */
|
|
43
51
|
export declare function stageWord(run: {
|