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/project-tools.js
CHANGED
|
@@ -29,6 +29,7 @@ import { fileURLToPath } from "node:url";
|
|
|
29
29
|
import { scanForSecrets } from "./evidence.js";
|
|
30
30
|
import { namedPath } from "./names.js";
|
|
31
31
|
import { startToolBroker } from "./tool-broker.js";
|
|
32
|
+
import { isAcpProvider } from "./acp-agents.js";
|
|
32
33
|
import { SecretStoreError, destroyOwnFile, prepareSecretFolder, registerSecretFolder, pathIn, readOwnFile, readSealed, removeValue, secretBackend, secretFolder, secretStorage, serviceOf, withSecretLock, withSecretLockAsync, writeOwnFile, writeSealed } from "./tool-secret-store.js";
|
|
33
34
|
import { ACCESS, GRANTED, REFRESH, mintRunToken, mintsRunTokens, oneAtATime, runScope, scopeList, signedIn } from "./oauth-run-token.js";
|
|
34
35
|
import { HEADER_NAME, SECRET_NAME, TOOL_NAME as NAME, TOOL_SPEC_LIMITS as LIMITS, readToolSpec } from "./contracts/integration-metadata.js";
|
|
@@ -710,7 +711,7 @@ export async function claudeToolArgs(launch, options = {}) {
|
|
|
710
711
|
omitEnv: [],
|
|
711
712
|
cleanup,
|
|
712
713
|
privateDir: dir,
|
|
713
|
-
...(broker === null ? {} : { serverPids: broker.pids }),
|
|
714
|
+
...(broker === null ? {} : { serverPids: broker.pids, brokerPort: broker.port }),
|
|
714
715
|
};
|
|
715
716
|
}
|
|
716
717
|
/** Where the tool launcher lives beside this module, built. */
|
|
@@ -749,7 +750,7 @@ export async function codexToolArgs(launch, options = {}) {
|
|
|
749
750
|
if (enabled !== undefined)
|
|
750
751
|
argv.push("-c", `${key}.enabled_tools=${tomlList(enabled)}`);
|
|
751
752
|
}
|
|
752
|
-
return { argv, env: {}, omitEnv: [], cleanup, privateDir: dir, serverPids: broker.pids };
|
|
753
|
+
return { argv, env: {}, omitEnv: [], cleanup, privateDir: dir, serverPids: broker.pids, brokerPort: broker.port };
|
|
753
754
|
}
|
|
754
755
|
/** Gemini: no way yet to hand it exactly these servers, so it gets none. */
|
|
755
756
|
export const GEMINI_NO_TOOLS_ARGV = ["--allowed-mcp-server-names", "standing-orders-none"];
|
|
@@ -846,8 +847,9 @@ export function secretToolRefusal(provider, containment) {
|
|
|
846
847
|
export async function prepareRunTools(store, runId, provider, options) {
|
|
847
848
|
const launch = limitedTo(toolLaunchFor(store, runId, options.home), options.readOnly, options.withheld);
|
|
848
849
|
// A local server with secrets starts only where the build can't read its environment; the rest are left out,
|
|
849
|
-
// saying why, before the broker exists. Gemini
|
|
850
|
-
const
|
|
850
|
+
// saying why, before the broker exists. Gemini and ACP agents start none (below).
|
|
851
|
+
const toolless = provider === "gemini" || isAcpProvider(provider);
|
|
852
|
+
const refusal = toolless ? null : secretToolRefusal(provider, options.containment);
|
|
851
853
|
if (refusal !== null) {
|
|
852
854
|
launch.tools = launch.tools.filter(one => {
|
|
853
855
|
if (one.spec.transport === "http" || Object.keys(one.values).length === 0)
|
|
@@ -856,14 +858,15 @@ export async function prepareRunTools(store, runId, provider, options) {
|
|
|
856
858
|
return false;
|
|
857
859
|
});
|
|
858
860
|
}
|
|
859
|
-
const revokes =
|
|
861
|
+
const revokes = toolless ? [] : await signInRun(launch, { readOnly: options.readOnly !== undefined, now: options.now, ...(options.home === undefined ? {} : { home: options.home }), ...(options.fetcher === undefined ? {} : { fetcher: options.fetcher }) });
|
|
860
862
|
const revoke = () => Promise.all(revokes.splice(0).map(one => one())).then(() => undefined);
|
|
861
863
|
const offered = { ...(options.readOnly === undefined ? {} : { enabledTools: options.readOnly }), ...(options.broker === undefined ? {} : { broker: options.broker }) };
|
|
862
864
|
let launched;
|
|
863
865
|
try {
|
|
864
866
|
launched = provider === "claude" ? await claudeToolArgs(launch, offered)
|
|
865
867
|
: provider === "codex" || provider === "openrouter" ? await codexToolArgs(launch, offered)
|
|
866
|
-
: { argv: [...GEMINI_NO_TOOLS_ARGV], env: {}, omitEnv: [], cleanup: () => undefined }
|
|
868
|
+
: provider === "gemini" ? { argv: [...GEMINI_NO_TOOLS_ARGV], env: {}, omitEnv: [], cleanup: () => undefined }
|
|
869
|
+
: { argv: [], env: {}, omitEnv: [], cleanup: () => undefined };
|
|
867
870
|
}
|
|
868
871
|
catch (error) {
|
|
869
872
|
// Tokens were made for a run that won't start: each is revoked, and the run with them.
|
|
@@ -874,13 +877,13 @@ export async function prepareRunTools(store, runId, provider, options) {
|
|
|
874
877
|
}
|
|
875
878
|
// The run's own sign-ins end with it: the broker closes first, so nothing uses a token as it is revoked.
|
|
876
879
|
const prepared = revokes.length === 0 ? launched : { ...launched, cleanup: () => { launched.cleanup(); void revoke(); } };
|
|
877
|
-
const skipped =
|
|
878
|
-
? [...launch.skipped, ...launch.tools.map(one => ({ name: one.spec.name, reason: "Gemini builds can't use project tools yet" }))]
|
|
880
|
+
const skipped = toolless
|
|
881
|
+
? [...launch.skipped, ...launch.tools.map(one => ({ name: one.spec.name, reason: provider === "gemini" ? "Gemini builds can't use project tools yet" : "ACP agents can't use project tools yet" }))]
|
|
879
882
|
: launch.skipped;
|
|
880
883
|
try {
|
|
881
884
|
store.recordRunTools(runId, JSON.stringify({
|
|
882
885
|
provider,
|
|
883
|
-
tools:
|
|
886
|
+
tools: toolless ? [] : launch.tools.map(one => ({ name: one.spec.name, digest: one.digest })),
|
|
884
887
|
skipped,
|
|
885
888
|
}), options.now);
|
|
886
889
|
}
|
package/dist/provider-auth.d.ts
CHANGED
|
@@ -14,16 +14,17 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import type { Store } from "./store.js";
|
|
16
16
|
import { type ProviderId } from "./provider.js";
|
|
17
|
+
import type { AuthMode } from "./keys.js";
|
|
17
18
|
/** Every sign-in message's dedupe key starts here: `signin:<kind>:<provider>:<ordinal>:<person>`.
|
|
18
19
|
* Not a lifecycle (`life:`) key: those are progress chatter, and an undelivered
|
|
19
20
|
* one stays out of the attention list — this one needs a person. */
|
|
20
21
|
export declare const SIGN_IN_KEY_PREFIX = "signin:";
|
|
21
|
-
export declare const PROVIDER_AUTH_SCHEMA = "\nCREATE TABLE IF NOT EXISTS provider_auth_pause (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n provider TEXT NOT NULL,\n ordinal INTEGER NOT NULL,\n auth_mode TEXT NOT NULL DEFAULT 'subscription' CHECK (auth_mode IN ('subscription', 'api-key')),\n opened_at TEXT NOT NULL,\n first_run INTEGER,\n task_ref INTEGER,\n runs INTEGER NOT NULL DEFAULT 1,\n -- 1 once a sign-in probe during this pause said the provider is signed out:\n -- only a probe that then sees it signed in again may lift the pause.\n probe_saw_out INTEGER NOT NULL DEFAULT 0,\n lifted_at TEXT,\n lifted_by TEXT,\n lifted_how TEXT CHECK (lifted_how IS NULL OR lifted_how IN ('run', 'probe', 'person')),\n resumed INTEGER,\n -- The last task let through as a trial while paused: a sign-in check can\n -- say \"logged in\" for a session that no longer works, so a real run decides.\n last_trial_at TEXT,\n UNIQUE (provider, ordinal)\n);\nCREATE UNIQUE INDEX IF NOT EXISTS provider_auth_pause_open ON provider_auth_pause (provider) WHERE lifted_at IS NULL;\n";
|
|
22
|
+
export declare const PROVIDER_AUTH_SCHEMA = "\nCREATE TABLE IF NOT EXISTS provider_auth_pause (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n provider TEXT NOT NULL,\n ordinal INTEGER NOT NULL,\n auth_mode TEXT NOT NULL DEFAULT 'subscription' CHECK (auth_mode IN ('subscription', 'api-key', 'agent-login')),\n opened_at TEXT NOT NULL,\n first_run INTEGER,\n task_ref INTEGER,\n runs INTEGER NOT NULL DEFAULT 1,\n -- 1 once a sign-in probe during this pause said the provider is signed out:\n -- only a probe that then sees it signed in again may lift the pause.\n probe_saw_out INTEGER NOT NULL DEFAULT 0,\n lifted_at TEXT,\n lifted_by TEXT,\n lifted_how TEXT CHECK (lifted_how IS NULL OR lifted_how IN ('run', 'probe', 'person')),\n resumed INTEGER,\n -- The last task let through as a trial while paused: a sign-in check can\n -- say \"logged in\" for a session that no longer works, so a real run decides.\n last_trial_at TEXT,\n UNIQUE (provider, ordinal)\n);\nCREATE UNIQUE INDEX IF NOT EXISTS provider_auth_pause_open ON provider_auth_pause (provider) WHERE lifted_at IS NULL;\n";
|
|
22
23
|
export type AuthPause = {
|
|
23
24
|
id: number;
|
|
24
25
|
provider: ProviderId;
|
|
25
26
|
ordinal: number;
|
|
26
|
-
authMode:
|
|
27
|
+
authMode: AuthMode;
|
|
27
28
|
openedAt: string;
|
|
28
29
|
firstRun: number | null;
|
|
29
30
|
taskRef: number | null;
|
|
@@ -63,7 +64,7 @@ export declare function openAuthPauses(store: Store): AuthPause[];
|
|
|
63
64
|
*/
|
|
64
65
|
export declare function pauseForAuth(store: Store, args: {
|
|
65
66
|
provider: string;
|
|
66
|
-
authMode:
|
|
67
|
+
authMode: AuthMode | null;
|
|
67
68
|
runId: number;
|
|
68
69
|
taskRef: number;
|
|
69
70
|
now: Date;
|
package/dist/provider-auth.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PROVIDER_IDS } from "./provider.js";
|
|
2
|
+
import { withAcp } from "./acp-agents.js";
|
|
2
3
|
/** Every sign-in message's dedupe key starts here: `signin:<kind>:<provider>:<ordinal>:<person>`.
|
|
3
4
|
* Not a lifecycle (`life:`) key: those are progress chatter, and an undelivered
|
|
4
5
|
* one stays out of the attention list — this one needs a person. */
|
|
@@ -8,7 +9,7 @@ CREATE TABLE IF NOT EXISTS provider_auth_pause (
|
|
|
8
9
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
9
10
|
provider TEXT NOT NULL,
|
|
10
11
|
ordinal INTEGER NOT NULL,
|
|
11
|
-
auth_mode TEXT NOT NULL DEFAULT 'subscription' CHECK (auth_mode IN ('subscription', 'api-key')),
|
|
12
|
+
auth_mode TEXT NOT NULL DEFAULT 'subscription' CHECK (auth_mode IN ('subscription', 'api-key', 'agent-login')),
|
|
12
13
|
opened_at TEXT NOT NULL,
|
|
13
14
|
first_run INTEGER,
|
|
14
15
|
task_ref INTEGER,
|
|
@@ -27,8 +28,8 @@ CREATE TABLE IF NOT EXISTS provider_auth_pause (
|
|
|
27
28
|
);
|
|
28
29
|
CREATE UNIQUE INDEX IF NOT EXISTS provider_auth_pause_open ON provider_auth_pause (provider) WHERE lifted_at IS NULL;
|
|
29
30
|
`;
|
|
30
|
-
const NAMES = { claude: "Claude", codex: "Codex", openrouter: "OpenRouter", gemini: "Gemini" };
|
|
31
|
-
const SIGN_IN = { claude: "claude /login", codex: "codex login", gemini: "gemini", openrouter: "toolroll keys set openrouter" };
|
|
31
|
+
const NAMES = withAcp({ claude: "Claude", codex: "Codex", openrouter: "OpenRouter", gemini: "Gemini" }, agent => agent.name);
|
|
32
|
+
const SIGN_IN = withAcp({ claude: "claude /login", codex: "codex login", gemini: "gemini", openrouter: "toolroll keys set openrouter" }, agent => agent.signIn);
|
|
32
33
|
export function providerName(provider) {
|
|
33
34
|
return PROVIDER_IDS.includes(provider) ? NAMES[provider] : provider;
|
|
34
35
|
}
|
|
@@ -57,7 +58,7 @@ function readPause(row) {
|
|
|
57
58
|
id: Number(row["id"]),
|
|
58
59
|
provider: String(row["provider"]),
|
|
59
60
|
ordinal: Number(row["ordinal"]),
|
|
60
|
-
authMode: row["auth_mode"] === "api-key" ? "api-key" : "subscription",
|
|
61
|
+
authMode: row["auth_mode"] === "api-key" ? "api-key" : row["auth_mode"] === "agent-login" ? "agent-login" : "subscription",
|
|
61
62
|
openedAt: String(row["opened_at"]),
|
|
62
63
|
firstRun: row["first_run"] == null ? null : Number(row["first_run"]),
|
|
63
64
|
taskRef: row["task_ref"] == null ? null : Number(row["task_ref"]),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { run, type ExecResult } from "./exec.js";
|
|
2
2
|
import { type ProviderId } from "./provider.js";
|
|
3
|
+
import { type KeyedProviderId } from "./acp-agents.js";
|
|
3
4
|
import { type AuthMode, type KeyVerdict } from "./keys.js";
|
|
4
5
|
export type ProviderConnection = {
|
|
5
6
|
state: "connected" | "signed-out" | "not-installed" | "unverified" | "key-present" | "key-works" | "key-refused" | "missing-key";
|
|
@@ -18,5 +19,5 @@ export declare function createConnectionChecker(options?: {
|
|
|
18
19
|
home?: string;
|
|
19
20
|
clock?: () => Date;
|
|
20
21
|
env?: NodeJS.ProcessEnv;
|
|
21
|
-
verify?: (provider:
|
|
22
|
+
verify?: (provider: KeyedProviderId, key: string) => Promise<KeyVerdict>;
|
|
22
23
|
}): ConnectionChecker;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { run } from "./exec.js";
|
|
4
4
|
import { ALL_CREDENTIAL_ENV, inspectionOf } from "./provider.js";
|
|
5
|
+
import { isAcpProvider } from "./acp-agents.js";
|
|
5
6
|
import { keyStatus, PROVIDER_KEY_ENV, readAuthMode, readProviderKey, verifyProviderKey } from "./keys.js";
|
|
6
7
|
export function signInFacts(provider, result, now = new Date()) {
|
|
7
8
|
const base = { mode: "subscription", checkedAt: now.toISOString() };
|
|
@@ -46,6 +47,10 @@ export function createConnectionChecker(options = {}) {
|
|
|
46
47
|
const cache = new Map();
|
|
47
48
|
const pending = new Map();
|
|
48
49
|
return async (provider, fresh = false) => {
|
|
50
|
+
// An ACP agent is never run to ask how it is signed in (nothing executes it outside the gateway): what is known
|
|
51
|
+
// of it is its conformance check, read elsewhere. Here it is simply unverified.
|
|
52
|
+
if (isAcpProvider(provider))
|
|
53
|
+
return { state: "unverified", mode: "agent-login", checkedAt: clock().toISOString() };
|
|
49
54
|
const mode = readAuthMode(provider, home);
|
|
50
55
|
const keyFacts = keyStatus(provider, home);
|
|
51
56
|
const ambient = !!(options.env ?? process.env)[PROVIDER_KEY_ENV[provider]];
|
package/dist/provider.d.ts
CHANGED
|
@@ -175,6 +175,29 @@ export type ParsedEnvelope = {
|
|
|
175
175
|
* failure terminal seen.
|
|
176
176
|
*/
|
|
177
177
|
structuralTerminal: import("./exhaustion.js").StructuralTerminal | null;
|
|
178
|
+
/** What an ACP agent itself reported it used — kept on the run, never priced. Absent for every other dialect. */
|
|
179
|
+
agentReportedUsage?: string | null;
|
|
180
|
+
/** An ACP conversation's facts the conformance check reads (acp-client.ts AcpEnvelope): nothing else does. */
|
|
181
|
+
acp?: {
|
|
182
|
+
offered: string[];
|
|
183
|
+
exit: {
|
|
184
|
+
how: "eof" | "term" | "kill" | "self" | "error";
|
|
185
|
+
leftover: boolean;
|
|
186
|
+
timedOut: boolean;
|
|
187
|
+
} | null;
|
|
188
|
+
agent: {
|
|
189
|
+
name: string;
|
|
190
|
+
version: string;
|
|
191
|
+
} | null;
|
|
192
|
+
stop: string | null;
|
|
193
|
+
/** The conversation broke the protocol (acpParse's protocolError), or null. */
|
|
194
|
+
protocolError: string | null;
|
|
195
|
+
/** The plane's isolation probes for a check's first step (invoke.ts), when it ran them. */
|
|
196
|
+
isolation?: {
|
|
197
|
+
fileDenied: boolean;
|
|
198
|
+
envClear: boolean;
|
|
199
|
+
};
|
|
200
|
+
};
|
|
178
201
|
};
|
|
179
202
|
type Adapter = {
|
|
180
203
|
binary: string;
|
|
@@ -266,13 +289,13 @@ export declare function adapterFor(provider: ProviderId): Adapter;
|
|
|
266
289
|
*/
|
|
267
290
|
export type ProviderAudit = {
|
|
268
291
|
/** How output reaches us — and therefore which signals can exist at all. */
|
|
269
|
-
transport: "buffered-json" | "streaming-jsonl";
|
|
292
|
+
transport: "buffered-json" | "streaming-jsonl" | "acp-jsonrpc";
|
|
270
293
|
/** Whether a later invocation can resume this provider's session. */
|
|
271
294
|
resume: "native" | "none";
|
|
272
295
|
/** The event whose absence on a failed run means "never initialized".
|
|
273
296
|
* CAPABILITY metadata, not a per-run observation (arc 1 finding 16):
|
|
274
297
|
* whether a given run actually saw it lives in ParsedEnvelope.initObserved. */
|
|
275
|
-
initSignal: "thread.started" | "system-init" | "init-event" | "none";
|
|
298
|
+
initSignal: "thread.started" | "system-init" | "init-event" | "acp-session" | "none";
|
|
276
299
|
/** How session identity is established: "announced" = read back from the
|
|
277
300
|
* harness's own stream; "minted" = the PLANE chooses the id pre-spawn
|
|
278
301
|
* (gemini --session-id) and the envelope must echo it (Phase 3 A5). */
|
package/dist/provider.js
CHANGED
|
@@ -18,13 +18,16 @@
|
|
|
18
18
|
* TOML-quoted deterministically, and the API key excluded from every
|
|
19
19
|
* shell the model itself launches (Codex provider review, Q5).
|
|
20
20
|
*/
|
|
21
|
+
import { INTERPRETER_INJECTION_ENV } from "./acp-attest.js";
|
|
21
22
|
import { claudeFenceSettings, codexFenceArgv } from "./agent-fence.js";
|
|
22
23
|
import { claudeSteerHooks } from "./steer-mailbox.js";
|
|
23
24
|
import {} from "./exec.js";
|
|
24
|
-
import { runStreamJsonl, runClaudeStreamJsonl, runGeminiStreamJsonl, CLAUDE_COST_UNKNOWN } from "./exec.js";
|
|
25
|
+
import { runStreamJsonl, runClaudeStreamJsonl, runGeminiStreamJsonl, runAcpSession, CLAUDE_COST_UNKNOWN } from "./exec.js";
|
|
25
26
|
import { scanForSecrets } from "./evidence.js";
|
|
26
27
|
import { FINDINGS_MODEL_SCHEMA } from "./contracts/review-findings.js";
|
|
27
28
|
import { REVIEW_OUTPUT_LIMITS } from "./structured-output.js";
|
|
29
|
+
import { ACP_AGENTS, isAcpProvider, withAcp } from "./acp-agents.js";
|
|
30
|
+
import { acpParse } from "./acp-client.js";
|
|
28
31
|
import { agentSpecSchema, MODEL_ID, modelIdSchema, PROVIDER_ID_VALUES, providerIdSchema } from "./contracts/provider.js";
|
|
29
32
|
export const PROVIDER_IDS = PROVIDER_ID_VALUES;
|
|
30
33
|
export function isProviderId(value) {
|
|
@@ -787,12 +790,13 @@ export function safeDiagnostic(text) {
|
|
|
787
790
|
* provider gets its OWN keys and nobody else's: S4 (live spike) confirmed
|
|
788
791
|
* what inheritance makes true by construction — an agent's shell reads its
|
|
789
792
|
* process env, so a foreign key in that env is a foreign key disclosed. */
|
|
790
|
-
const CREDENTIAL_ENV = {
|
|
793
|
+
const CREDENTIAL_ENV = withAcp({
|
|
791
794
|
claude: ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"],
|
|
792
795
|
codex: ["OPENAI_API_KEY", "CODEX_API_KEY"],
|
|
793
796
|
openrouter: [OPENROUTER_ENV_KEY],
|
|
794
797
|
gemini: ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
|
|
795
|
-
|
|
798
|
+
// An ACP agent signs in its own way; Toolroll holds no key for it, so it sheds every key above.
|
|
799
|
+
}, () => []);
|
|
796
800
|
const foreignCredentialEnv = (own) => Object.entries(CREDENTIAL_ENV)
|
|
797
801
|
.filter(([provider]) => provider !== own)
|
|
798
802
|
.flatMap(([, keys]) => [...keys]);
|
|
@@ -800,7 +804,40 @@ const foreignCredentialEnv = (own) => Object.entries(CREDENTIAL_ENV)
|
|
|
800
804
|
* since a feature check needs no key at all (Codex gemini verify,
|
|
801
805
|
* finding 2). Agent spawns keep their own; probes keep none. */
|
|
802
806
|
export const ALL_CREDENTIAL_ENV = Object.values(CREDENTIAL_ENV).flat();
|
|
803
|
-
|
|
807
|
+
/**
|
|
808
|
+
* An ACP agent's retained record (acp-client.ts) read into the gateway's
|
|
809
|
+
* envelope. Its own usage report is never priced: tokens and cost stay null.
|
|
810
|
+
*/
|
|
811
|
+
function acpEnvelope(stdout) {
|
|
812
|
+
const read = acpParse(stdout);
|
|
813
|
+
return {
|
|
814
|
+
sessionId: read.sessionId,
|
|
815
|
+
protocolError: read.protocolError,
|
|
816
|
+
finalMessage: read.finalMessage,
|
|
817
|
+
tokensIn: null,
|
|
818
|
+
tokensOut: null,
|
|
819
|
+
costUsd: null,
|
|
820
|
+
usageRaw: null,
|
|
821
|
+
initObserved: read.initObserved,
|
|
822
|
+
promptConsumed: read.promptConsumed,
|
|
823
|
+
diagnostic: read.diagnostic === null ? null : safeDiagnostic(read.diagnostic),
|
|
824
|
+
structuralTerminal: read.terminal === null ? null : { failed: true, text: read.terminal.text === null ? null : safeDiagnostic(read.terminal.text), code: read.terminal.code.slice(0, 128) },
|
|
825
|
+
agentReportedUsage: read.reportedUsage,
|
|
826
|
+
acp: { offered: read.offered, exit: read.exit, agent: read.agent, stop: read.stop, protocolError: read.protocolError },
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
function acpAdapter(provider) {
|
|
830
|
+
return {
|
|
831
|
+
binary: ACP_AGENTS[provider].binary,
|
|
832
|
+
argv: () => [...ACP_AGENTS[provider].argv],
|
|
833
|
+
parse: acpEnvelope,
|
|
834
|
+
defaultRunner: runAcpSession,
|
|
835
|
+
// Every key Toolroll knows, and whatever would make the pinned interpreter load code beside the checked script.
|
|
836
|
+
extraOmitEnv: [...foreignCredentialEnv(provider), ...INTERPRETER_INJECTION_ENV],
|
|
837
|
+
clampTimeout: (_phase, requested) => requested,
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
const ADAPTERS = withAcp({
|
|
804
841
|
claude: {
|
|
805
842
|
binary: "claude",
|
|
806
843
|
argv: claudeArgv,
|
|
@@ -852,7 +889,7 @@ const ADAPTERS = {
|
|
|
852
889
|
extraOmitEnv: foreignCredentialEnv("gemini"),
|
|
853
890
|
clampTimeout: (phase, requested) => Math.min(requested, GEMINI_TIMEOUT_CAP_MS[phase]),
|
|
854
891
|
},
|
|
855
|
-
};
|
|
892
|
+
}, (_agent, provider) => acpAdapter(provider));
|
|
856
893
|
/**
|
|
857
894
|
* The spawning surface. Imported by invoke.ts and NOWHERE else — the
|
|
858
895
|
* architecture test reads imports, not string literals, because provider
|
|
@@ -861,7 +898,7 @@ const ADAPTERS = {
|
|
|
861
898
|
export function adapterFor(provider) {
|
|
862
899
|
return ADAPTERS[provider];
|
|
863
900
|
}
|
|
864
|
-
const AUDITS = {
|
|
901
|
+
const AUDITS = withAcp({
|
|
865
902
|
claude: {
|
|
866
903
|
transport: "streaming-jsonl",
|
|
867
904
|
resume: "native",
|
|
@@ -923,8 +960,17 @@ const AUDITS = {
|
|
|
923
960
|
"trusted-folder store (S2, live at 0.57.0: headless REFUSES untrusted directories; the plane grants --skip-trust per-invocation to its own leased worktrees)",
|
|
924
961
|
],
|
|
925
962
|
},
|
|
926
|
-
}
|
|
927
|
-
|
|
963
|
+
}, agent => ({
|
|
964
|
+
// One ACP conversation per build: the prompt's answer is the only success (acp-client.ts), so exit 0 alone is never believed.
|
|
965
|
+
transport: "acp-jsonrpc",
|
|
966
|
+
resume: "none",
|
|
967
|
+
initSignal: "acp-session",
|
|
968
|
+
sessionIdentity: "announced",
|
|
969
|
+
terminalContract: "required",
|
|
970
|
+
isolation: { flag: agent.argv.filter(one => one.startsWith("--") && one !== "--acp").join(" ") || null, resumeSafe: null, enforced: agent.argv.some(one => one.startsWith("--") && one !== "--acp") },
|
|
971
|
+
configSurface: [...agent.configSurface],
|
|
972
|
+
}));
|
|
973
|
+
export const MONEY_CAPABILITIES = withAcp({
|
|
928
974
|
claude: {
|
|
929
975
|
incrementalUsage: true,
|
|
930
976
|
nativeDollarCapFlag: "--max-budget-usd",
|
|
@@ -947,7 +993,12 @@ export const MONEY_CAPABILITIES = {
|
|
|
947
993
|
// numbers cover that invocation only (conformance fixture j).
|
|
948
994
|
usageSemantics: "per-invocation",
|
|
949
995
|
},
|
|
950
|
-
}
|
|
996
|
+
}, () => ({
|
|
997
|
+
// ACP carries no dollars Toolroll trusts: an agent's own usage report is kept apart, unpriced.
|
|
998
|
+
incrementalUsage: false,
|
|
999
|
+
nativeDollarCapFlag: null,
|
|
1000
|
+
usageSemantics: "per-invocation",
|
|
1001
|
+
}));
|
|
951
1002
|
/**
|
|
952
1003
|
* The fail-closed budget-flag probe (finding 24's amendment): resolve
|
|
953
1004
|
* the EXACT executable that will spawn, read its version, and prove the
|
|
@@ -997,6 +1048,9 @@ export function validateSpec(spec) {
|
|
|
997
1048
|
if (spec.provider === "gemini" && spec.model === null) {
|
|
998
1049
|
return { ok: false, problem: "model: gemini needs an explicit model — the harness default drifts with its releases" };
|
|
999
1050
|
}
|
|
1051
|
+
if (isAcpProvider(spec.provider) && spec.model === null) {
|
|
1052
|
+
return { ok: false, problem: `model: ${ACP_AGENTS[spec.provider].name} needs an explicit model — one it offered when it was checked` };
|
|
1053
|
+
}
|
|
1000
1054
|
return { ok: true };
|
|
1001
1055
|
}
|
|
1002
1056
|
/**
|
package/dist/pull-request-ui.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/** Settings → Projects → Pull requests: one project's pull-request setup, in plain words. Off shows what setup
|
|
2
2
|
* found (or what to fix) and one Turn on; on shows where pull requests go and how they merge. Every change takes
|
|
3
3
|
* the password; turning on restates the exact repository and branch that were checked. */
|
|
4
|
-
import { html, postForm } from "./html.js";
|
|
4
|
+
import { html, postForm, reauth } from "./html.js";
|
|
5
5
|
const METHOD_WORDS = { squash: "Squash and merge", merge: "Merge commit", rebase: "Rebase and merge" };
|
|
6
6
|
export function pullRequestSettingsHtml(view) {
|
|
7
7
|
const post = (act, body, hidden = {}) => postForm("/settings/pull-requests", body, { attrs: { class: "card pr-settings" }, hidden: { repo: view.repo, act, ...hidden } });
|
|
8
|
-
const password = html `<label>Password<input type="password" name="password" autocomplete="current-password" required></label
|
|
8
|
+
const password = html `${reauth(html `<label>Password<input type="password" name="password" autocomplete="current-password" required></label>`)}`;
|
|
9
9
|
const notes = [
|
|
10
10
|
view.said === null ? "" : html `<p class="notice" role="status">${view.said}</p>`,
|
|
11
11
|
view.problem === null ? "" : html `<p class="problem" role="alert">${view.problem}</p>`,
|
package/dist/push.js
CHANGED
|
@@ -259,7 +259,7 @@ export async function pushPass(store, options) {
|
|
|
259
259
|
// Renew and re-fence immediately before the network (finding 12).
|
|
260
260
|
if (!store.renewPushClaim(pair.id, owner, pair.claimGeneration, PUSH_CLAIM_TTL_MS, clock()))
|
|
261
261
|
continue;
|
|
262
|
-
const fenced = store.pushSendFence(pair.id, owner, pair.claimGeneration);
|
|
262
|
+
const fenced = store.pushSendFence(pair.id, owner, pair.claimGeneration, clock());
|
|
263
263
|
if (fenced === null)
|
|
264
264
|
continue; // resolved, retired, rotated, or lost — nothing to send
|
|
265
265
|
const subscription = fenced.subscriptionRow;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type AttachSeams } from "./console-instance.js";
|
|
2
|
+
export type QuickStartSeams = {
|
|
3
|
+
databaseFile: string;
|
|
4
|
+
cwd: string;
|
|
5
|
+
env: Record<string, string | undefined>;
|
|
6
|
+
build: string;
|
|
7
|
+
/** The repository's top folder for a directory, or null outside one. */
|
|
8
|
+
findRepo: (cwd: string) => Promise<string | null>;
|
|
9
|
+
/** Whether this installation already admitted the repository. */
|
|
10
|
+
enrolled: (repo: string) => Promise<boolean>;
|
|
11
|
+
/** The ordinary admission (`repos add`): its registry lock and ceilings, from this CLI's own database. */
|
|
12
|
+
admit: (repo: string) => Promise<number>;
|
|
13
|
+
/** `up`, for this repository. */
|
|
14
|
+
startUp: () => Promise<number>;
|
|
15
|
+
openBrowser: (url: string) => void | Promise<void>;
|
|
16
|
+
terminal: boolean;
|
|
17
|
+
attach?: AttachSeams;
|
|
18
|
+
/** Tests: the agent check (default: the environment and the process's ancestors). */
|
|
19
|
+
underAgent?: boolean;
|
|
20
|
+
};
|
|
21
|
+
/** Whether argv is a bare `toolroll` (nothing, or only --json). */
|
|
22
|
+
export declare const isBareInvocation: (argv: readonly string[]) => boolean;
|
|
23
|
+
/** null: not this command (outside a repository) — the caller scans as before. */
|
|
24
|
+
export declare function runQuickStart(argv: readonly string[], write: (line: string) => void, seams: QuickStartSeams): Promise<number | null>;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BARE `toolroll` IN A REPOSITORY (docs/plans/agents-cockpit.md B1): the two-action start.
|
|
3
|
+
*
|
|
4
|
+
* Inside a git repository, `toolroll` with nothing else (or only --json) either starts the console for it (`up`), or —
|
|
5
|
+
* when this installation's console is already running and PROVES it is (console-instance.ts: a fresh challenge on the
|
|
6
|
+
* discovery socket and on its HTTP listener, answered with the secret in the database this CLI opened itself) —
|
|
7
|
+
* adds the repository through the ordinary project admission and opens that proved console. Under an agent, or with
|
|
8
|
+
* --json, it starts nothing and changes nothing: it says what it would do. Outside a repository, it scans as before
|
|
9
|
+
* (the scan is also `toolroll scan`).
|
|
10
|
+
*/
|
|
11
|
+
import { lstatSync, realpathSync } from "node:fs";
|
|
12
|
+
import { proveLiveConsole } from "./console-instance.js";
|
|
13
|
+
import { underAgent } from "./prompt.js";
|
|
14
|
+
import { envelopeJson } from "./envelope.js";
|
|
15
|
+
import { createRequire } from "node:module";
|
|
16
|
+
import { openStoreReadOnly, readSchemaVersion, SCHEMA_VERSION } from "./store.js";
|
|
17
|
+
const EXIT = { ok: 0, refused: 3 };
|
|
18
|
+
/** Whether argv is a bare `toolroll` (nothing, or only --json). */
|
|
19
|
+
export const isBareInvocation = (argv) => argv.every(one => one === "--json");
|
|
20
|
+
/** The database's schema version by the store's own strict reader, read without changing anything. */
|
|
21
|
+
function schemaOf(file) {
|
|
22
|
+
try {
|
|
23
|
+
// Loaded only here, as the store loads it: the CLI's other commands never pay for (or warn about) it.
|
|
24
|
+
const { DatabaseSync } = createRequire(import.meta.url)("node:sqlite");
|
|
25
|
+
const db = new DatabaseSync(file, { readOnly: true });
|
|
26
|
+
try {
|
|
27
|
+
const read = readSchemaVersion(db);
|
|
28
|
+
return read.ok ? read : { ok: false };
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
db.close();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return { ok: false };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Whether the database file is there: "absent" only when the system says so (ENOENT), never for a path it won't show. */
|
|
39
|
+
function databasePresence(file) {
|
|
40
|
+
try {
|
|
41
|
+
lstatSync(file);
|
|
42
|
+
return "present";
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
return error.code === "ENOENT" ? "absent" : "unknown";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function prove(seams) {
|
|
49
|
+
// Read only, never created or migrated, never cleaned up: proving changes nothing. "None" only when nothing of this
|
|
50
|
+
// build can be running on it (no database yet, or one older than this build, which `up` brings up to date); a
|
|
51
|
+
// database that can't be read, is mid-upgrade or is newer than this build can't be checked, so it is refused.
|
|
52
|
+
const none = { ok: false, state: "none", message: "no console is running" };
|
|
53
|
+
const unverifiable = (why) => ({ ok: false, state: "refused", message: `${why}, so a running console can't be checked — run \`toolroll up\` to see what's wrong` });
|
|
54
|
+
const presence = databasePresence(seams.databaseFile);
|
|
55
|
+
if (presence === "absent")
|
|
56
|
+
return none;
|
|
57
|
+
if (presence === "unknown")
|
|
58
|
+
return unverifiable("Toolroll's database can't be reached");
|
|
59
|
+
const schema = schemaOf(seams.databaseFile);
|
|
60
|
+
if (!schema.ok)
|
|
61
|
+
return unverifiable("Toolroll's database can't be read");
|
|
62
|
+
if (schema.version === null || (schema.version > 0 && schema.version < SCHEMA_VERSION))
|
|
63
|
+
return none;
|
|
64
|
+
if (schema.version !== SCHEMA_VERSION)
|
|
65
|
+
return unverifiable("Toolroll's database is being upgraded");
|
|
66
|
+
const store = openStoreReadOnly(seams.databaseFile);
|
|
67
|
+
if (store === null)
|
|
68
|
+
return unverifiable("Toolroll's database can't be read");
|
|
69
|
+
try {
|
|
70
|
+
return await proveLiveConsole(seams.databaseFile, store, seams.build, seams.attach ?? {});
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return unverifiable("the running console couldn't be checked");
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
store.close();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** null: not this command (outside a repository) — the caller scans as before. */
|
|
80
|
+
export async function runQuickStart(argv, write, seams) {
|
|
81
|
+
const found = await seams.findRepo(seams.cwd);
|
|
82
|
+
if (found === null)
|
|
83
|
+
return null;
|
|
84
|
+
let repo = found;
|
|
85
|
+
try {
|
|
86
|
+
repo = realpathSync(found);
|
|
87
|
+
}
|
|
88
|
+
catch { /* as found */ }
|
|
89
|
+
const json = argv.includes("--json");
|
|
90
|
+
const dry = json || (seams.underAgent ?? underAgent(seams.env));
|
|
91
|
+
const proof = await prove(seams);
|
|
92
|
+
const enrolled = await seams.enrolled(repo);
|
|
93
|
+
const would = proof.ok ? (enrolled ? "open" : "add-and-open") : proof.state === "none" ? "start" : "refuse";
|
|
94
|
+
if (dry) {
|
|
95
|
+
// Nothing starts, nothing is added, nothing opens: what a person would get, in words or one envelope.
|
|
96
|
+
const words = would === "start" ? `Would start Toolroll for ${repo} (toolroll up).`
|
|
97
|
+
: would === "refuse" ? `Would refuse: ${proof.message}.`
|
|
98
|
+
: `Would ${would === "add-and-open" ? `add ${repo} and ` : ""}open the running console at ${proof.url}.`;
|
|
99
|
+
if (json)
|
|
100
|
+
write(envelopeJson({ ok: would !== "refuse", command: "toolroll", repo, would, ...(proof.ok ? { url: proof.url } : { console: proof.state, message: proof.message }), dryRun: true }));
|
|
101
|
+
else
|
|
102
|
+
write(words);
|
|
103
|
+
return would === "refuse" ? EXIT.refused : EXIT.ok;
|
|
104
|
+
}
|
|
105
|
+
if (!proof.ok && proof.state === "refused") {
|
|
106
|
+
write(`Toolroll can't attach: ${proof.message}.`);
|
|
107
|
+
return EXIT.refused;
|
|
108
|
+
}
|
|
109
|
+
if (!proof.ok)
|
|
110
|
+
return seams.startUp();
|
|
111
|
+
if (!enrolled) {
|
|
112
|
+
const admitted = await seams.admit(repo);
|
|
113
|
+
if (admitted !== 0)
|
|
114
|
+
return admitted;
|
|
115
|
+
}
|
|
116
|
+
// Whatever happened since the first proof (reading the registry, adding the project), the console is proved again,
|
|
117
|
+
// on both channels, right before its address is shown or opened — never a listener that took the port meanwhile.
|
|
118
|
+
const again = await prove(seams);
|
|
119
|
+
if (!again.ok || again.url !== proof.url || again.pid !== proof.pid) {
|
|
120
|
+
write(`Toolroll can't attach: ${again.ok ? "the console changed while the project was being added" : again.message} — run \`toolroll up\`.`);
|
|
121
|
+
return EXIT.refused;
|
|
122
|
+
}
|
|
123
|
+
write(`Toolroll is on ${proof.url}`);
|
|
124
|
+
if (seams.terminal)
|
|
125
|
+
await seams.openBrowser(proof.url);
|
|
126
|
+
return EXIT.ok;
|
|
127
|
+
}
|
package/dist/release.d.ts
CHANGED
|
@@ -226,10 +226,13 @@ export type ReleaseJournal = {
|
|
|
226
226
|
by: string;
|
|
227
227
|
at: string;
|
|
228
228
|
} | null;
|
|
229
|
+
/** The passing result: its run, receipt and checkout, and the commit it built (`head`), which a prepared candidate
|
|
230
|
+
* records as a commit of its own with exactly the candidate's tree (absent in a journal from before that was known). */
|
|
229
231
|
check: {
|
|
230
232
|
run: number;
|
|
231
233
|
receipt: string;
|
|
232
234
|
worktree: string;
|
|
235
|
+
head?: string;
|
|
233
236
|
} | null;
|
|
234
237
|
completion: {
|
|
235
238
|
actor: string;
|
package/dist/release.js
CHANGED
|
@@ -213,9 +213,11 @@ export async function runRelease(options, adapters) {
|
|
|
213
213
|
if (gate.result.check !== "passed")
|
|
214
214
|
throw new Stop("check-failed", `The Full release check of ${j.candidate.sha.slice(0, 7)} did not pass (run ${gate.result.run}: ${gate.result.check}). It stays as it is: fix the branch and release the new commit with --new.`);
|
|
215
215
|
};
|
|
216
|
+
/** The commit the passing build recorded: what the deployed runtime is stamped with. */
|
|
217
|
+
const builtHead = () => j.check?.head ?? j.candidate.sha;
|
|
216
218
|
const checked = (gate) => {
|
|
217
219
|
approved(gate);
|
|
218
|
-
if (gate.result?.head !==
|
|
220
|
+
if (gate.result?.head !== builtHead() || gate.result?.receipt !== j.check?.receipt)
|
|
219
221
|
throw new Stop("drift", `Task ${j.task.id}'s result changed since its check passed.`);
|
|
220
222
|
fullCheck(gate);
|
|
221
223
|
};
|
|
@@ -268,12 +270,16 @@ export async function runRelease(options, adapters) {
|
|
|
268
270
|
throw new Stop("check-failed", `The release build of ${j.task.id} failed: ${gate.failed}. Fix it and release the new commit with --new.`);
|
|
269
271
|
if (gate.result === null || gate.result.check === "running" || gate.result.check === "pending")
|
|
270
272
|
return null;
|
|
271
|
-
|
|
272
|
-
|
|
273
|
+
// The build records a prepared candidate as a commit of its own (its message names the task): it must hold
|
|
274
|
+
// exactly the candidate's tree, or it isn't the release.
|
|
275
|
+
const head = gate.result.head;
|
|
276
|
+
if (head === null || (head !== j.candidate.sha && await adapters.repo.tree(head) !== await adapters.repo.tree(j.candidate.sha))) {
|
|
277
|
+
throw new Stop("drift", `The release result is for ${head?.slice(0, 7) ?? "no commit"}, which doesn't hold exactly ${j.candidate.sha.slice(0, 7)}'s files.`);
|
|
278
|
+
}
|
|
273
279
|
fullCheck(gate);
|
|
274
280
|
if (gate.result.worktree === null)
|
|
275
281
|
throw new Stop("worktree", `Run ${gate.result.run} names no checkout to deploy from.`);
|
|
276
|
-
return { run: gate.result.run, receipt: gate.result.receipt, worktree: gate.result.worktree };
|
|
282
|
+
return { run: gate.result.run, receipt: gate.result.receipt, worktree: gate.result.worktree, head };
|
|
277
283
|
});
|
|
278
284
|
j.check = result;
|
|
279
285
|
say(` run ${result.run} passed on ${j.candidate.sha.slice(0, 7)}`);
|
|
@@ -310,7 +316,7 @@ export async function runRelease(options, adapters) {
|
|
|
310
316
|
await recoverDeploy();
|
|
311
317
|
checked(await adapters.toolroll.gate(j.task.id));
|
|
312
318
|
// A deployment that finished but whose answer was lost: the services already run a runtime of this commit.
|
|
313
|
-
let runtime = await served(adapters,
|
|
319
|
+
let runtime = await served(adapters, builtHead());
|
|
314
320
|
if (runtime === null) {
|
|
315
321
|
await pinned();
|
|
316
322
|
j.deployAttempt = { stage: join(options.stateDir, "staged-upgrades", `browser-${j.candidate.sha.slice(0, 7)}-${randomUUID()}`) };
|
|
@@ -326,7 +332,7 @@ export async function runRelease(options, adapters) {
|
|
|
326
332
|
save();
|
|
327
333
|
}
|
|
328
334
|
const want = runtime;
|
|
329
|
-
await waitFor("deploy", `the console and worker to run the gated commit from ${want}`, async () => (await served(adapters,
|
|
335
|
+
await waitFor("deploy", `the console and worker to run the gated commit from ${want}`, async () => (await served(adapters, builtHead())) === want ? true : null);
|
|
330
336
|
j.deploy = { runtime: want, at: j.deploy?.runtime === want ? j.deploy.at : adapters.now().toISOString() };
|
|
331
337
|
j.deployAttempt = null;
|
|
332
338
|
delete j.intents.deploy;
|
|
@@ -336,7 +342,7 @@ export async function runRelease(options, adapters) {
|
|
|
336
342
|
}
|
|
337
343
|
case "link": {
|
|
338
344
|
const runtime = j.deploy.runtime;
|
|
339
|
-
if (await served(adapters,
|
|
345
|
+
if (await served(adapters, builtHead()) !== runtime)
|
|
340
346
|
throw new Stop("drift", `The console and worker no longer run ${runtime}; the CLI names were not moved.`);
|
|
341
347
|
if (j.links === undefined || j.links === null) {
|
|
342
348
|
j.links = await adapters.toolroll.cliLinks();
|
|
@@ -348,7 +354,7 @@ export async function runRelease(options, adapters) {
|
|
|
348
354
|
catch (error) {
|
|
349
355
|
throw new Stop("link", `The CLI names were not moved to ${runtime} (they point where they did): ${error.message}. The service already runs the new build; rerun to try again.`);
|
|
350
356
|
}
|
|
351
|
-
const everywhere = await deployed(adapters,
|
|
357
|
+
const everywhere = await deployed(adapters, builtHead());
|
|
352
358
|
if (everywhere !== runtime)
|
|
353
359
|
throw new Stop("mixed-runtime", `After relinking, the console, worker and both CLI names don't all run ${runtime} at ${j.candidate.sha.slice(0, 7)}.`);
|
|
354
360
|
say(` toolroll and standing-orders run ${join(runtime, "bin.js")}`);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { html, postForm } from "./html.js";
|
|
1
|
+
import { html, postForm, reauth } from "./html.js";
|
|
2
2
|
import { effectiveLimits, limitOverrides, PER_DAY_MAX, PER_MINUTE_MAX, REQUEST_BUDGET_DEFAULTS } from "./request-budget.js";
|
|
3
3
|
const n = (value) => value.toLocaleString("en-US");
|
|
4
4
|
export const REQUEST_LIMITS_CSS = `.credentials .limits{display:grid;gap:14px}.credentials .limits form{display:grid;gap:10px}.credentials .limits label{display:grid;gap:6px}` +
|
|
@@ -14,7 +14,7 @@ export function requestLimitsHtml(db, tokens, now) {
|
|
|
14
14
|
const overrides = limitOverrides(db);
|
|
15
15
|
const everyone = overrides.get("*") ?? { readPerMinute: null, actPerMinute: null, perDay: null };
|
|
16
16
|
const field = (name, label, value, placeholder, max) => html `<label>${label}<input type="number" name="${name}" min="1" max="${max}" step="1" inputmode="numeric" value="${value === null ? "" : value}" placeholder="${placeholder}"></label>`;
|
|
17
|
-
const password = html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password"></label
|
|
17
|
+
const password = html `${reauth(html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password"></label>`)}`;
|
|
18
18
|
const buttons = html `<div class="actions"><button name="action" value="save">Save limits</button><button class="secondary" name="action" value="clear">Use defaults</button></div>`;
|
|
19
19
|
const live = tokens.filter(one => one.revokedAt === null && Date.parse(one.expiresAt) > now);
|
|
20
20
|
const installation = postForm("/settings/request-limits", html `\
|