toolroll 0.9.34 → 0.9.37
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 +102 -0
- package/README.md +7 -0
- package/dist/assignment-adapters.d.ts +12 -48
- package/dist/assignment-adapters.js +33 -43
- package/dist/assignment.d.ts +1 -0
- package/dist/assignment.js +2 -2
- package/dist/browser-workspace.d.ts +3 -3
- package/dist/build-review.d.ts +1 -1
- package/dist/build-review.js +2 -1
- package/dist/builder.js +4 -7
- package/dist/chat-acceptance.d.ts +2 -2
- 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-decide.d.ts +14 -1
- package/dist/chat-decide.js +12 -6
- 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 +40 -7
- package/dist/contracts/contract.js +155 -19
- 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-propose.d.ts +325 -0
- package/dist/contracts/flow-propose.js +41 -0
- package/dist/contracts/flow-send.d.ts +119 -0
- package/dist/contracts/flow-send.js +112 -0
- package/dist/contracts/flow.d.ts +19405 -0
- package/dist/contracts/flow.js +364 -0
- package/dist/contracts/gateway-tools.d.ts +374 -0
- package/dist/contracts/gateway-tools.js +94 -0
- package/dist/contracts/handoff.d.ts +91 -0
- package/dist/contracts/handoff.js +82 -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/proof.d.ts +136 -0
- package/dist/contracts/proof.js +110 -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/contracts/verification-receipt.d.ts +148 -0
- package/dist/contracts/verification-receipt.js +87 -0
- package/dist/decision.d.ts +11 -20
- package/dist/decision.js +86 -167
- package/dist/demo.js +28 -11
- package/dist/discord-chat.js +31 -20
- package/dist/evidence.d.ts +3 -40
- package/dist/evidence.js +5 -1
- package/dist/flow-engine.js +14 -17
- package/dist/flow-gallery.d.ts +3 -2
- 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.d.ts +10 -32
- package/dist/flow-share.js +72 -84
- package/dist/flow-sort.d.ts +7 -26
- package/dist/flow-sort.js +25 -20
- package/dist/flow-starters.d.ts +2 -1
- package/dist/flow-steps.js +3 -2
- package/dist/flow-triggers.d.ts +15 -72
- package/dist/flow-triggers.js +94 -83
- package/dist/flows.d.ts +37 -220
- package/dist/flows.js +356 -387
- package/dist/invoke.js +4 -4
- package/dist/lead-context.d.ts +4 -2
- package/dist/lead-context.js +24 -17
- package/dist/mate-tools.d.ts +4 -0
- package/dist/mate-tools.js +111 -308
- package/dist/mcp-connect.d.ts +27 -4
- package/dist/mcp-connect.js +92 -17
- package/dist/mcp.js +84 -216
- package/dist/memory-pass.d.ts +17 -42
- package/dist/memory-pass.js +54 -26
- package/dist/operate.js +31 -2
- package/dist/phase-routing.d.ts +19 -72
- package/dist/phase-routing.js +34 -164
- package/dist/plan.d.ts +5 -5
- package/dist/plan.js +5 -25
- package/dist/process-recovery-eligibility.d.ts +2 -2
- package/dist/process-recovery-eligibility.js +13 -8
- 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/proof.d.ts +15 -91
- package/dist/proof.js +90 -249
- package/dist/provider.d.ts +2 -0
- package/dist/provider.js +4 -16
- package/dist/result-evidence-readers.d.ts +42 -0
- package/dist/result-evidence-readers.js +91 -0
- 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 +17 -101
- 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/surface.js +8 -1
- package/dist/task-review-brief.d.ts +69 -0
- package/dist/task-review-brief.js +179 -0
- 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.js +4 -3
- package/dist/telegram-mate.d.ts +4 -14
- package/dist/telegram-mate.js +8 -7
- package/dist/telegram-team.d.ts +2 -19
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +52 -42
- package/dist/text-limits.d.ts +212 -1
- package/dist/text-limits.js +214 -1
- package/dist/tools-ui.d.ts +2 -1
- package/dist/tools-ui.js +7 -4
- package/dist/verification-evidence.js +24 -12
- package/package.json +1 -1
package/dist/mcp-connect.d.ts
CHANGED
|
@@ -11,6 +11,12 @@ export type OneClick = {
|
|
|
11
11
|
url: string;
|
|
12
12
|
about: string;
|
|
13
13
|
reads?: readonly string[];
|
|
14
|
+
/** Send no scope: the server applies its own preset for this resource (PostHog's MCP preset). Naming scopes there
|
|
15
|
+
* shows a 75-group permission grid instead, and the person can stop on it without ever coming back. */
|
|
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[];
|
|
14
20
|
};
|
|
15
21
|
/**
|
|
16
22
|
* Services that connect this way: their servers speak streamable HTTP and
|
|
@@ -38,13 +44,18 @@ export declare const localConnectOf: (id: string) => {
|
|
|
38
44
|
label: string;
|
|
39
45
|
about: string;
|
|
40
46
|
} | null;
|
|
41
|
-
/**
|
|
42
|
-
|
|
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): {
|
|
43
53
|
id: string;
|
|
44
54
|
label: string;
|
|
45
55
|
about: string;
|
|
46
56
|
state: "connected" | "open" | "taken";
|
|
47
57
|
local?: true;
|
|
58
|
+
research?: string;
|
|
48
59
|
}[];
|
|
49
60
|
/**
|
|
50
61
|
* Whether an action only reads: its name has a reading verb, and every other
|
|
@@ -52,13 +63,21 @@ export declare function connectionsOf(store: Store, repo: string): {
|
|
|
52
63
|
* research read. An allow-list: a word nobody listed withholds the action.
|
|
53
64
|
*/
|
|
54
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;
|
|
55
73
|
/**
|
|
56
74
|
* What a research step may use of a project's connected services: each
|
|
57
75
|
* signed-in service's read-only actions, as its last test listed them —
|
|
58
76
|
* by server (`reads`, for Codex's `enabled_tools`) and named the way Claude
|
|
59
77
|
* allows them (`mcp__<service>__<action>`). `local` is the loopback host of
|
|
60
78
|
* each app on this computer it reads (its proxy lets only those through). A service not connected, without
|
|
61
|
-
* 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.
|
|
62
81
|
*/
|
|
63
82
|
export type ResearchTools = {
|
|
64
83
|
services: {
|
|
@@ -68,8 +87,9 @@ export type ResearchTools = {
|
|
|
68
87
|
allowed: string[];
|
|
69
88
|
reads: Record<string, string[]>;
|
|
70
89
|
local: string[];
|
|
90
|
+
withheld?: Record<string, string>;
|
|
71
91
|
};
|
|
72
|
-
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;
|
|
73
93
|
/** Where a service sends the person back after they sign in. */
|
|
74
94
|
export declare const CONNECT_CALLBACK = "/settings/tools/connected";
|
|
75
95
|
type Fetch = typeof fetch;
|
|
@@ -94,6 +114,8 @@ export type ConnectVisit = {
|
|
|
94
114
|
resource: string;
|
|
95
115
|
redirect: string;
|
|
96
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;
|
|
97
119
|
};
|
|
98
120
|
/** A server's sign-in: the resource metadata its 401 names (or the well-known one), then its authorization server's metadata. */
|
|
99
121
|
export declare function discoverSignIn(mcpUrl: string, fetcher?: Fetch): Promise<Server | null>;
|
|
@@ -109,6 +131,7 @@ export declare function startConnect(input: {
|
|
|
109
131
|
origin: string;
|
|
110
132
|
kit?: string | null;
|
|
111
133
|
template?: string | null;
|
|
134
|
+
readOnly?: boolean;
|
|
112
135
|
}, fetcher?: Fetch, now?: number): Promise<{
|
|
113
136
|
ok: true;
|
|
114
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.",
|
|
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,10 +193,15 @@ 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;
|
|
149
201
|
const renewTried = new Map();
|
|
202
|
+
/** RFC 6749 scope tokens; and the most scope text a sign-in address carries (addresses stay well under 8 KB). */
|
|
203
|
+
const SCOPE_TOKEN = /^[\x21\x23-\x5b\x5d-\x7e]{1,128}$/;
|
|
204
|
+
const SCOPE_PARAM_LIMIT = 6_000;
|
|
150
205
|
const json = async (fetcher, url) => {
|
|
151
206
|
try {
|
|
152
207
|
const answer = await fetcher(url, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(10_000) });
|
|
@@ -195,10 +250,19 @@ export async function discoverSignIn(mcpUrl, fetcher = fetch) {
|
|
|
195
250
|
const methods = Array.isArray(meta["code_challenge_methods_supported"]) ? meta["code_challenge_methods_supported"] : ["S256"];
|
|
196
251
|
if (!methods.includes("S256"))
|
|
197
252
|
return null;
|
|
198
|
-
|
|
253
|
+
// Ask for every scope the server lists for this resource: a token missing one the server itself requires is
|
|
254
|
+
// refused after a successful sign-in (PostHog lists 155 and needs user:read, the 140th). Only well-formed scope
|
|
255
|
+
// tokens, and at most what fits a sign-in address.
|
|
256
|
+
const listed = Array.isArray(resource?.["scopes_supported"]) ? resource["scopes_supported"].filter((one) => typeof one === "string" && SCOPE_TOKEN.test(one)) : [];
|
|
257
|
+
const scopes = [];
|
|
258
|
+
for (const one of listed) {
|
|
259
|
+
if (scopes.join(" ").length + one.length + 1 > SCOPE_PARAM_LIMIT)
|
|
260
|
+
break;
|
|
261
|
+
scopes.push(one);
|
|
262
|
+
}
|
|
199
263
|
// A token is for the resource the server names (Stripe's has no trailing slash), when it names one.
|
|
200
264
|
const named = resource?.["resource"];
|
|
201
|
-
return { authorize: meta["authorization_endpoint"], token: meta["token_endpoint"], register: meta["registration_endpoint"], scopes
|
|
265
|
+
return { authorize: meta["authorization_endpoint"], token: meta["token_endpoint"], register: meta["registration_endpoint"], scopes, resource: https(named) ? named : null };
|
|
202
266
|
}
|
|
203
267
|
/**
|
|
204
268
|
* Start connecting a service: find its sign-in, register this console as a
|
|
@@ -240,9 +304,16 @@ export async function startConnect(input, fetcher = fetch, now = Date.now()) {
|
|
|
240
304
|
go.searchParams.set("code_challenge_method", "S256");
|
|
241
305
|
go.searchParams.set("state", state);
|
|
242
306
|
go.searchParams.set("resource", resource);
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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 } };
|
|
246
317
|
}
|
|
247
318
|
/** The spec a connected service joins the project with: its MCP address, signed in with the token its sign-in gave. */
|
|
248
319
|
export function connectedSpec(serviceId) {
|
|
@@ -288,7 +359,9 @@ export async function finishConnect(store, visit, code, now, options = {}) {
|
|
|
288
359
|
if (!added.ok)
|
|
289
360
|
return { ok: false, said: added.message };
|
|
290
361
|
}
|
|
291
|
-
|
|
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(" ") ?? "",
|
|
292
365
|
...(tokens.refresh === null ? {} : { [REFRESH]: tokens.refresh }), ...(tokens.expires === null ? {} : { [EXPIRES]: tokens.expires }), ...(visit.clientSecret === null ? {} : { [CLIENT_SECRET]: visit.clientSecret }) }, home);
|
|
293
366
|
// The success line names the project it went to: the page and chat may be looking at another.
|
|
294
367
|
const to = basename(visit.repo);
|
|
@@ -324,7 +397,9 @@ export async function refreshConnections(store, repos, now, options = {}) {
|
|
|
324
397
|
report.problems.push(`${tool.name}: its sign-in ran out; connect it again on the Tools page`);
|
|
325
398
|
continue;
|
|
326
399
|
}
|
|
327
|
-
|
|
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);
|
|
328
403
|
renewTried.delete(key);
|
|
329
404
|
report.refreshed++;
|
|
330
405
|
}
|