toolroll 0.9.62 → 0.9.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +80 -0
- package/README.md +66 -8
- package/dist/acp-agents.d.ts +43 -0
- package/dist/acp-agents.js +62 -0
- package/dist/acp-attest.d.ts +44 -0
- package/dist/acp-attest.js +269 -0
- package/dist/acp-check.d.ts +28 -0
- package/dist/acp-check.js +410 -0
- package/dist/acp-client.d.ts +163 -0
- package/dist/acp-client.js +738 -0
- package/dist/acp-inspect.d.ts +23 -0
- package/dist/acp-inspect.js +109 -0
- package/dist/agent-board.d.ts +19 -6
- package/dist/agent-board.js +30 -9
- package/dist/agent-fence.d.ts +203 -10
- package/dist/agent-fence.js +623 -41
- package/dist/agent-pulse.js +9 -2
- package/dist/agent-state-words.d.ts +19 -0
- package/dist/agent-state-words.js +20 -0
- package/dist/agent-state.d.ts +65 -0
- package/dist/agent-state.js +229 -0
- package/dist/agentconfig.js +36 -13
- package/dist/agents-board.d.ts +34 -2
- package/dist/agents-board.js +110 -1
- package/dist/approval-rules-ui.js +2 -2
- package/dist/assignment-delivery.d.ts +14 -0
- package/dist/assignment-delivery.js +13 -0
- package/dist/assignment.js +13 -0
- package/dist/attest.js +7 -0
- package/dist/backup-ui.js +2 -2
- package/dist/browser/THIRD_PARTY_NOTICES.txt +48 -0
- package/dist/browser/terminal.css +37 -0
- package/dist/browser/terminal.js +117 -0
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +42 -38
- package/dist/browser-shell.js +6 -3
- package/dist/browser-workspace.d.ts +263 -1
- package/dist/browser-workspace.js +60 -0
- package/dist/builder.d.ts +6 -1
- package/dist/builder.js +34 -13
- package/dist/chat-actions.d.ts +13 -1
- package/dist/chat-actions.js +199 -17
- package/dist/chat-ask.d.ts +4 -0
- package/dist/chat-ask.js +14 -0
- package/dist/chat-channel.js +20 -0
- package/dist/chat-continuity.js +5 -4
- package/dist/chat-decide.d.ts +0 -14
- package/dist/chat-decide.js +4 -1
- package/dist/chat-delivery.d.ts +9 -0
- package/dist/chat-delivery.js +45 -6
- package/dist/chat-messages.d.ts +8 -0
- package/dist/chat-messages.js +36 -4
- package/dist/check-levels-ui.js +2 -2
- package/dist/check-levels.js +3 -0
- package/dist/checkout-proof.d.ts +28 -0
- package/dist/checkout-proof.js +62 -0
- package/dist/claim.d.ts +12 -0
- package/dist/claim.js +20 -1
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +5 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +24 -2
- package/dist/coding-ui.js +6 -4
- package/dist/console-instance.d.ts +124 -0
- package/dist/console-instance.js +337 -0
- package/dist/contracts/chat-actions.d.ts +16 -0
- package/dist/contracts/chat-actions.js +4 -0
- package/dist/contracts/chat-content.d.ts +19 -0
- package/dist/contracts/chat-content.js +6 -0
- package/dist/contracts/cli.d.ts +8 -0
- package/dist/contracts/cli.js +3 -1
- package/dist/contracts/console-api.d.ts +30 -4
- package/dist/contracts/console-api.js +13 -4
- package/dist/contracts/flow-card.d.ts +4 -0
- package/dist/contracts/flow-card.js +3 -0
- package/dist/contracts/flow.d.ts +46 -0
- package/dist/contracts/flow.js +11 -2
- package/dist/contracts/integration-metadata.d.ts +2 -1
- package/dist/contracts/integration-metadata.js +2 -1
- package/dist/contracts/lead-tools.d.ts +92 -0
- package/dist/contracts/lead-tools.js +13 -0
- package/dist/contracts/provider.d.ts +11 -1
- package/dist/contracts/provider.js +1 -1
- package/dist/contracts/route.d.ts +76 -1
- package/dist/contracts/route.js +1 -1
- package/dist/contracts/store-json.d.ts +2 -0
- package/dist/control-setup.d.ts +1 -1
- package/dist/control-ui.js +2 -2
- package/dist/converse.d.ts +16 -0
- package/dist/converse.js +32 -4
- package/dist/demo-chat.js +4 -4
- package/dist/demo.js +2 -2
- package/dist/desktop-update-gate.js +6 -0
- package/dist/discord-chat.js +4 -1
- package/dist/dispatch.js +7 -4
- package/dist/doctor.d.ts +53 -0
- package/dist/doctor.js +427 -0
- package/dist/egress-bridge.d.ts +17 -0
- package/dist/egress-bridge.js +111 -0
- package/dist/egress.d.ts +119 -0
- package/dist/egress.js +423 -0
- package/dist/evidence-pack.d.ts +1 -1
- package/dist/evidence-pack.js +2 -2
- package/dist/exec.d.ts +81 -0
- package/dist/exec.js +368 -9
- package/dist/export-ui.js +2 -2
- package/dist/flow-code.d.ts +2 -2
- package/dist/flow-code.js +4 -15
- package/dist/flow-engine.js +35 -11
- package/dist/flow-gallery-ui.js +2 -2
- package/dist/flow-gallery.js +27 -8
- package/dist/flow-share.js +16 -5
- package/dist/flow-triggers.d.ts +84 -11
- package/dist/flow-triggers.js +554 -87
- package/dist/flow-unattended-words.d.ts +28 -0
- package/dist/flow-unattended-words.js +50 -0
- package/dist/flow-unattended.d.ts +227 -0
- package/dist/flow-unattended.js +776 -0
- package/dist/flows-cli.d.ts +28 -1
- package/dist/flows-cli.js +63 -12
- package/dist/flows-ui.d.ts +2 -3
- package/dist/flows-ui.js +56 -5
- package/dist/flows.d.ts +4 -0
- package/dist/flows.js +9 -1
- package/dist/google-mail.d.ts +1 -0
- package/dist/google-mail.js +61 -18
- package/dist/html.d.ts +9 -0
- package/dist/html.js +11 -0
- package/dist/integrations-ui.d.ts +2 -1
- package/dist/integrations-ui.js +6 -3
- package/dist/integrations.d.ts +17 -3
- package/dist/integrations.js +38 -18
- package/dist/invoke.d.ts +17 -0
- package/dist/invoke.js +510 -182
- package/dist/keys.d.ts +18 -14
- package/dist/keys.js +5 -0
- package/dist/landed-cleanup.d.ts +3 -2
- package/dist/landed-cleanup.js +12 -5
- package/dist/lead-capabilities.js +3 -2
- package/dist/lead-contract.js +2 -0
- package/dist/lead-doors.d.ts +2 -0
- package/dist/lead-doors.js +7 -0
- package/dist/lead-follow.d.ts +2 -0
- package/dist/lead-follow.js +58 -2
- package/dist/lead-progress.js +1 -0
- package/dist/lead-queue-runtime.d.ts +46 -0
- package/dist/lead-queue-runtime.js +191 -0
- package/dist/lead-queue.d.ts +104 -0
- package/dist/lead-queue.js +187 -0
- package/dist/lead-sweep.d.ts +67 -0
- package/dist/lead-sweep.js +99 -0
- package/dist/lead-tools.d.ts +22 -0
- package/dist/lead-tools.js +163 -4
- package/dist/lead.d.ts +13 -5
- package/dist/lead.js +89 -29
- package/dist/limits-ui.d.ts +8 -1
- package/dist/limits-ui.js +26 -0
- package/dist/live-bus.d.ts +8 -0
- package/dist/live-bus.js +40 -4
- package/dist/live-detail.d.ts +27 -0
- package/dist/live-detail.js +57 -0
- package/dist/live-notes.d.ts +20 -0
- package/dist/live-notes.js +47 -0
- package/dist/live-records.d.ts +140 -0
- package/dist/live-records.js +935 -0
- package/dist/live-terminal.d.ts +258 -0
- package/dist/live-terminal.js +649 -0
- package/dist/live.d.ts +38 -27
- package/dist/live.js +142 -124
- package/dist/mcp-connect.d.ts +8 -7
- package/dist/mcp-connect.js +49 -51
- package/dist/mcp-oauth.d.ts +6 -3
- package/dist/mcp-oauth.js +8 -4
- package/dist/memory-pass.js +2 -2
- package/dist/mobile-viewport.d.ts +5 -1
- package/dist/mobile-viewport.js +18 -2
- package/dist/model-catalog.js +4 -0
- package/dist/monitoring-ui.js +2 -2
- package/dist/needs-you.d.ts +1 -1
- package/dist/needs-you.js +11 -4
- package/dist/oauth-run-token.d.ts +51 -0
- package/dist/oauth-run-token.js +127 -0
- package/dist/operate.d.ts +2 -0
- package/dist/operate.js +282 -18
- package/dist/passwordless.d.ts +23 -0
- package/dist/passwordless.js +26 -0
- package/dist/peek-cli.d.ts +12 -4
- package/dist/peek-cli.js +19 -11
- package/dist/phase-routing.js +10 -3
- package/dist/plan-auto.d.ts +11 -0
- package/dist/plan-auto.js +62 -11
- package/dist/planner-source.d.ts +3 -0
- package/dist/planner-source.js +14 -0
- package/dist/planner.d.ts +2 -1
- package/dist/planner.js +7 -1
- package/dist/policy-ui.js +2 -2
- package/dist/policy.d.ts +1 -20
- package/dist/policy.js +29 -2
- package/dist/process-recovery-eligibility.d.ts +1 -1
- package/dist/project-access.d.ts +3 -0
- package/dist/project-access.js +3 -0
- package/dist/project-delete-ui.d.ts +12 -0
- package/dist/project-delete-ui.js +41 -9
- package/dist/project-delete.d.ts +3 -1
- package/dist/project-delete.js +12 -6
- package/dist/project-file.d.ts +3 -0
- package/dist/project-file.js +16 -0
- package/dist/project-memory.d.ts +3 -1
- package/dist/project-memory.js +57 -25
- package/dist/project-tools.d.ts +103 -27
- package/dist/project-tools.js +399 -100
- package/dist/provider-auth.d.ts +4 -3
- package/dist/provider-auth.js +5 -4
- package/dist/provider-connection.d.ts +2 -1
- package/dist/provider-connection.js +5 -0
- package/dist/provider-home.d.ts +22 -0
- package/dist/provider-home.js +99 -0
- package/dist/provider.d.ts +42 -5
- package/dist/provider.js +97 -19
- package/dist/pull-request-ui.js +2 -2
- package/dist/push.js +1 -1
- package/dist/quick-start.d.ts +24 -0
- package/dist/quick-start.js +127 -0
- package/dist/release.d.ts +3 -0
- package/dist/release.js +14 -8
- package/dist/remote-main.d.ts +4 -0
- package/dist/remote-main.js +3 -3
- package/dist/request-budget-ui.js +2 -2
- package/dist/result-review.d.ts +1 -1
- package/dist/result-review.js +9 -3
- package/dist/retention-ui.js +2 -2
- package/dist/retention.js +10 -1
- package/dist/run-efficiency.d.ts +73 -0
- package/dist/run-efficiency.js +228 -0
- package/dist/runner.d.ts +5 -1
- package/dist/runner.js +22 -1
- package/dist/scope.d.ts +171 -6
- package/dist/scope.js +251 -31
- package/dist/scout-net.d.ts +4 -1
- package/dist/scout-net.js +37 -12
- package/dist/scout.d.ts +20 -6
- package/dist/scout.js +27 -3
- package/dist/serve.js +258 -43
- package/dist/server/chat.d.ts +1 -1
- package/dist/server/chat.js +122 -31
- package/dist/server/chrome.d.ts +6 -1
- package/dist/server/chrome.js +18 -7
- package/dist/server/console.css +52 -38
- package/dist/server/flows.js +65 -13
- package/dist/server/guards.d.ts +1 -0
- package/dist/server/guards.js +23 -9
- package/dist/server/http.d.ts +7 -0
- package/dist/server/http.js +3 -2
- package/dist/server/live.d.ts +34 -1
- package/dist/server/live.js +39 -17
- package/dist/server/pages-assets.js +5 -0
- package/dist/server/pages.js +90 -13
- package/dist/server/people-tokens.d.ts +3 -0
- package/dist/server/people-tokens.js +55 -17
- package/dist/server/remote-hooks.js +12 -6
- package/dist/server/remote.d.ts +2 -1
- package/dist/server/remote.js +14 -5
- package/dist/server/render-chat.d.ts +12 -3
- package/dist/server/render-chat.js +47 -16
- package/dist/server/render-pages.d.ts +4 -1
- package/dist/server/render-pages.js +5 -4
- package/dist/server/render-people.d.ts +3 -2
- package/dist/server/render-people.js +3 -2
- package/dist/server/render-results.d.ts +18 -2
- package/dist/server/render-results.js +37 -28
- package/dist/server/render-settings.js +6 -5
- package/dist/server/render-tasks.d.ts +35 -10
- package/dist/server/render-tasks.js +110 -27
- package/dist/server/request-context.d.ts +2 -0
- package/dist/server/request-context.js +2 -1
- package/dist/server/route-table.d.ts +3 -0
- package/dist/server/route-table.js +49 -5
- package/dist/server/runtime.d.ts +34 -3
- package/dist/server/session.d.ts +25 -1
- package/dist/server/session.js +18 -4
- package/dist/server/settings.js +132 -13
- package/dist/server/sweeps.d.ts +4 -0
- package/dist/server/sweeps.js +58 -0
- package/dist/server/task-views.d.ts +2 -1
- package/dist/server/task-views.js +96 -6
- package/dist/server/tasks.js +320 -74
- package/dist/server/terminals.d.ts +31 -0
- package/dist/server/terminals.js +365 -0
- package/dist/setup-guide.js +3 -2
- package/dist/slack-chat.js +4 -1
- package/dist/spend-ui.js +3 -3
- package/dist/spend.js +2 -1
- package/dist/steer-capabilities.d.ts +32 -0
- package/dist/steer-capabilities.js +60 -0
- package/dist/steer-controls.d.ts +14 -0
- package/dist/steer-controls.js +36 -0
- package/dist/steer-status.js +2 -2
- package/dist/storage-sweep-child.js +3 -1
- package/dist/storage-sweep-process.d.ts +12 -5
- package/dist/storage-sweep-process.js +23 -6
- package/dist/storage-ui.js +4 -4
- package/dist/store-baseline.d.ts +1 -1
- package/dist/store-baseline.js +1 -1
- package/dist/store.d.ts +466 -19
- package/dist/store.js +1185 -63
- package/dist/subagent-desk.js +1 -1
- package/dist/subagent-tools.d.ts +1 -1
- package/dist/summary.js +3 -0
- package/dist/surface.js +6 -4
- package/dist/task-source.d.ts +58 -0
- package/dist/task-source.js +93 -0
- package/dist/team-leads.js +1 -1
- package/dist/teams-chat.js +4 -1
- package/dist/teardown-git.d.ts +14 -0
- package/dist/teardown-git.js +119 -0
- package/dist/telegram-lead.d.ts +3 -0
- package/dist/telegram-lead.js +44 -7
- package/dist/telegram.d.ts +11 -2
- package/dist/telegram.js +51 -17
- package/dist/terminal-catalog.d.ts +11 -0
- package/dist/terminal-catalog.js +129 -0
- package/dist/text-limits.d.ts +2 -0
- package/dist/text-limits.js +2 -0
- package/dist/tool-broker.d.ts +39 -0
- package/dist/tool-broker.js +370 -0
- package/dist/tool-launcher.js +5 -4
- package/dist/tool-secret-store.d.ts +170 -0
- package/dist/tool-secret-store.js +834 -0
- package/dist/toolroll-update-ui.js +2 -2
- package/dist/toolroll-update.js +14 -3
- package/dist/tools-ui.d.ts +7 -1
- package/dist/tools-ui.js +19 -10
- package/dist/webhook-limits.d.ts +23 -0
- package/dist/webhook-limits.js +30 -0
- package/dist/webhook-readers.d.ts +56 -0
- package/dist/webhook-readers.js +138 -0
- package/dist/webhook-signing.d.ts +27 -0
- package/dist/webhook-signing.js +119 -0
- package/dist/work-index.d.ts +29 -0
- package/dist/work-index.js +21 -5
- package/dist/workspace-revision.js +3 -1
- package/dist/worktree.d.ts +10 -4
- package/dist/worktree.js +70 -29
- package/package.json +13 -2
- package/scripts/provider-canary.mjs +210 -31
package/dist/mcp-connect.js
CHANGED
|
@@ -7,16 +7,19 @@
|
|
|
7
7
|
* authorization server at its origin), registers Toolroll as a client
|
|
8
8
|
* with this console's callback, and sends the person to the service to sign
|
|
9
9
|
* in. The callback trades the code for tokens, which live in the tool's own
|
|
10
|
-
* secrets
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* secrets (the OS keychain, or its file where there is none; never the
|
|
11
|
+
* database, never a log), and the tool joins the project like any other:
|
|
12
|
+
* tested, listed, and given to subagents under rules. The worker refreshes
|
|
13
|
+
* tokens before they expire, and each run gets an access token of its own
|
|
14
|
+
* (oauth-run-token.ts).
|
|
13
15
|
*/
|
|
14
16
|
import { createHash, randomBytes } from "node:crypto";
|
|
15
|
-
import { homedir } from "node:os";
|
|
16
17
|
import { basename } from "node:path";
|
|
17
|
-
import { LOCAL_APPS, addToolTo, catalogTool, localAppOf, projectToolsOf, readToolSecrets, setToolSecrets, testToolOf } from "./project-tools.js";
|
|
18
|
+
import { LOCAL_APPS, addToolTo, catalogTool, localAppOf, projectToolsOf, readToolSecrets, renewingSignIn, setToolSecrets, testToolOf } from "./project-tools.js";
|
|
18
19
|
import { envValue } from "./names.js";
|
|
19
|
-
import {
|
|
20
|
+
import { readAuthorizationServer, readProtectedResource, readRegistration, readTokenResponse } from "./contracts/integration-metadata.js";
|
|
21
|
+
import { ACCESS, CLIENT, CLIENT_SECRET, EXPIRES, GRANTED, REFRESH, RESOURCE, REVOKE_URL, TOKEN_URL, readScope, scopeList } from "./oauth-run-token.js";
|
|
22
|
+
export { readScope, scopeList };
|
|
20
23
|
/**
|
|
21
24
|
* Services that connect this way: their servers speak streamable HTTP and
|
|
22
25
|
* advertise registering clients on the spot. Advertising isn't accepting:
|
|
@@ -92,7 +95,7 @@ export const localConnectOf = (id) => {
|
|
|
92
95
|
* `local` connects with no sign-in. `research` says, for a connected service research can't use because of the access
|
|
93
96
|
* it was granted, why (shown on its Tools entry beside Reconnect read-only).
|
|
94
97
|
*/
|
|
95
|
-
export function connectionsOf(store, repo, home
|
|
98
|
+
export function connectionsOf(store, repo, home) {
|
|
96
99
|
const tools = projectToolsOf(store, repo);
|
|
97
100
|
const signIn = oneClickServices().map(service => {
|
|
98
101
|
const had = tools.find(one => one.name === service.id);
|
|
@@ -126,29 +129,13 @@ export function readsOnly(action, subjects) {
|
|
|
126
129
|
const subject = (word) => subjects.includes(word) || subjects.includes(word.replace(/s$/, ""));
|
|
127
130
|
return words.some(word => READING.has(word)) && words.every(word => READING.has(word) || QUALIFIERS.has(word) || subject(word));
|
|
128
131
|
}
|
|
129
|
-
/**
|
|
130
|
-
* Identity scopes a read-only grant may carry beside `<resource>:read` ones. An allow-list: any other scope (a
|
|
131
|
-
* write, a wildcard, `introspection`, one nobody listed) makes the grant unsafe for research.
|
|
132
|
-
*/
|
|
133
|
-
const IDENTITY_SCOPES = new Set(["openid", "profile", "email"]);
|
|
134
|
-
const READ_SCOPE = /^[a-z][a-z0-9_]{0,63}:read$/;
|
|
135
|
-
export const readScope = (scope) => IDENTITY_SCOPES.has(scope) || READ_SCOPE.test(scope);
|
|
136
132
|
/** Whether a saved grant only reads: it names at least one scope and every one is a read or identity scope. Unknown (null) never is. */
|
|
137
133
|
// A scope a server adds to every token without being asked, that grants no access to data. PostHog adds
|
|
138
134
|
// `introspection` (its ALWAYS_ALLOWED_SCOPES): it lets the token be checked, and has no write form. Never requested.
|
|
139
135
|
const GRANTED_ANYWAY = new Set(["introspection"]);
|
|
140
136
|
export const readOnlyGrant = (grant) => grant !== null && grant.some(readScope) && grant.every(scope => readScope(scope) || GRANTED_ANYWAY.has(scope));
|
|
141
|
-
/** An OAuth `scope` value as scope tokens (RFC 6749), sorted and deduplicated; null when absent, empty or malformed. */
|
|
142
|
-
export function scopeList(value) {
|
|
143
|
-
if (typeof value !== "string")
|
|
144
|
-
return null;
|
|
145
|
-
const tokens = value.split(" ").filter(one => one !== "");
|
|
146
|
-
if (tokens.length === 0 || tokens.length > 512 || !tokens.every(one => SCOPE_TOKEN.test(one)))
|
|
147
|
-
return null;
|
|
148
|
-
return [...new Set(tokens)].sort();
|
|
149
|
-
}
|
|
150
137
|
/** The scope a connected service's sign-in was granted, as saved beside its token; null when it was never proved. */
|
|
151
|
-
export function grantOf(repo, tool, home
|
|
138
|
+
export function grantOf(repo, tool, home) {
|
|
152
139
|
try {
|
|
153
140
|
return scopeList(readToolSecrets(repo, tool, home)[GRANTED]);
|
|
154
141
|
}
|
|
@@ -195,10 +182,6 @@ export function researchToolsOf(tools, launched = null, granted = () => null) {
|
|
|
195
182
|
}
|
|
196
183
|
return research;
|
|
197
184
|
}
|
|
198
|
-
/** The secrets a connected tool keeps: the bearer the MCP server takes, and what refreshing it needs. */
|
|
199
|
-
const ACCESS = "OAUTH_ACCESS_TOKEN", REFRESH = "OAUTH_REFRESH_TOKEN", CLIENT = "OAUTH_CLIENT_ID", CLIENT_SECRET = "OAUTH_CLIENT_SECRET", TOKEN_URL = "OAUTH_TOKEN_URL", EXPIRES = "OAUTH_EXPIRES_AT", RESOURCE = "OAUTH_RESOURCE";
|
|
200
|
-
/** The scope the sign-in granted, kept beside its token. Not one of the spec's secrets, so it never reaches a launch. */
|
|
201
|
-
const GRANTED = "OAUTH_GRANTED_SCOPE";
|
|
202
185
|
/** Where a service sends the person back after they sign in. */
|
|
203
186
|
export const CONNECT_CALLBACK = "/settings/tools/connected";
|
|
204
187
|
const VISIT_MS = 15 * 60_000, RENEW_RETRY_MS = 5 * 60_000;
|
|
@@ -269,7 +252,7 @@ export async function discoverSignIn(mcpUrl, fetcher = fetch) {
|
|
|
269
252
|
}
|
|
270
253
|
// A token is for the resource the server names (Stripe's has no trailing slash), when it names one.
|
|
271
254
|
const named = resource.resource;
|
|
272
|
-
return { authorize, token, register, scopes, resource: https(named) ? named : null };
|
|
255
|
+
return { authorize, token, register, scopes, resource: https(named) ? named : null, revoke: https(meta.revocation_endpoint) ? meta.revocation_endpoint : null };
|
|
273
256
|
}
|
|
274
257
|
/**
|
|
275
258
|
* Start connecting a service: find its sign-in, register this console as a
|
|
@@ -323,7 +306,7 @@ export async function startConnect(input, fetcher = fetch, now = Date.now()) {
|
|
|
323
306
|
const scope = scopes.length > 0 ? scopes.join(" ") : null;
|
|
324
307
|
if (scope !== null)
|
|
325
308
|
go.searchParams.set("scope", scope);
|
|
326
|
-
return { ok: true, go: go.toString(), state, visit: { service: service.id, repo: input.repo, by: input.by, kit: input.kit ?? null, template: input.template ?? null, verifier, clientId, clientSecret, token: server.token, resource, redirect, expires: now + VISIT_MS, scope } };
|
|
309
|
+
return { ok: true, go: go.toString(), state, visit: { service: service.id, repo: input.repo, by: input.by, kit: input.kit ?? null, template: input.template ?? null, verifier, clientId, clientSecret, token: server.token, resource, redirect, expires: now + VISIT_MS, scope, revoke: server.revoke } };
|
|
327
310
|
}
|
|
328
311
|
/** The spec a connected service joins the project with: its MCP address, signed in with the token its sign-in gave. */
|
|
329
312
|
export function connectedSpec(serviceId) {
|
|
@@ -341,11 +324,11 @@ const tokensFrom = (body, now) => ({
|
|
|
341
324
|
/**
|
|
342
325
|
* Finish connecting: trade the code for tokens, add the tool to the project
|
|
343
326
|
* (or refresh its sign-in, when it's there already), keep the tokens in its
|
|
344
|
-
* secrets
|
|
327
|
+
* secrets, and test it.
|
|
345
328
|
*/
|
|
346
329
|
export async function finishConnect(store, visit, code, now, options = {}) {
|
|
347
330
|
const service = oneClickOf(visit.service);
|
|
348
|
-
const fetcher = options.fetcher ?? fetch, home = options.home
|
|
331
|
+
const fetcher = options.fetcher ?? fetch, home = options.home;
|
|
349
332
|
const spec = connectedSpec(service.id);
|
|
350
333
|
const had = projectToolsOf(store, visit.repo).find(one => one.name === spec.name);
|
|
351
334
|
if (had !== undefined && (had.spec.url !== spec.url || had.spec.bearer !== ACCESS))
|
|
@@ -375,7 +358,7 @@ export async function finishConnect(store, visit, code, now, options = {}) {
|
|
|
375
358
|
}
|
|
376
359
|
// The grant: what the answer says, or what was asked for when it says nothing; never one from an earlier sign-in.
|
|
377
360
|
const granted = body.scopeSaid ? scopeList(body.scope) : scopeList(visit.scope ?? null);
|
|
378
|
-
setToolSecrets(visit.repo, spec.name, { [ACCESS]: tokens.access, [CLIENT]: visit.clientId, [TOKEN_URL]: visit.token, [RESOURCE]: visit.resource, [GRANTED]: granted?.join(" ") ?? "",
|
|
361
|
+
setToolSecrets(visit.repo, spec.name, { [ACCESS]: tokens.access, [CLIENT]: visit.clientId, [TOKEN_URL]: visit.token, [RESOURCE]: visit.resource, [GRANTED]: granted?.join(" ") ?? "", [REVOKE_URL]: visit.revoke ?? "", [REFRESH]: "",
|
|
379
362
|
...(tokens.refresh === null ? {} : { [REFRESH]: tokens.refresh }), ...(tokens.expires === null ? {} : { [EXPIRES]: tokens.expires }), ...(visit.clientSecret === null ? {} : { [CLIENT_SECRET]: visit.clientSecret }) }, home);
|
|
380
363
|
// The success line names the project it went to: the page and chat may be looking at another.
|
|
381
364
|
const to = basename(visit.repo);
|
|
@@ -387,7 +370,7 @@ export async function finishConnect(store, visit, code, now, options = {}) {
|
|
|
387
370
|
}
|
|
388
371
|
/** Keep connected services signed in: any token expiring within ten minutes is refreshed (the worker's pass, and before a subagent's call). */
|
|
389
372
|
export async function refreshConnections(store, repos, now, options = {}) {
|
|
390
|
-
const fetcher = options.fetcher ?? fetch, home = options.home
|
|
373
|
+
const fetcher = options.fetcher ?? fetch, home = options.home;
|
|
391
374
|
const report = { refreshed: 0, problems: [] };
|
|
392
375
|
for (const repo of repos)
|
|
393
376
|
for (const tool of projectToolsOf(store, repo)) {
|
|
@@ -402,28 +385,43 @@ export async function refreshConnections(store, repos, now, options = {}) {
|
|
|
402
385
|
if (now.getTime() - (renewTried.get(key) ?? 0) < RENEW_RETRY_MS)
|
|
403
386
|
continue;
|
|
404
387
|
renewTried.set(key, now.getTime());
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
388
|
+
// Renewed one at a time with a run's own sign-in, across Toolroll's processes: a rotated refresh token is never used twice.
|
|
389
|
+
const problem = await renewingSignIn(repo, tool.name, home, async () => {
|
|
390
|
+
const latest = readToolSecrets(repo, tool.name, home);
|
|
391
|
+
if (!latest[REFRESH] || !latest[TOKEN_URL] || !latest[CLIENT])
|
|
392
|
+
return null;
|
|
393
|
+
try {
|
|
394
|
+
const answer = await fetcher(latest[TOKEN_URL], { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" }, signal: AbortSignal.timeout(15_000),
|
|
395
|
+
body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: latest[REFRESH], client_id: latest[CLIENT], ...(latest[CLIENT_SECRET] === undefined ? {} : { client_secret: latest[CLIENT_SECRET] }), ...(latest[RESOURCE] === undefined ? {} : { resource: latest[RESOURCE] }) }) });
|
|
396
|
+
const body = readTokenResponse(await answer.json());
|
|
397
|
+
if (body === null)
|
|
398
|
+
return `${tool.name}: couldn't reach it to renew its sign-in`;
|
|
399
|
+
const tokens = tokensFrom(body, now.getTime());
|
|
400
|
+
if (!answer.ok || tokens.access === null)
|
|
401
|
+
return `${tool.name}: its sign-in ran out; connect it again on the Tools page`;
|
|
402
|
+
// A refresh carries the same grant or a narrower one, and says so when it changes; said badly, it is unknown.
|
|
403
|
+
// Not saved (a locked keychain): the saved sign-in stays as it was, and this says so.
|
|
404
|
+
try {
|
|
405
|
+
setToolSecrets(repo, tool.name, { [ACCESS]: tokens.access, ...(tokens.refresh === null ? {} : { [REFRESH]: tokens.refresh }), ...(tokens.expires === null ? {} : { [EXPIRES]: tokens.expires }),
|
|
406
|
+
...(body.scopeSaid ? { [GRANTED]: scopeList(body.scope)?.join(" ") ?? "" } : {}) }, home);
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
return `${tool.name}: its renewed sign-in couldn't be saved; check that the keychain is unlocked`;
|
|
410
|
+
}
|
|
411
|
+
return false;
|
|
412
412
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
report.problems.push(`${tool.name}: its sign-in ran out; connect it again on the Tools page`);
|
|
416
|
-
continue;
|
|
413
|
+
catch {
|
|
414
|
+
return `${tool.name}: couldn't reach it to renew its sign-in`;
|
|
417
415
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
416
|
+
});
|
|
417
|
+
if (typeof problem === "string") {
|
|
418
|
+
report.problems.push(problem);
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
if (problem === false) {
|
|
421
422
|
renewTried.delete(key);
|
|
422
423
|
report.refreshed++;
|
|
423
424
|
}
|
|
424
|
-
catch {
|
|
425
|
-
report.problems.push(`${tool.name}: couldn't reach it to renew its sign-in`);
|
|
426
|
-
}
|
|
427
425
|
}
|
|
428
426
|
return report;
|
|
429
427
|
}
|
package/dist/mcp-oauth.d.ts
CHANGED
|
@@ -51,7 +51,9 @@ export type OAuthSession = {
|
|
|
51
51
|
name: string;
|
|
52
52
|
role: "approver" | "viewer";
|
|
53
53
|
csrf: string;
|
|
54
|
-
/**
|
|
54
|
+
/** The account's credential generation this session signed in under: a code is never issued under another. */
|
|
55
|
+
generation: number;
|
|
56
|
+
/** Signed in with an identity provider: its name, and whether an explicit re-check made on request awaits one step-up. */
|
|
55
57
|
sso: {
|
|
56
58
|
label: string;
|
|
57
59
|
fresh: boolean;
|
|
@@ -68,8 +70,9 @@ export type OAuthHttpOptions = {
|
|
|
68
70
|
session: (request: IncomingMessage) => OAuthSession | null;
|
|
69
71
|
/** A posted form came from a page of this console (its Origin, or Referer, names an allowed host). */
|
|
70
72
|
sameSite: (request: IncomingMessage) => boolean;
|
|
71
|
-
/** The step-up: the person's password, or (empty)
|
|
72
|
-
|
|
73
|
+
/** The step-up, proved at the moment of consent on this request's live session: the person's password, or (empty)
|
|
74
|
+
* an explicit identity-provider re-check, spent by this consent. */
|
|
75
|
+
confirm: (request: IncomingMessage, session: OAuthSession, typed: string) => boolean;
|
|
73
76
|
/** The projects this person may choose: each a project the server knows that they can use now. */
|
|
74
77
|
projectsFor: (account: string) => string[];
|
|
75
78
|
/** The token exchange's budget by source (request-budget.ts): charged once per request, before its body is read or any code or refresh secret is spent. */
|
package/dist/mcp-oauth.js
CHANGED
|
@@ -308,8 +308,8 @@ export function createOAuthHttp(options) {
|
|
|
308
308
|
return backToClient(response, origin, one.redirectUri, one.state, { error: "access_denied", error_description: "the person declined" }, "Cancelled. Returning to your MCP client…");
|
|
309
309
|
}
|
|
310
310
|
const account = store.accountOf(session.name);
|
|
311
|
-
if (account === null || account.revokedAt !== null)
|
|
312
|
-
return problemPage(response, 403, "Sign in first", "Your
|
|
311
|
+
if (account === null || account.revokedAt !== null || account.generation !== session.generation)
|
|
312
|
+
return problemPage(response, 403, "Sign in first", "Your access changed. Sign in again; nothing was allowed.");
|
|
313
313
|
const access = form.get("access") === "act" ? "act" : "read";
|
|
314
314
|
const chosen = [...new Set(form.getAll("project"))];
|
|
315
315
|
const again = (status, problem) => page(response, status, consentHtml(session, id, one, problem, { access, projects: chosen }));
|
|
@@ -323,11 +323,15 @@ export function createOAuthHttp(options) {
|
|
|
323
323
|
return again(403, "Choose from your own projects.");
|
|
324
324
|
if (!adapterPolicy({ caller: "cookie", capability: account.role === "approver" ? "act" : "read", viewer: account.role === "viewer" }, "step-up", source => source === "none" && chosen.every(path => mine.includes(path))).ok)
|
|
325
325
|
return again(403, "Choose from your own projects.");
|
|
326
|
-
if (!options.confirm(session, form.get("password") ?? ""))
|
|
326
|
+
if (!options.confirm(request, session, form.get("password") ?? ""))
|
|
327
327
|
return again(403, "Enter your Toolroll password to allow access.");
|
|
328
|
+
// Re-proved after the step-up: the code carries the generation the person consented under, never a newer one.
|
|
329
|
+
const current = store.accountOf(session.name);
|
|
330
|
+
if (current === null || current.revokedAt !== null || current.generation !== session.generation)
|
|
331
|
+
return problemPage(response, 403, "Sign in first", "Your access changed. Sign in again; nothing was allowed.");
|
|
328
332
|
waiting.delete(id);
|
|
329
333
|
const code = randomBytes(32).toString("base64url");
|
|
330
|
-
store.saveOAuthCode(sha256(code), { client: client.id, account: session.name, generation:
|
|
334
|
+
store.saveOAuthCode(sha256(code), { client: client.id, account: session.name, generation: session.generation, access, projects: chosen, redirectUri: one.redirectUri, resource: one.resource,
|
|
331
335
|
challenge: one.challenge, expiresAt: new Date(now.getTime() + OAUTH_TIMES.codeMs).toISOString() });
|
|
332
336
|
return backToClient(response, origin, one.redirectUri, one.state, { code }, "Allowed. Returning to your MCP client…");
|
|
333
337
|
}
|
package/dist/memory-pass.js
CHANGED
|
@@ -138,11 +138,11 @@ function fileSources(repo, kind, home) {
|
|
|
138
138
|
/** The plane's own sessions: lead threads that touched the project, and finished crew runs with their handoff and check output. */
|
|
139
139
|
function planeSources(store, repo, evidenceRoot) {
|
|
140
140
|
const out = [];
|
|
141
|
-
const threads = store.handle.prepare(`SELECT c.thread AS thread, c.id AS conversation, MAX(m.id) AS last, MAX(m.created_at) AS at FROM team_conversation c JOIN lead_message m ON m.thread=c.thread
|
|
141
|
+
const threads = store.handle.prepare(`SELECT c.thread AS thread, c.id AS conversation, MAX(m.id) AS last, MAX(m.created_at) AS at FROM team_conversation c JOIN lead_message m ON m.thread=c.thread AND m.pending=0
|
|
142
142
|
WHERE EXISTS (SELECT 1 FROM json_each(c.projects_json) WHERE value=?) GROUP BY c.thread`).all(repo);
|
|
143
143
|
for (const row of threads) {
|
|
144
144
|
const thread = Number(row['thread']);
|
|
145
|
-
out.push({ id: `lead:${thread}:${Number(row['last'])}`, repo, kind: 'lead', source: `conversation:${String(row['conversation'])}`, at: String(row['at']), trace: () => redact(store.handle.prepare('SELECT role,text FROM lead_message WHERE thread=? ORDER BY id LIMIT 400').all(thread).map(m => `${m['role']}: ${clip(String(m['text']), 4000)}`).join('\n')) });
|
|
145
|
+
out.push({ id: `lead:${thread}:${Number(row['last'])}`, repo, kind: 'lead', source: `conversation:${String(row['conversation'])}`, at: String(row['at']), trace: () => redact(store.handle.prepare('SELECT role,text FROM lead_message WHERE thread=? AND pending=0 ORDER BY id LIMIT 400').all(thread).map(m => `${m['role']}: ${clip(String(m['text']), 4000)}`).join('\n')) });
|
|
146
146
|
}
|
|
147
147
|
const runs = store.handle.prepare(`SELECT r.id, r.role, r.finished_at, r.handoff, t.external_id FROM run r JOIN task_ref t ON t.id=r.task_ref WHERE t.repo=? AND r.role IN ('builder','repair') AND r.outcome IS NOT NULL ORDER BY r.id DESC LIMIT 100`).all(repo);
|
|
148
148
|
for (const row of runs) {
|
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
/** Presentation only: no fields are read, no requests are sent, and no
|
|
2
|
-
* keyboard events are captured. A focused input alone is NOT a keyboard.
|
|
2
|
+
* keyboard events are captured. A focused input alone is NOT a keyboard.
|
|
3
|
+
*
|
|
4
|
+
* It publishes the visible area (--viewport-height, --viewport-top): a phone's
|
|
5
|
+
* keyboard shrinks only the visual viewport on iOS (100dvh never moves), so a
|
|
6
|
+
* full-height app shell sized to it is what keeps the composer above the keys. */
|
|
3
7
|
export declare const MOBILE_VIEWPORT_SCRIPT: string;
|
package/dist/mobile-viewport.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/** Presentation only: no fields are read, no requests are sent, and no
|
|
2
|
-
* keyboard events are captured. A focused input alone is NOT a keyboard.
|
|
2
|
+
* keyboard events are captured. A focused input alone is NOT a keyboard.
|
|
3
|
+
*
|
|
4
|
+
* It publishes the visible area (--viewport-height, --viewport-top): a phone's
|
|
5
|
+
* keyboard shrinks only the visual viewport on iOS (100dvh never moves), so a
|
|
6
|
+
* full-height app shell sized to it is what keeps the composer above the keys. */
|
|
3
7
|
export const MOBILE_VIEWPORT_SCRIPT = String.raw `
|
|
4
8
|
(function(){
|
|
5
9
|
var root=document.documentElement,viewport=window.visualViewport;
|
|
6
10
|
if(!viewport)return;
|
|
7
|
-
var phone=window.matchMedia('(max-width:
|
|
11
|
+
var phone=window.matchMedia('(max-width: 900px)'),baseline=window.innerHeight,frame=0;
|
|
8
12
|
function update(){
|
|
9
13
|
frame=0;
|
|
10
14
|
var active=document.activeElement;
|
|
@@ -18,8 +22,20 @@ export const MOBILE_VIEWPORT_SCRIPT = String.raw `
|
|
|
18
22
|
var opening=open&&!root.hasAttribute('data-mobile-keyboard');
|
|
19
23
|
var atEnd=opening&&window.innerHeight+window.scrollY>=root.scrollHeight-120;
|
|
20
24
|
root.toggleAttribute('data-mobile-keyboard',Boolean(open));
|
|
25
|
+
// A pinch zoom shrinks the visual viewport too, and then a keyboard can't be told from the zoom: zoomed, the shell
|
|
26
|
+
// is the whole screen (the reader pans to the field), so a keyboard closed while zoomed never leaves it short.
|
|
27
|
+
if(Math.abs(viewport.scale-1)<0.01){
|
|
28
|
+
root.style.setProperty('--viewport-height',Math.round(viewport.height)+'px');
|
|
29
|
+
root.style.setProperty('--viewport-top',Math.max(0,Math.round(viewport.offsetTop))+'px');
|
|
30
|
+
}else{
|
|
31
|
+
root.style.removeProperty('--viewport-height');
|
|
32
|
+
root.style.removeProperty('--viewport-top');
|
|
33
|
+
}
|
|
21
34
|
root.style.setProperty('--keyboard-inset',open?Math.round(gap)+'px':'0px');
|
|
22
35
|
if(atEnd)window.scrollTo({top:root.scrollHeight,behavior:'instant'});
|
|
36
|
+
// In the app shell the field sits in its own scrolling panel, which the shell's new size just cut short:
|
|
37
|
+
// bring it back into that panel's view once the smaller layout is in.
|
|
38
|
+
if(opening&&active.closest('.so-workspace'))requestAnimationFrame(function(){active.scrollIntoView({block:'nearest',behavior:'instant'});});
|
|
23
39
|
}
|
|
24
40
|
function schedule(){if(!frame)frame=requestAnimationFrame(update);}
|
|
25
41
|
viewport.addEventListener('resize',schedule);
|
package/dist/model-catalog.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* installed CLIs are current. Everything lands in orders.db so pickers,
|
|
7
7
|
* spend pins and "new model" notices read one saved snapshot.
|
|
8
8
|
*/
|
|
9
|
+
import { isAcpProvider } from "./acp-agents.js";
|
|
9
10
|
import { execFile } from "node:child_process";
|
|
10
11
|
import { accessSync, constants, lstatSync, readFileSync, realpathSync } from "node:fs";
|
|
11
12
|
import { homedir } from "node:os";
|
|
@@ -197,6 +198,9 @@ export function modelOptions(store, provider, now, home = homedir()) {
|
|
|
197
198
|
}
|
|
198
199
|
if (provider === "gemini")
|
|
199
200
|
return seenModels(store, "gemini").slice(0, 20).map(option);
|
|
201
|
+
// An ACP agent offers only the models its check recorded, on the runner that checked it — read there, never guessed.
|
|
202
|
+
if (isAcpProvider(provider))
|
|
203
|
+
return [];
|
|
200
204
|
return seenModels(store, "openrouter").filter(model => model.tools).sort((a, b) => a.name.localeCompare(b.name)).map(option);
|
|
201
205
|
}
|
|
202
206
|
/** Chat reserves worst-case spend: price per token in whole micro-dollars,
|
package/dist/monitoring-ui.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* the company's tools. One form, the status of each destination beside it,
|
|
4
4
|
* and how Prometheus reads `/metrics`. The signing secret is shown once.
|
|
5
5
|
*/
|
|
6
|
-
import { html, postForm } from "./html.js";
|
|
6
|
+
import { html, postForm, reauth } from "./html.js";
|
|
7
7
|
import { targetOf } from "./monitoring.js";
|
|
8
8
|
import { whenUtc } from "./when-html.js";
|
|
9
9
|
const when = (at) => whenUtc(at);
|
|
@@ -31,7 +31,7 @@ export function monitoringHtml(view, notice) {
|
|
|
31
31
|
const s = view.settings;
|
|
32
32
|
const note = notice.problem ? html `<p class="problem" role="alert">${notice.problem}</p>` : notice.said ? html `<p role="status">${notice.said}</p>` : "";
|
|
33
33
|
const metrics = view.origin === null ? "/metrics" : `${view.origin}/metrics`;
|
|
34
|
-
return html `<section class="monitoring">${note}${postForm("/settings/monitoring", html `<fieldset><legend>Audit stream</legend><p class="meta">Every ledger entry with its seal, as it happens, to your log system.</p><label>Webhook<input type="url" name="webhook" value="${s.webhook?.url ?? ""}" placeholder="https://logs.example.com/standing-orders" spellcheck="false"></label>${statusLine(view, "webhook", s.webhook?.url ?? null)}${s.webhook === null ? "" : html `<label class="choice"><input type="checkbox" name="rotate" value="1"> Make a new signing secret</label>`}<label>Folder (JSON Lines, a file per day)<input type="text" name="folder" value="${s.folder?.path ?? ""}" placeholder="/var/log/standing-orders" spellcheck="false"></label>${statusLine(view, "folder", s.folder?.path ?? null)}</fieldset><fieldset><legend>Traces</legend><p class="meta">Each run as an OpenTelemetry span: timings, model, tokens and cost. Never a prompt or code.</p><label>Collector (OTLP over HTTP)<input type="url" name="traces" value="${s.traces?.endpoint ?? ""}" placeholder="https://otel.example.com:4318" spellcheck="false"></label>${statusLine(view, "traces", s.traces?.endpoint ?? null)}<label>Header name (optional)<input type="text" name="header-name" value="${s.traces?.header?.name ?? ""}" placeholder="x-honeycomb-team" spellcheck="false"></label><label>Header value<input type="password" name="header-value" autocomplete="off" placeholder="${s.traces?.header ? "Saved; leave blank to keep" : "API key"}"></label></fieldset><div class="step-up"
|
|
34
|
+
return html `<section class="monitoring">${note}${postForm("/settings/monitoring", html `<fieldset><legend>Audit stream</legend><p class="meta">Every ledger entry with its seal, as it happens, to your log system.</p><label>Webhook<input type="url" name="webhook" value="${s.webhook?.url ?? ""}" placeholder="https://logs.example.com/standing-orders" spellcheck="false"></label>${statusLine(view, "webhook", s.webhook?.url ?? null)}${s.webhook === null ? "" : html `<label class="choice"><input type="checkbox" name="rotate" value="1"> Make a new signing secret</label>`}<label>Folder (JSON Lines, a file per day)<input type="text" name="folder" value="${s.folder?.path ?? ""}" placeholder="/var/log/standing-orders" spellcheck="false"></label>${statusLine(view, "folder", s.folder?.path ?? null)}</fieldset><fieldset><legend>Traces</legend><p class="meta">Each run as an OpenTelemetry span: timings, model, tokens and cost. Never a prompt or code.</p><label>Collector (OTLP over HTTP)<input type="url" name="traces" value="${s.traces?.endpoint ?? ""}" placeholder="https://otel.example.com:4318" spellcheck="false"></label>${statusLine(view, "traces", s.traces?.endpoint ?? null)}<label>Header name (optional)<input type="text" name="header-name" value="${s.traces?.header?.name ?? ""}" placeholder="x-honeycomb-team" spellcheck="false"></label><label>Header value<input type="password" name="header-value" autocomplete="off" placeholder="${s.traces?.header ? "Saved; leave blank to keep" : "API key"}"></label></fieldset><div class="step-up">${reauth(html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password"></label>`)}</div><button type="submit">Save</button>`)}<fieldset><legend>Metrics</legend><p class="meta">Prometheus reads <code>${metrics}</code> with an instance operator's API token (<a href="/settings/sessions">Sessions & tokens</a>).</p><details><summary>Example</summary><pre>curl -H "Authorization: Bearer so_…" ${metrics}</pre></details></fieldset></section>`;
|
|
35
35
|
}
|
|
36
36
|
/** The webhook's signing secret, once: a focused page with no script. */
|
|
37
37
|
export function signingSecretHtml(url, secret) {
|
package/dist/needs-you.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* The words never name the machine's internals; `plainSentence` keeps a
|
|
7
7
|
* recorded reason only when it is free of them. */
|
|
8
8
|
/** What a person is asked for. */
|
|
9
|
-
export type NeedKey = "approval" | "card" | "answer" | "questions" | "sign-in" | "confirm-stopped" | "check-stopped" | "hold" | "choose-project" | "define-task" | "fix-request" | "choose-agent" | "fix-dependency" | "add-requirement" | "connect-builder" | "start-builder" | "vanished" | "review-result" | "rebuild" | "earlier-version" | "other";
|
|
9
|
+
export type NeedKey = "approval" | "card" | "answer" | "questions" | "sign-in" | "confirm-stopped" | "check-stopped" | "hold" | "choose-project" | "define-task" | "fix-request" | "choose-agent" | "fix-dependency" | "add-requirement" | "connect-builder" | "start-builder" | "vanished" | "review-result" | "rebuild" | "replan" | "earlier-version" | "other";
|
|
10
10
|
/** What the work waits for when no person can act. */
|
|
11
11
|
export type WaitKey = "build-stopping" | "build-finishing" | "other-computer" | "card-reply" | "card-time" | "card-ci";
|
|
12
12
|
/** The action's code: the WorkAction that owns the act, or one of the acts a surface renders itself. */
|
package/dist/needs-you.js
CHANGED
|
@@ -25,7 +25,11 @@ export const NEEDS = {
|
|
|
25
25
|
useReason: reason => /the planner can carry at most/.test(reason) },
|
|
26
26
|
"choose-agent": { sentence: () => "Choose an agent to build this.", action: { code: "select-agent", label: "Choose an agent" } },
|
|
27
27
|
"fix-dependency": { sentence: () => "A task this one waits on didn't finish. Fix or remove it so this one can start.", action: { code: "repair-dependency", label: "Fix the required task" },
|
|
28
|
-
useReason: reason => /didn't finish|did not finish|before it finished/i.test(reason), withReason: reason =>
|
|
28
|
+
useReason: reason => /didn't finish|did not finish|before it finished/i.test(reason), withReason: reason => {
|
|
29
|
+
// A reason that already says what to do is said once.
|
|
30
|
+
const said = reason.replace(/[.\s]*$/, ".");
|
|
31
|
+
return /Fix or remove it so this one can start\.$/.test(said) ? said : `${said} Fix or remove it so this one can start.`;
|
|
32
|
+
} },
|
|
29
33
|
"add-requirement": { sentence: () => "This project is missing something the task needs. Add it so a builder can start.", action: { code: "repair-capability", label: "Add the requirement" } },
|
|
30
34
|
"connect-builder": { sentence: () => "No builder works on this project yet. Connect one so the task can start.", action: { code: "start-worker", label: "Connect a builder" } },
|
|
31
35
|
"start-builder": { sentence: () => "The builder for this project is offline. Start it and the task begins on its own.", action: { code: "start-worker", label: "Start the builder" } },
|
|
@@ -33,6 +37,8 @@ export const NEEDS = {
|
|
|
33
37
|
"review-result": { sentence: () => "Check the result, then accept it or ask for changes.", action: { code: "open-result", label: "Open result" } },
|
|
34
38
|
// Accepting it would not help: it was built to terms that are no longer the plan.
|
|
35
39
|
rebuild: { sentence: context => `${Build(context)} was made to an earlier plan. Build it again to the current plan.`, action: { code: "retry-task", label: "Build again" } },
|
|
40
|
+
// The plan was rewritten after the build and not yet approved: the yes comes first, then Build again.
|
|
41
|
+
replan: { sentence: context => `The plan changed after ${build(context)}. Approve the new plan, then build it again.`, action: { code: "approve-scope", label: "Approve plan" } },
|
|
36
42
|
"earlier-version": { sentence: () => "An earlier version of this task still needs you. Finish it first.", action: { code: "inspect-task", label: "Open earlier version" } },
|
|
37
43
|
other: { sentence: () => "Open the task to see what it needs from you.", action: { code: "inspect-task", label: "Open the task" }, useReason: always },
|
|
38
44
|
};
|
|
@@ -42,7 +48,7 @@ export const ASK_LABEL = { decide: "Decide", review: "Review", unblock: "Unblock
|
|
|
42
48
|
export function askChipOf(read) {
|
|
43
49
|
if (read.headline === "Failed")
|
|
44
50
|
return "Failed";
|
|
45
|
-
if (read.planChanged === true || read.need === "rebuild")
|
|
51
|
+
if (read.planChanged === true || read.need === "rebuild" || read.need === "replan")
|
|
46
52
|
return "Plan changed";
|
|
47
53
|
if (read.mismatch === true)
|
|
48
54
|
return "Mismatch";
|
|
@@ -60,7 +66,8 @@ export function resultHoldUpSentence(facts) {
|
|
|
60
66
|
if (facts.hold != null)
|
|
61
67
|
return "A hold is on this result. Release it to accept the result.";
|
|
62
68
|
if (facts.planChanged)
|
|
63
|
-
return "The plan changed after this was built.
|
|
69
|
+
return facts.unapproved ? "The plan changed after this was built. Approve the new plan, then build it again."
|
|
70
|
+
: "The plan changed after this was built. Build it again to the current plan.";
|
|
64
71
|
if (facts.unapproved)
|
|
65
72
|
return "The current plan isn't approved. Approve it, then review the result.";
|
|
66
73
|
if (facts.question)
|
|
@@ -76,7 +83,7 @@ export function resultHoldUpSentence(facts) {
|
|
|
76
83
|
export const NEED_ASK = {
|
|
77
84
|
approval: "decide", card: "decide", answer: "decide", questions: "decide", "choose-project": "decide", "define-task": "decide",
|
|
78
85
|
"fix-request": "decide", "choose-agent": "decide",
|
|
79
|
-
"review-result": "review", rebuild: "review",
|
|
86
|
+
"review-result": "review", rebuild: "review", replan: "decide",
|
|
80
87
|
"sign-in": "unblock", "confirm-stopped": "unblock", "check-stopped": "unblock", hold: "unblock", "fix-dependency": "unblock",
|
|
81
88
|
"add-requirement": "unblock", "connect-builder": "unblock", "start-builder": "unblock", vanished: "unblock", "earlier-version": "unblock", other: "unblock",
|
|
82
89
|
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ToolSpec } from "./contracts/integration-metadata.js";
|
|
2
|
+
/** The secrets a connected tool keeps: the bearer the MCP server takes, and what renewing it needs. */
|
|
3
|
+
export declare const ACCESS = "OAUTH_ACCESS_TOKEN", REFRESH = "OAUTH_REFRESH_TOKEN", CLIENT = "OAUTH_CLIENT_ID", CLIENT_SECRET = "OAUTH_CLIENT_SECRET", TOKEN_URL = "OAUTH_TOKEN_URL", EXPIRES = "OAUTH_EXPIRES_AT", RESOURCE = "OAUTH_RESOURCE";
|
|
4
|
+
/** The scope the sign-in granted, kept beside its token. Not one of the spec's secrets, so it never reaches a launch. */
|
|
5
|
+
export declare const GRANTED = "OAUTH_GRANTED_SCOPE";
|
|
6
|
+
/** Where the service revokes a token (RFC 7009), when its sign-in said. Not one of the spec's secrets either. */
|
|
7
|
+
export declare const REVOKE_URL = "OAUTH_REVOKE_URL";
|
|
8
|
+
/** A tool connected by signing in: a web tool whose sign-in is the access token. */
|
|
9
|
+
export declare const signedIn: (spec: Pick<ToolSpec, "transport" | "bearer">) => boolean;
|
|
10
|
+
/** Whether runs of a signed-in tool get their own token: only when there is a refresh token to make one with. */
|
|
11
|
+
export declare const mintsRunTokens: (values: Readonly<Record<string, string>> | readonly string[]) => boolean;
|
|
12
|
+
/** A sign-in address Toolroll will send a token to: https, or a stand-in on this computer. */
|
|
13
|
+
export declare const signInAddress: (value: unknown) => value is string;
|
|
14
|
+
export declare const readScope: (scope: string) => boolean;
|
|
15
|
+
/** An OAuth `scope` value as scope tokens (RFC 6749), sorted and deduplicated; null when absent, empty or malformed. */
|
|
16
|
+
export declare function scopeList(value: unknown): string[] | null;
|
|
17
|
+
/**
|
|
18
|
+
* The scope a run asks for: none (the saved grant, never more) for a build; for a research step, only the grant's
|
|
19
|
+
* read scopes when it has some and others besides. A grant never said is left as it is.
|
|
20
|
+
*/
|
|
21
|
+
export declare function runScope(granted: readonly string[] | null, readOnly: boolean): string[] | null;
|
|
22
|
+
/**
|
|
23
|
+
* One renewal of a sign-in at a time (per project and tool, in this process): a service that rotates its refresh
|
|
24
|
+
* token on every use would otherwise see the same one twice, and whichever answer was saved last would be the
|
|
25
|
+
* newest only by luck.
|
|
26
|
+
*/
|
|
27
|
+
export declare function oneAtATime<T>(key: string, work: () => Promise<T>): Promise<T>;
|
|
28
|
+
type Fetch = typeof fetch;
|
|
29
|
+
/** A run's token: the access token alone, its expiry, a rotated refresh token to keep, and how to revoke it. */
|
|
30
|
+
export type RunToken = {
|
|
31
|
+
access: string;
|
|
32
|
+
expires: string | null;
|
|
33
|
+
rotated: string | null;
|
|
34
|
+
revoke: (() => Promise<void>) | null;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Trade the saved refresh token for an access token of this run's own. Plain reasons only: a service's answer is
|
|
38
|
+
* never repeated, so no token can ride an error into a log or the run's record.
|
|
39
|
+
*/
|
|
40
|
+
export declare function mintRunToken(values: Readonly<Record<string, string>>, options: {
|
|
41
|
+
scope: readonly string[] | null;
|
|
42
|
+
now: Date;
|
|
43
|
+
fetcher?: Fetch;
|
|
44
|
+
}): Promise<{
|
|
45
|
+
ok: true;
|
|
46
|
+
token: RunToken;
|
|
47
|
+
} | {
|
|
48
|
+
ok: false;
|
|
49
|
+
reason: string;
|
|
50
|
+
}>;
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A run's own sign-in for a service connected by signing in (mcp-connect.ts).
|
|
3
|
+
*
|
|
4
|
+
* The service's secrets are an access token and the refresh token that renews
|
|
5
|
+
* it. A run never gets the refresh token. When it launches, Toolroll — outside
|
|
6
|
+
* the fence — trades the refresh token for a fresh access token of the run's
|
|
7
|
+
* own, no broader than the saved grant (a research step asks for only its read
|
|
8
|
+
* scopes, where the service takes a narrower one), and only that access token
|
|
9
|
+
* enters the run's broker. When the run ends the token is revoked, where the
|
|
10
|
+
* service said how at sign-in, and otherwise runs out at its own expiry. A
|
|
11
|
+
* service that gave no refresh token has only its one access token: the broker
|
|
12
|
+
* holds it, as it holds an API key.
|
|
13
|
+
*/
|
|
14
|
+
import { SCOPE_TOKEN, readTokenResponse } from "./contracts/integration-metadata.js";
|
|
15
|
+
/** The secrets a connected tool keeps: the bearer the MCP server takes, and what renewing it needs. */
|
|
16
|
+
export const ACCESS = "OAUTH_ACCESS_TOKEN", REFRESH = "OAUTH_REFRESH_TOKEN", CLIENT = "OAUTH_CLIENT_ID", CLIENT_SECRET = "OAUTH_CLIENT_SECRET", TOKEN_URL = "OAUTH_TOKEN_URL", EXPIRES = "OAUTH_EXPIRES_AT", RESOURCE = "OAUTH_RESOURCE";
|
|
17
|
+
/** The scope the sign-in granted, kept beside its token. Not one of the spec's secrets, so it never reaches a launch. */
|
|
18
|
+
export const GRANTED = "OAUTH_GRANTED_SCOPE";
|
|
19
|
+
/** Where the service revokes a token (RFC 7009), when its sign-in said. Not one of the spec's secrets either. */
|
|
20
|
+
export const REVOKE_URL = "OAUTH_REVOKE_URL";
|
|
21
|
+
/** A tool connected by signing in: a web tool whose sign-in is the access token. */
|
|
22
|
+
export const signedIn = (spec) => spec.transport === "http" && spec.bearer === ACCESS;
|
|
23
|
+
/** Whether runs of a signed-in tool get their own token: only when there is a refresh token to make one with. */
|
|
24
|
+
export const mintsRunTokens = (values) => {
|
|
25
|
+
const has = (name) => Array.isArray(values) ? values.includes(name) : (values[name] ?? "") !== "";
|
|
26
|
+
return has(REFRESH) && has(TOKEN_URL) && has(CLIENT);
|
|
27
|
+
};
|
|
28
|
+
const loopback = (url) => url.protocol === "http:" && ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname);
|
|
29
|
+
/** A sign-in address Toolroll will send a token to: https, or a stand-in on this computer. */
|
|
30
|
+
export const signInAddress = (value) => {
|
|
31
|
+
if (typeof value !== "string")
|
|
32
|
+
return false;
|
|
33
|
+
try {
|
|
34
|
+
const url = new URL(value);
|
|
35
|
+
return url.protocol === "https:" || loopback(url);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
/** Identity scopes a read-only grant may carry beside `<resource>:read` ones. */
|
|
42
|
+
const IDENTITY_SCOPES = new Set(["openid", "profile", "email"]);
|
|
43
|
+
const READ_SCOPE = /^[a-z][a-z0-9_]{0,63}:read$/;
|
|
44
|
+
export const readScope = (scope) => IDENTITY_SCOPES.has(scope) || READ_SCOPE.test(scope);
|
|
45
|
+
/** An OAuth `scope` value as scope tokens (RFC 6749), sorted and deduplicated; null when absent, empty or malformed. */
|
|
46
|
+
export function scopeList(value) {
|
|
47
|
+
if (typeof value !== "string")
|
|
48
|
+
return null;
|
|
49
|
+
const tokens = value.split(" ").filter(one => one !== "");
|
|
50
|
+
if (tokens.length === 0 || tokens.length > 512 || !tokens.every(one => SCOPE_TOKEN.test(one)))
|
|
51
|
+
return null;
|
|
52
|
+
return [...new Set(tokens)].sort();
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The scope a run asks for: none (the saved grant, never more) for a build; for a research step, only the grant's
|
|
56
|
+
* read scopes when it has some and others besides. A grant never said is left as it is.
|
|
57
|
+
*/
|
|
58
|
+
export function runScope(granted, readOnly) {
|
|
59
|
+
if (!readOnly || granted === null)
|
|
60
|
+
return null;
|
|
61
|
+
const reads = granted.filter(readScope);
|
|
62
|
+
return reads.length > 0 && reads.length < granted.length ? reads : null;
|
|
63
|
+
}
|
|
64
|
+
const busy = new Map();
|
|
65
|
+
/**
|
|
66
|
+
* One renewal of a sign-in at a time (per project and tool, in this process): a service that rotates its refresh
|
|
67
|
+
* token on every use would otherwise see the same one twice, and whichever answer was saved last would be the
|
|
68
|
+
* newest only by luck.
|
|
69
|
+
*/
|
|
70
|
+
export function oneAtATime(key, work) {
|
|
71
|
+
const ran = (busy.get(key) ?? Promise.resolve()).then(work, work);
|
|
72
|
+
const settled = ran.then(() => undefined, () => undefined);
|
|
73
|
+
busy.set(key, settled);
|
|
74
|
+
void settled.then(() => { if (busy.get(key) === settled)
|
|
75
|
+
busy.delete(key); });
|
|
76
|
+
return ran;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Trade the saved refresh token for an access token of this run's own. Plain reasons only: a service's answer is
|
|
80
|
+
* never repeated, so no token can ride an error into a log or the run's record.
|
|
81
|
+
*/
|
|
82
|
+
export async function mintRunToken(values, options) {
|
|
83
|
+
const fetcher = options.fetcher ?? fetch;
|
|
84
|
+
const refresh = values[REFRESH], tokenUrl = values[TOKEN_URL], client = values[CLIENT];
|
|
85
|
+
if (!refresh || !client || !signInAddress(tokenUrl))
|
|
86
|
+
return { ok: false, reason: "its sign-in can't be renewed for this run; connect it again on the Tools page" };
|
|
87
|
+
const ask = async (scope) => {
|
|
88
|
+
const answer = await fetcher(tokenUrl, { method: "POST", redirect: "error", headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" }, signal: AbortSignal.timeout(15_000),
|
|
89
|
+
body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refresh, client_id: client,
|
|
90
|
+
...(values[CLIENT_SECRET] ? { client_secret: values[CLIENT_SECRET] } : {}), ...(values[RESOURCE] ? { resource: values[RESOURCE] } : {}),
|
|
91
|
+
...(scope === null ? {} : { scope: scope.join(" ") }) }) });
|
|
92
|
+
let body = null;
|
|
93
|
+
try {
|
|
94
|
+
body = readTokenResponse(await answer.json());
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
body = null;
|
|
98
|
+
}
|
|
99
|
+
return { ok: answer.ok, body };
|
|
100
|
+
};
|
|
101
|
+
try {
|
|
102
|
+
let answered = await ask(options.scope);
|
|
103
|
+
// A service that won't narrow a grant on renewal gets asked for the grant as saved; the broker still offers the
|
|
104
|
+
// step only its read-only actions.
|
|
105
|
+
if (!answered.ok && options.scope !== null && answered.body?.error === "invalid_scope")
|
|
106
|
+
answered = await ask(null);
|
|
107
|
+
const access = answered.body?.access_token;
|
|
108
|
+
if (!answered.ok || typeof access !== "string" || access === "")
|
|
109
|
+
return { ok: false, reason: "its sign-in ran out; connect it again on the Tools page" };
|
|
110
|
+
const body = answered.body;
|
|
111
|
+
const expires = body.expires_in === undefined ? null : new Date(options.now.getTime() + body.expires_in * 1000).toISOString();
|
|
112
|
+
const rotated = typeof body.refresh_token === "string" && body.refresh_token !== "" && body.refresh_token !== refresh ? body.refresh_token : null;
|
|
113
|
+
const revokeUrl = values[REVOKE_URL];
|
|
114
|
+
const revoke = signInAddress(revokeUrl) ? async () => {
|
|
115
|
+
try {
|
|
116
|
+
const answer = await fetcher(revokeUrl, { method: "POST", redirect: "error", headers: { "content-type": "application/x-www-form-urlencoded" }, signal: AbortSignal.timeout(10_000),
|
|
117
|
+
body: new URLSearchParams({ token: access, token_type_hint: "access_token", client_id: client, ...(values[CLIENT_SECRET] ? { client_secret: values[CLIENT_SECRET] } : {}) }) });
|
|
118
|
+
await answer.body?.cancel();
|
|
119
|
+
}
|
|
120
|
+
catch { /* it runs out at its own expiry */ }
|
|
121
|
+
} : null;
|
|
122
|
+
return { ok: true, token: { access, expires, rotated, revoke } };
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return { ok: false, reason: "its service couldn't be reached to sign this run in" };
|
|
126
|
+
}
|
|
127
|
+
}
|
package/dist/operate.d.ts
CHANGED
|
@@ -145,6 +145,8 @@ export declare function runOperateAs(argv: string[], opts: {
|
|
|
145
145
|
* finding 27): exclusive, 0600, fsynced — file and directory both — so the
|
|
146
146
|
* row only ever follows a durable secret. Throws on any failure. */
|
|
147
147
|
export declare function writeLoginFileDurably(path: string, name: string, password: string): void;
|
|
148
|
+
/** The platform browser opener — detached, silent, never load-bearing. */
|
|
149
|
+
export declare function openBrowser(url: string): void;
|
|
148
150
|
export declare function readUpConsoleUrl(dir: string): string | null;
|
|
149
151
|
/**
|
|
150
152
|
* The people allowed to agree to a scope.
|