toolroll 0.9.36 → 0.9.38
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 +77 -0
- package/dist/assignment-adapters.d.ts +12 -48
- package/dist/assignment-adapters.js +33 -43
- package/dist/browser/THIRD_PARTY_NOTICES.txt +3 -3
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +26 -26
- package/dist/browser-crew.js +5 -0
- package/dist/browser-workspace.d.ts +2 -0
- package/dist/build-review.d.ts +1 -1
- package/dist/build-review.js +2 -1
- package/dist/chat-acceptance.d.ts +1 -1
- package/dist/chat-actions.d.ts +18 -12
- package/dist/chat-actions.js +15 -65
- package/dist/chat-ask.js +5 -5
- package/dist/chat-channel.js +20 -9
- package/dist/chat-decide.d.ts +18 -4
- package/dist/chat-decide.js +31 -11
- package/dist/chat-delivery-state.d.ts +12 -70
- package/dist/chat-delivery-state.js +17 -135
- package/dist/chat-delivery.js +58 -30
- package/dist/chat-flow.js +8 -6
- package/dist/chat-inbox.js +1 -1
- package/dist/coding-context.d.ts +2 -39
- package/dist/coding-context.js +2 -1
- package/dist/coding-handoff.d.ts +6 -25
- package/dist/coding-handoff.js +11 -5
- package/dist/coding-types.d.ts +2 -26
- package/dist/coding-workspace.d.ts +2 -0
- package/dist/coding-workspace.js +50 -14
- package/dist/contracts/acceptance-terms.d.ts +14 -0
- package/dist/contracts/acceptance-terms.js +16 -0
- package/dist/contracts/chat-actions.d.ts +328 -0
- package/dist/contracts/chat-actions.js +133 -0
- package/dist/contracts/chat-callback-rows.d.ts +59 -0
- package/dist/contracts/chat-callback-rows.js +46 -0
- package/dist/contracts/chat-content.d.ts +276 -0
- package/dist/contracts/chat-content.js +127 -0
- package/dist/contracts/chat-tables.d.ts +6 -0
- package/dist/contracts/chat-tables.js +136 -0
- package/dist/contracts/coding-context.d.ts +97 -0
- package/dist/contracts/coding-context.js +60 -0
- package/dist/contracts/coding-handoff.d.ts +44 -0
- package/dist/contracts/coding-handoff.js +47 -0
- package/dist/contracts/coding-workspace.d.ts +165 -0
- package/dist/contracts/coding-workspace.js +71 -0
- package/dist/contracts/contract.d.ts +7 -2
- package/dist/contracts/contract.js +21 -12
- package/dist/contracts/decision.d.ts +45 -0
- package/dist/contracts/decision.js +45 -0
- package/dist/contracts/discord-callback.d.ts +45 -0
- package/dist/contracts/discord-callback.js +34 -0
- package/dist/contracts/flow-card.d.ts +50 -0
- package/dist/contracts/flow-card.js +46 -0
- package/dist/contracts/flow-send.d.ts +119 -0
- package/dist/contracts/flow-send.js +112 -0
- package/dist/contracts/flow.d.ts +5 -0
- package/dist/contracts/flow.js +5 -0
- package/dist/contracts/gateway-tools.d.ts +374 -0
- package/dist/contracts/gateway-tools.js +94 -0
- package/dist/contracts/lead-context.d.ts +165 -0
- package/dist/contracts/lead-context.js +73 -0
- package/dist/contracts/lead-tools.d.ts +1905 -0
- package/dist/contracts/lead-tools.js +265 -0
- package/dist/contracts/memory-pass.d.ts +158 -0
- package/dist/contracts/memory-pass.js +102 -0
- package/dist/contracts/project-knowledge.d.ts +126 -0
- package/dist/contracts/project-knowledge.js +93 -0
- package/dist/contracts/project-memory.d.ts +117 -0
- package/dist/contracts/project-memory.js +51 -0
- package/dist/contracts/project-skills.d.ts +121 -0
- package/dist/contracts/project-skills.js +76 -0
- package/dist/contracts/review-findings.d.ts +46 -0
- package/dist/contracts/review-findings.js +48 -0
- package/dist/contracts/route.d.ts +484 -0
- package/dist/contracts/route.js +128 -0
- package/dist/contracts/scope.d.ts +105 -0
- package/dist/contracts/scope.js +114 -0
- package/dist/contracts/scout-report.d.ts +234 -0
- package/dist/contracts/scout-report.js +141 -0
- package/dist/contracts/slack-callback.d.ts +43 -0
- package/dist/contracts/slack-callback.js +35 -0
- package/dist/contracts/sort-answer.d.ts +83 -0
- package/dist/contracts/sort-answer.js +77 -0
- package/dist/contracts/stage-output.d.ts +127 -0
- package/dist/contracts/stage-output.js +166 -0
- package/dist/contracts/task-sizing.d.ts +24 -0
- package/dist/contracts/task-sizing.js +28 -0
- package/dist/contracts/teammate-turn.d.ts +43 -0
- package/dist/contracts/teammate-turn.js +51 -0
- package/dist/contracts/teams-callback.d.ts +14 -0
- package/dist/contracts/teams-callback.js +14 -0
- package/dist/contracts/telegram-callback.d.ts +120 -0
- package/dist/contracts/telegram-callback.js +97 -0
- package/dist/decision.d.ts +11 -20
- package/dist/decision.js +86 -167
- package/dist/desktop-update.d.ts +1 -1
- package/dist/discord-chat.js +31 -20
- package/dist/flow-engine.js +14 -17
- package/dist/flow-items.d.ts +4 -13
- package/dist/flow-items.js +1 -15
- package/dist/flow-send.d.ts +6 -37
- package/dist/flow-send.js +18 -14
- package/dist/flow-share.js +7 -1
- package/dist/flow-sort.d.ts +7 -26
- package/dist/flow-sort.js +25 -20
- package/dist/flow-steps.js +3 -2
- package/dist/flows.d.ts +2 -4
- package/dist/flows.js +9 -5
- package/dist/invoke.js +4 -4
- package/dist/lead-context.d.ts +4 -2
- package/dist/lead-context.js +24 -17
- package/dist/mate-progress.d.ts +26 -0
- package/dist/mate-progress.js +24 -0
- package/dist/mate-tools.d.ts +4 -0
- package/dist/mate-tools.js +108 -275
- package/dist/mate.js +11 -2
- package/dist/mcp-connect.d.ts +24 -4
- package/dist/mcp-connect.js +78 -15
- package/dist/mcp.js +84 -216
- package/dist/memory-pass.d.ts +17 -42
- package/dist/memory-pass.js +54 -26
- package/dist/phase-routing.d.ts +19 -72
- package/dist/phase-routing.js +34 -164
- package/dist/project-knowledge.d.ts +16 -35
- package/dist/project-knowledge.js +42 -21
- package/dist/project-memory.d.ts +2 -28
- package/dist/project-memory.js +28 -13
- package/dist/project-skills.d.ts +16 -38
- package/dist/project-skills.js +41 -32
- package/dist/project-tools.d.ts +1 -0
- package/dist/project-tools.js +4 -4
- package/dist/provider.d.ts +2 -0
- package/dist/provider.js +4 -16
- package/dist/result-shots.d.ts +1 -1
- package/dist/result-shots.js +2 -1
- package/dist/review-switch.d.ts +3 -8
- package/dist/reviewer.d.ts +8 -12
- package/dist/reviewer.js +11 -35
- package/dist/scope.d.ts +21 -43
- package/dist/scope.js +42 -128
- package/dist/scout-report.d.ts +9 -194
- package/dist/scout-report.js +113 -264
- package/dist/scout.js +15 -11
- package/dist/serve.js +23 -5
- package/dist/setup-guide.d.ts +2 -2
- package/dist/slack-chat.js +26 -20
- package/dist/slack-state.js +2 -1
- package/dist/store.d.ts +9 -32
- package/dist/store.js +18 -15
- package/dist/structured-output.js +2 -1
- package/dist/subscription-chat.d.ts +4 -0
- package/dist/subscription-chat.js +4 -1
- package/dist/task-review-brief.d.ts +1 -7
- package/dist/task-review-brief.js +6 -6
- package/dist/task-sizing.d.ts +3 -27
- package/dist/task-sizing.js +11 -26
- package/dist/task-text.d.ts +0 -5
- package/dist/task-text.js +1 -3
- package/dist/teammate-question.js +7 -5
- package/dist/teammate-work.js +9 -5
- package/dist/teammates.d.ts +13 -64
- package/dist/teammates.js +32 -33
- package/dist/teams-chat.js +20 -11
- package/dist/telegram-flow.d.ts +9 -0
- package/dist/telegram-flow.js +44 -8
- package/dist/telegram-mate.d.ts +4 -14
- package/dist/telegram-mate.js +9 -8
- package/dist/telegram-team.d.ts +2 -19
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +94 -68
- package/dist/text-limits.d.ts +142 -1
- package/dist/text-limits.js +144 -1
- package/dist/tools-ui.d.ts +2 -1
- package/dist/tools-ui.js +7 -4
- package/package.json +17 -12
package/dist/mate.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
|
-
import { mateToolLabel } from "./mate-progress.js";
|
|
2
|
+
import { mateToolFailureReason, mateToolLabel } from "./mate-progress.js";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { mateTimeoutNotice } from "./store.js";
|
|
5
5
|
import { RESULT_IMAGES_PER_TURN_CAP } from "./chat-evidence.js";
|
|
@@ -499,8 +499,17 @@ export async function runMateTurn(input) {
|
|
|
499
499
|
if (changed !== null)
|
|
500
500
|
return changed;
|
|
501
501
|
}
|
|
502
|
-
|
|
502
|
+
// Position within this turn is unique even if a provider reuses call ids.
|
|
503
|
+
const progressId = `${turnId}:${steps}:${index}`;
|
|
504
|
+
progress({ kind: "tool", turn: turnId, step: steps, id: progressId, label: mateToolLabel(call.name) });
|
|
503
505
|
const outcome = executeMateTool({ store, who, now: clock(), draft, selectEvidence, step: steps, readDecisions, readResults, searchedMemory, checkedCapabilities, ask, ...(input.integrations === undefined ? {} : { integrations: input.integrations }), thread: thread.id, turn: turnId, ...(promiseChannelOfTurn(input.channel) === undefined ? {} : { channel: promiseChannelOfTurn(input.channel) }), ...(input.evidenceRoot === undefined ? {} : { evidenceRoot: input.evidenceRoot }), ...(input.mediaDelivery === undefined ? {} : { mediaDelivery: input.mediaDelivery }) }, call.name, call.args, view);
|
|
506
|
+
let displayOutcome = { state: "succeeded" };
|
|
507
|
+
if (!outcome.ok) {
|
|
508
|
+
const reason = mateToolFailureReason(call.name, outcome.message);
|
|
509
|
+
displayOutcome = { state: "failed", reason: scanForSecrets(reason).length === 0
|
|
510
|
+
? reason : `That step didn't work (${mateToolLabel(call.name)}).` };
|
|
511
|
+
}
|
|
512
|
+
progress({ kind: "tool-result", turn: turnId, step: steps, id: progressId, outcome: displayOutcome });
|
|
504
513
|
if (READ_TOOLS.has(call.name))
|
|
505
514
|
reads++;
|
|
506
515
|
if (call.name === "show_control" && outcome.ok)
|
package/dist/mcp-connect.d.ts
CHANGED
|
@@ -14,6 +14,9 @@ export type OneClick = {
|
|
|
14
14
|
/** Send no scope: the server applies its own preset for this resource (PostHog's MCP preset). Naming scopes there
|
|
15
15
|
* shows a 75-group permission grid instead, and the person can stop on it without ever coming back. */
|
|
16
16
|
serverScopes?: true;
|
|
17
|
+
/** Actions a research step may use by exact name, checked before the name rule: only while the connection's saved
|
|
18
|
+
* grant is read-only (readOnlyGrant). PostHog's server lists one action, `exec`, that reads or writes as the grant allows. */
|
|
19
|
+
readActions?: readonly string[];
|
|
17
20
|
};
|
|
18
21
|
/**
|
|
19
22
|
* Services that connect this way: their servers speak streamable HTTP and
|
|
@@ -41,13 +44,18 @@ export declare const localConnectOf: (id: string) => {
|
|
|
41
44
|
label: string;
|
|
42
45
|
about: string;
|
|
43
46
|
} | null;
|
|
44
|
-
/**
|
|
45
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Where each service stands in a project: connected, open to connect, or its name taken by a tool set up another way.
|
|
49
|
+
* `local` connects with no sign-in. `research` says, for a connected service research can't use because of the access
|
|
50
|
+
* it was granted, why (shown on its Tools entry beside Reconnect read-only).
|
|
51
|
+
*/
|
|
52
|
+
export declare function connectionsOf(store: Store, repo: string, home?: string): {
|
|
46
53
|
id: string;
|
|
47
54
|
label: string;
|
|
48
55
|
about: string;
|
|
49
56
|
state: "connected" | "open" | "taken";
|
|
50
57
|
local?: true;
|
|
58
|
+
research?: string;
|
|
51
59
|
}[];
|
|
52
60
|
/**
|
|
53
61
|
* Whether an action only reads: its name has a reading verb, and every other
|
|
@@ -55,13 +63,21 @@ export declare function connectionsOf(store: Store, repo: string): {
|
|
|
55
63
|
* research read. An allow-list: a word nobody listed withholds the action.
|
|
56
64
|
*/
|
|
57
65
|
export declare function readsOnly(action: string, subjects: readonly string[]): boolean;
|
|
66
|
+
export declare const readScope: (scope: string) => boolean;
|
|
67
|
+
/** 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. */
|
|
68
|
+
export declare const readOnlyGrant: (grant: readonly string[] | null) => boolean;
|
|
69
|
+
/** An OAuth `scope` value as scope tokens (RFC 6749), sorted and deduplicated; null when absent, empty or malformed. */
|
|
70
|
+
export declare function scopeList(value: unknown): string[] | null;
|
|
71
|
+
/** The scope a connected service's sign-in was granted, as saved beside its token; null when it was never proved. */
|
|
72
|
+
export declare function grantOf(repo: string, tool: string, home?: string): string[] | null;
|
|
58
73
|
/**
|
|
59
74
|
* What a research step may use of a project's connected services: each
|
|
60
75
|
* signed-in service's read-only actions, as its last test listed them —
|
|
61
76
|
* by server (`reads`, for Codex's `enabled_tools`) and named the way Claude
|
|
62
77
|
* allows them (`mcp__<service>__<action>`). `local` is the loopback host of
|
|
63
78
|
* each app on this computer it reads (its proxy lets only those through). A service not connected, without
|
|
64
|
-
* read-only actions, or left out of this run (`launched`) gives nothing.
|
|
79
|
+
* read-only actions, or left out of this run (`launched`) gives nothing. `withheld` is why a service that can name
|
|
80
|
+
* its research actions gave none (its grant, `granted`, writes or was never proved): the run says so.
|
|
65
81
|
*/
|
|
66
82
|
export type ResearchTools = {
|
|
67
83
|
services: {
|
|
@@ -71,8 +87,9 @@ export type ResearchTools = {
|
|
|
71
87
|
allowed: string[];
|
|
72
88
|
reads: Record<string, string[]>;
|
|
73
89
|
local: string[];
|
|
90
|
+
withheld?: Record<string, string>;
|
|
74
91
|
};
|
|
75
|
-
export declare function researchToolsOf(tools: readonly ProjectTool[], launched?: ReadonlySet<string> | null): ResearchTools;
|
|
92
|
+
export declare function researchToolsOf(tools: readonly ProjectTool[], launched?: ReadonlySet<string> | null, granted?: (tool: string) => readonly string[] | null): ResearchTools;
|
|
76
93
|
/** Where a service sends the person back after they sign in. */
|
|
77
94
|
export declare const CONNECT_CALLBACK = "/settings/tools/connected";
|
|
78
95
|
type Fetch = typeof fetch;
|
|
@@ -97,6 +114,8 @@ export type ConnectVisit = {
|
|
|
97
114
|
resource: string;
|
|
98
115
|
redirect: string;
|
|
99
116
|
expires: number;
|
|
117
|
+
/** The scope asked for, when one was: the grant when the token answer leaves `scope` out (RFC 6749 §5.1). */
|
|
118
|
+
scope?: string | null;
|
|
100
119
|
};
|
|
101
120
|
/** A server's sign-in: the resource metadata its 401 names (or the well-known one), then its authorization server's metadata. */
|
|
102
121
|
export declare function discoverSignIn(mcpUrl: string, fetcher?: Fetch): Promise<Server | null>;
|
|
@@ -112,6 +131,7 @@ export declare function startConnect(input: {
|
|
|
112
131
|
origin: string;
|
|
113
132
|
kit?: string | null;
|
|
114
133
|
template?: string | null;
|
|
134
|
+
readOnly?: boolean;
|
|
115
135
|
}, fetcher?: Fetch, now?: number): Promise<{
|
|
116
136
|
ok: true;
|
|
117
137
|
go: string;
|
package/dist/mcp-connect.js
CHANGED
|
@@ -52,7 +52,7 @@ export const ONE_CLICK = [
|
|
|
52
52
|
// Mobbin's sign-in is its Supabase auth server, on another origin: its protected-resource metadata names it.
|
|
53
53
|
{ id: "mobbin", label: "Mobbin", url: "https://api.mobbin.com/mcp", about: "Real app screens and flows to learn from.",
|
|
54
54
|
reads: ["screen", "flow", "app", "site", "element", "pattern", "ui", "ios", "android", "web"] },
|
|
55
|
-
{ id: "posthog", label: "PostHog", url: "https://mcp.posthog.com/mcp", about: "Product analytics, funnels and events.", serverScopes: true,
|
|
55
|
+
{ id: "posthog", label: "PostHog", url: "https://mcp.posthog.com/mcp", about: "Product analytics, funnels and events.", serverScopes: true, readActions: ["exec"],
|
|
56
56
|
reads: ["insight", "run", "event", "definition", "property", "properties"] },
|
|
57
57
|
{ id: "betterstack", label: "Better Stack", url: "https://mcp.betterstack.com", about: "Uptime checks and incidents.",
|
|
58
58
|
reads: ["uptime", "monitor", "incident", "availability", "response", "time"] },
|
|
@@ -75,7 +75,7 @@ export function oneClickServices(environment = process.env) {
|
|
|
75
75
|
if (!/^[a-z0-9-]{1,40}$/.test(id) || label === "" || !loopback(url))
|
|
76
76
|
return ONE_CLICK;
|
|
77
77
|
const known = ONE_CLICK.find(one => one.id === id);
|
|
78
|
-
const standIn = { id, label, url, about: known?.about ?? "A service on this computer.", ...(known?.reads === undefined ? {} : { reads: known.reads }) };
|
|
78
|
+
const standIn = { id, label, url, about: known?.about ?? "A service on this computer.", ...(known?.reads === undefined ? {} : { reads: known.reads }), ...(known?.readActions === undefined ? {} : { readActions: known.readActions }) };
|
|
79
79
|
return ONE_CLICK.some(one => one.id === id) ? ONE_CLICK.map(one => one.id === id ? standIn : one) : [...ONE_CLICK, standIn];
|
|
80
80
|
}
|
|
81
81
|
export const oneClickOf = (id) => oneClickServices().find(one => one.id === id) ?? null;
|
|
@@ -86,12 +86,18 @@ export const localConnectOf = (id) => {
|
|
|
86
86
|
const app = LOCAL_APPS.find(one => one.tool === id), tool = catalogTool(id);
|
|
87
87
|
return app === undefined || tool === null ? null : { id, label: tool.label, about: app.connect };
|
|
88
88
|
};
|
|
89
|
-
/**
|
|
90
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Where each service stands in a project: connected, open to connect, or its name taken by a tool set up another way.
|
|
91
|
+
* `local` connects with no sign-in. `research` says, for a connected service research can't use because of the access
|
|
92
|
+
* it was granted, why (shown on its Tools entry beside Reconnect read-only).
|
|
93
|
+
*/
|
|
94
|
+
export function connectionsOf(store, repo, home = homedir()) {
|
|
91
95
|
const tools = projectToolsOf(store, repo);
|
|
92
96
|
const signIn = oneClickServices().map(service => {
|
|
93
97
|
const had = tools.find(one => one.name === service.id);
|
|
94
|
-
|
|
98
|
+
const state = had === undefined ? "open" : had.spec.url === service.url && had.spec.bearer === ACCESS ? "connected" : "taken";
|
|
99
|
+
const withheld = state === "connected" && service.readActions !== undefined ? researchAccess(service, had, grantOf(repo, service.id, home)).withheld : null;
|
|
100
|
+
return { id: service.id, label: service.label, about: service.about, state, ...(withheld === null ? {} : { research: withheld.said }) };
|
|
95
101
|
});
|
|
96
102
|
const local = LOCAL_APPS.flatMap(app => {
|
|
97
103
|
const one = localConnectOf(app.tool);
|
|
@@ -119,18 +125,62 @@ export function readsOnly(action, subjects) {
|
|
|
119
125
|
const subject = (word) => subjects.includes(word) || subjects.includes(word.replace(/s$/, ""));
|
|
120
126
|
return words.some(word => READING.has(word)) && words.every(word => READING.has(word) || QUALIFIERS.has(word) || subject(word));
|
|
121
127
|
}
|
|
122
|
-
|
|
128
|
+
/**
|
|
129
|
+
* Identity scopes a read-only grant may carry beside `<resource>:read` ones. An allow-list: any other scope (a
|
|
130
|
+
* write, a wildcard, `introspection`, one nobody listed) makes the grant unsafe for research.
|
|
131
|
+
*/
|
|
132
|
+
const IDENTITY_SCOPES = new Set(["openid", "profile", "email"]);
|
|
133
|
+
const READ_SCOPE = /^[a-z][a-z0-9_]{0,63}:read$/;
|
|
134
|
+
export const readScope = (scope) => IDENTITY_SCOPES.has(scope) || READ_SCOPE.test(scope);
|
|
135
|
+
/** 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. */
|
|
136
|
+
export const readOnlyGrant = (grant) => grant !== null && grant.length > 0 && grant.every(readScope);
|
|
137
|
+
/** An OAuth `scope` value as scope tokens (RFC 6749), sorted and deduplicated; null when absent, empty or malformed. */
|
|
138
|
+
export function scopeList(value) {
|
|
139
|
+
if (typeof value !== "string")
|
|
140
|
+
return null;
|
|
141
|
+
const tokens = value.split(" ").filter(one => one !== "");
|
|
142
|
+
if (tokens.length === 0 || tokens.length > 512 || !tokens.every(one => SCOPE_TOKEN.test(one)))
|
|
143
|
+
return null;
|
|
144
|
+
return [...new Set(tokens)].sort();
|
|
145
|
+
}
|
|
146
|
+
/** The scope a connected service's sign-in was granted, as saved beside its token; null when it was never proved. */
|
|
147
|
+
export function grantOf(repo, tool, home = homedir()) {
|
|
148
|
+
try {
|
|
149
|
+
return scopeList(readToolSecrets(repo, tool, home)[GRANTED]);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* What research may use of one connected service: its exact `readActions` while the saved grant only reads, and
|
|
157
|
+
* otherwise (as for every service) the actions whose names only read. A service with `readActions` that gives
|
|
158
|
+
* research nothing says why, plainly, for the run and its Tools entry.
|
|
159
|
+
*/
|
|
160
|
+
function researchAccess(service, tool, grant) {
|
|
161
|
+
const listed = tool.lastTest?.ok ? tool.lastTest.tools : [];
|
|
162
|
+
const exact = service.readActions !== undefined && readOnlyGrant(grant) ? service.readActions : [];
|
|
163
|
+
const reads = listed.filter(action => exact.includes(action) || (service.reads !== undefined && readsOnly(action, service.reads)));
|
|
164
|
+
if (reads.length > 0 || service.readActions === undefined || listed.length === 0 || readOnlyGrant(grant))
|
|
165
|
+
return { reads, withheld: null };
|
|
166
|
+
const why = grant === null ? `${service.label} didn't say what access it granted, so research runs can't use it`
|
|
167
|
+
: `${service.label} is connected with write access, so research runs can't use it`;
|
|
168
|
+
return { reads, withheld: { said: `${why}.`, reason: `${why}; reconnect read-only` } };
|
|
169
|
+
}
|
|
170
|
+
export function researchToolsOf(tools, launched = null, granted = () => null) {
|
|
123
171
|
const research = { services: [], allowed: [], reads: {}, local: [] };
|
|
124
172
|
// Signed-in services, then apps on this computer (each exactly as listed: its own address, no key).
|
|
125
173
|
const readable = [
|
|
126
|
-
...oneClickServices().map(service => ({ id: service.id, label: service.label, reads: service.reads, is: (spec) => spec.url === service.url && spec.bearer === ACCESS })),
|
|
127
|
-
...LOCAL_APPS.map(app => ({ id: app.tool, label: catalogTool(app.tool)?.label ?? app.tool, reads: app.reads, is: (spec) => localAppOf(spec) === app })),
|
|
174
|
+
...oneClickServices().map(service => ({ id: service.id, label: service.label, reads: service.reads, readActions: service.readActions, is: (spec) => spec.url === service.url && spec.bearer === ACCESS })),
|
|
175
|
+
...LOCAL_APPS.map(app => ({ id: app.tool, label: catalogTool(app.tool)?.label ?? app.tool, reads: app.reads, readActions: undefined, is: (spec) => localAppOf(spec) === app })),
|
|
128
176
|
];
|
|
129
177
|
for (const service of readable) {
|
|
130
178
|
const tool = tools.find(one => one.name === service.id);
|
|
131
|
-
if (service.reads === undefined || tool === undefined || !service.is(tool.spec) || (launched !== null && !launched.has(tool.name)))
|
|
179
|
+
if ((service.reads === undefined && service.readActions === undefined) || tool === undefined || !service.is(tool.spec) || (launched !== null && !launched.has(tool.name)))
|
|
132
180
|
continue;
|
|
133
|
-
const reads = (tool.
|
|
181
|
+
const { reads, withheld } = researchAccess(service, tool, service.readActions === undefined ? null : granted(tool.name));
|
|
182
|
+
if (withheld !== null)
|
|
183
|
+
research.withheld = { ...research.withheld, [service.id]: withheld.reason };
|
|
134
184
|
if (reads.length === 0)
|
|
135
185
|
continue;
|
|
136
186
|
research.services.push({ id: service.id, label: service.label });
|
|
@@ -143,6 +193,8 @@ export function researchToolsOf(tools, launched = null) {
|
|
|
143
193
|
}
|
|
144
194
|
/** The secrets a connected tool keeps: the bearer the MCP server takes, and what refreshing it needs. */
|
|
145
195
|
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";
|
|
196
|
+
/** The scope the sign-in granted, kept beside its token. Not one of the spec's secrets, so it never reaches a launch. */
|
|
197
|
+
const GRANTED = "OAUTH_GRANTED_SCOPE";
|
|
146
198
|
/** Where a service sends the person back after they sign in. */
|
|
147
199
|
export const CONNECT_CALLBACK = "/settings/tools/connected";
|
|
148
200
|
const VISIT_MS = 15 * 60_000, RENEW_RETRY_MS = 5 * 60_000;
|
|
@@ -252,9 +304,16 @@ export async function startConnect(input, fetcher = fetch, now = Date.now()) {
|
|
|
252
304
|
go.searchParams.set("code_challenge_method", "S256");
|
|
253
305
|
go.searchParams.set("state", state);
|
|
254
306
|
go.searchParams.set("resource", resource);
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
307
|
+
// Reconnect read-only (a service that names its research actions): only the read and identity scopes the server
|
|
308
|
+
// lists. Otherwise every listed scope, or none where the server applies its own preset.
|
|
309
|
+
const scopes = input.readOnly === true && service.readActions !== undefined ? server.scopes.filter(readScope)
|
|
310
|
+
: service.serverScopes === true ? [] : server.scopes;
|
|
311
|
+
if (input.readOnly === true && service.readActions !== undefined && scopes.length === 0)
|
|
312
|
+
return { ok: false, said: `${service.label} didn't list any read-only access to ask for.` };
|
|
313
|
+
const scope = scopes.length > 0 ? scopes.join(" ") : null;
|
|
314
|
+
if (scope !== null)
|
|
315
|
+
go.searchParams.set("scope", scope);
|
|
316
|
+
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 } };
|
|
258
317
|
}
|
|
259
318
|
/** The spec a connected service joins the project with: its MCP address, signed in with the token its sign-in gave. */
|
|
260
319
|
export function connectedSpec(serviceId) {
|
|
@@ -300,7 +359,9 @@ export async function finishConnect(store, visit, code, now, options = {}) {
|
|
|
300
359
|
if (!added.ok)
|
|
301
360
|
return { ok: false, said: added.message };
|
|
302
361
|
}
|
|
303
|
-
|
|
362
|
+
// The grant: what the answer says, or what was asked for when it says nothing; never one from an earlier sign-in.
|
|
363
|
+
const granted = "scope" in body ? scopeList(body["scope"]) : scopeList(visit.scope ?? null);
|
|
364
|
+
setToolSecrets(visit.repo, spec.name, { [ACCESS]: tokens.access, [CLIENT]: visit.clientId, [TOKEN_URL]: visit.token, [RESOURCE]: visit.resource, [GRANTED]: granted?.join(" ") ?? "",
|
|
304
365
|
...(tokens.refresh === null ? {} : { [REFRESH]: tokens.refresh }), ...(tokens.expires === null ? {} : { [EXPIRES]: tokens.expires }), ...(visit.clientSecret === null ? {} : { [CLIENT_SECRET]: visit.clientSecret }) }, home);
|
|
305
366
|
// The success line names the project it went to: the page and chat may be looking at another.
|
|
306
367
|
const to = basename(visit.repo);
|
|
@@ -336,7 +397,9 @@ export async function refreshConnections(store, repos, now, options = {}) {
|
|
|
336
397
|
report.problems.push(`${tool.name}: its sign-in ran out; connect it again on the Tools page`);
|
|
337
398
|
continue;
|
|
338
399
|
}
|
|
339
|
-
|
|
400
|
+
// A refresh carries the same grant or a narrower one, and says so when it changes; said badly, it is unknown.
|
|
401
|
+
setToolSecrets(repo, tool.name, { [ACCESS]: tokens.access, ...(tokens.refresh === null ? {} : { [REFRESH]: tokens.refresh }), ...(tokens.expires === null ? {} : { [EXPIRES]: tokens.expires }),
|
|
402
|
+
...("scope" in body ? { [GRANTED]: scopeList(body["scope"])?.join(" ") ?? "" } : {}) }, home);
|
|
340
403
|
renewTried.delete(key);
|
|
341
404
|
report.refreshed++;
|
|
342
405
|
}
|