@zackbart/connecta 0.25.0 → 0.26.1
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/AGENTS.md +6 -5
- package/CHANGELOG.md +387 -0
- package/README.md +32 -15
- package/SECURITY.md +6 -5
- package/bin/connecta.mjs +5 -1
- package/dist/activity.d.ts +10 -3
- package/dist/activity.js +10 -1
- package/dist/artifacts/connector.d.ts +55 -0
- package/dist/artifacts/connector.js +724 -0
- package/dist/artifacts/document.js +80 -0
- package/dist/artifacts/guide.js +140 -0
- package/dist/artifacts/html-scan.js +329 -0
- package/dist/artifacts/json.js +68 -0
- package/dist/artifacts/kv-store.d.ts +20 -0
- package/dist/artifacts/kv-store.js +140 -0
- package/dist/artifacts/markdown.js +478 -0
- package/dist/artifacts/module.d.ts +37 -0
- package/dist/artifacts/module.js +60 -0
- package/dist/artifacts/operations.d.ts +195 -0
- package/dist/artifacts/operations.js +944 -0
- package/dist/artifacts/refresh.d.ts +53 -0
- package/dist/artifacts/refresh.js +235 -0
- package/dist/artifacts/routes.js +217 -0
- package/dist/artifacts/types.d.ts +214 -0
- package/dist/artifacts/types.js +22 -0
- package/dist/artifacts/validate.d.ts +62 -0
- package/dist/artifacts/validate.js +653 -0
- package/dist/artifacts.d.ts +6 -0
- package/dist/artifacts.js +9 -0
- package/dist/auth/downstream-oauth.js +397 -48
- package/dist/auth/oauth-start-reset.js +20 -0
- package/dist/branding.d.ts +87 -0
- package/dist/branding.js +170 -1
- package/dist/catalog-service.js +39 -29
- package/dist/catalog.js +42 -8
- package/dist/connector-access.d.ts +18 -4
- package/dist/connector-access.js +65 -20
- package/dist/connector-caller.d.ts +8 -0
- package/dist/connector-caller.js +19 -0
- package/dist/connectors/remote-mcp.js +48 -9
- package/dist/errors.d.ts +16 -1
- package/dist/errors.js +28 -0
- package/dist/execute.js +431 -75
- package/dist/executor-admission.d.ts +11 -1
- package/dist/executor-admission.js +79 -14
- package/dist/executors/quickjs.js +4 -2
- package/dist/index.d.ts +62 -6
- package/dist/index.js +230 -6
- package/dist/invocation.js +56 -10
- package/dist/meta-tools.js +7 -2
- package/dist/module-contracts.d.ts +21 -1
- package/dist/oauth-callback-outcome.js +43 -18
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/model.d.ts +62 -5
- package/dist/operator-ui/view.js +372 -28
- package/dist/page-styles.js +7 -0
- package/dist/program-source.js +116 -0
- package/dist/registry.d.ts +42 -6
- package/dist/registry.js +164 -60
- package/dist/resumable.js +1291 -0
- package/dist/routes/activity.js +6 -69
- package/dist/routes/actor-labels.js +114 -0
- package/dist/routes/credentials.js +27 -5
- package/dist/routes/mcp.js +120 -59
- package/dist/routes/oauth-management.js +87 -20
- package/dist/routes/oauth.js +54 -87
- package/dist/routes/operator.js +1 -0
- package/dist/routes/shared.d.ts +23 -1
- package/dist/routes/shared.js +20 -2
- package/dist/routes/ui-data.js +2 -1
- package/dist/routes/ui.js +19 -4
- package/dist/run-journal.js +347 -0
- package/dist/runtime/shared-read.js +106 -0
- package/dist/server.js +30 -2
- package/dist/skills.js +30 -11
- package/dist/timeout.js +26 -0
- package/dist/tool-safety.d.ts +35 -0
- package/dist/tool-safety.js +31 -0
- package/dist/types.d.ts +23 -0
- package/dist/ui.d.ts +17 -5
- package/dist/ui.js +51 -68
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +214 -27
- package/documentation/auth.md +186 -2
- package/documentation/code-mode.md +236 -63
- package/documentation/meta-tools.md +55 -29
- package/ethos.md +12 -10
- package/examples/worker/AGENTS.md +6 -0
- package/examples/worker/README.md +174 -10
- package/examples/worker/src/d1-activity-row.ts +14 -2
- package/examples/worker/src/d1-activity.ts +4 -2
- package/examples/worker/src/index.ts +36 -1
- package/examples/worker/src/r2-artifact-blobs.ts +29 -0
- package/examples/worker/wrangler.jsonc +5 -1
- package/package.json +6 -2
- package/templates/node/AGENTS.md +9 -2
- package/templates/node/README.md +31 -3
- package/templates/node/package.json +1 -1
- package/templates/node/src/index.ts +27 -0
package/dist/branding.d.ts
CHANGED
|
@@ -18,6 +18,11 @@ interface ResolvedBranding {
|
|
|
18
18
|
pageTitle: string;
|
|
19
19
|
/** href for the page's icon link. */
|
|
20
20
|
faviconHref: string;
|
|
21
|
+
/**
|
|
22
|
+
* The configured `theme-color`, or the documented `#ffffff` default.
|
|
23
|
+
* Informational: pages emit a configured value as-is, and without one they
|
|
24
|
+
* follow the scheme (`themeColorMeta`) rather than painting this default.
|
|
25
|
+
*/
|
|
21
26
|
themeColor: string;
|
|
22
27
|
/** Only the tokens that survived their gate; the stylesheet owns the rest. */
|
|
23
28
|
theme: ResolvedTheme;
|
|
@@ -43,6 +48,88 @@ export declare function droppedThemeTokens(theme?: ConnectaTheme): string[];
|
|
|
43
48
|
* anything that would need escaping is dropped rather than rewritten.
|
|
44
49
|
*/
|
|
45
50
|
export declare function themeCss(theme: ResolvedTheme): string;
|
|
51
|
+
/**
|
|
52
|
+
* The resolved scheme as the root element's attribute. The dark palette keys
|
|
53
|
+
* off `html[data-scheme]`, so "system" leaves it off and the media query
|
|
54
|
+
* decides.
|
|
55
|
+
*/
|
|
56
|
+
export declare function schemeAttribute(theme: ResolvedTheme): string;
|
|
57
|
+
/** Text or attribute value, escaped for any position in an HTML document. */
|
|
58
|
+
export declare function escapeHtml(value: string): string;
|
|
59
|
+
export interface PageLayout {
|
|
60
|
+
/** The complete `<title>`. */
|
|
61
|
+
title: string;
|
|
62
|
+
/** Whether the operator UI is mounted, and so serves `/favicon.*` and `/`. */
|
|
63
|
+
uiMounted: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* The origin that serves `/favicon.*`, for a page that may be rendered on
|
|
66
|
+
* another host: root-relative icon hrefs resolve against it. A browser 404
|
|
67
|
+
* on the artifact host passes `publicUrl`, since that host answers every
|
|
68
|
+
* non-artifact path, the favicons included, with 404. Omitted, they stay
|
|
69
|
+
* root-relative.
|
|
70
|
+
*/
|
|
71
|
+
iconOrigin?: string | undefined;
|
|
72
|
+
/** Markup after the masthead: the page's `<main>`. */
|
|
73
|
+
body: string;
|
|
74
|
+
/**
|
|
75
|
+
* The page's stylesheet before the theme block. Defaults to the shared
|
|
76
|
+
* tokens and primitives; the operator shell passes its own bundle, which
|
|
77
|
+
* already imports both.
|
|
78
|
+
*/
|
|
79
|
+
styles?: string;
|
|
80
|
+
/** Page-specific rules appended to `styles`, still before the theme. */
|
|
81
|
+
extraStyles?: string;
|
|
82
|
+
/** Additional head markup, emitted verbatim (the Clerk loader). */
|
|
83
|
+
head?: string;
|
|
84
|
+
/** Emitted verbatim at the masthead's end (the operator nav mount). */
|
|
85
|
+
mastheadEnd?: string;
|
|
86
|
+
/** A skip link to this element id, for pages with navigation to skip. */
|
|
87
|
+
skipTo?: {
|
|
88
|
+
id: string;
|
|
89
|
+
label: string;
|
|
90
|
+
};
|
|
91
|
+
/** Markup after the body content, emitted verbatim (the operator script). */
|
|
92
|
+
tail?: string;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The one page layout every server-rendered page shares: the operator shell,
|
|
96
|
+
* the OAuth callback, and browser-facing 404s. Doctype and meta, the favicon a
|
|
97
|
+
* deployment actually serves, the resolved scheme, the token stylesheet with
|
|
98
|
+
* the theme block after it, and the masthead.
|
|
99
|
+
*
|
|
100
|
+
* `/favicon.svg` and `/favicon.ico` are operator UI routes. Without the UI they
|
|
101
|
+
* are 404s, so the default links are left out rather than pointing at nothing
|
|
102
|
+
* (a browser then asks for `/favicon.ico` on its own and gets the same 404). A
|
|
103
|
+
* configured `favicon.href` is the operator's own icon and is linked either way.
|
|
104
|
+
*
|
|
105
|
+
* Every interpolated branding value is escaped here; `body`, `head`,
|
|
106
|
+
* `mastheadEnd`, and `tail` are the caller's markup and are not.
|
|
107
|
+
*/
|
|
108
|
+
export declare function renderPage(branding: ConnectaBranding | undefined, layout: PageLayout): string;
|
|
109
|
+
/**
|
|
110
|
+
* Status marks for one-message pages. Decorative: the label and heading beside
|
|
111
|
+
* each carry the meaning, so the shape repeats it and color is never alone.
|
|
112
|
+
*/
|
|
113
|
+
export declare const STATUS_ICONS: {
|
|
114
|
+
readonly ok: "<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 10.5l3.2 3.2L15 6.8\"/></svg>";
|
|
115
|
+
readonly problem: "<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.2\" stroke-linecap=\"round\"><path d=\"M10 5.5v5.5\"/><path d=\"M10 14.6v.1\"/></svg>";
|
|
116
|
+
readonly declined: "<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.2\" stroke-linecap=\"round\"><path d=\"M6 10h8\"/></svg>";
|
|
117
|
+
readonly missing: "<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"9\" cy=\"9\" r=\"4.5\"/><path d=\"M12.4 12.4L16 16\"/></svg>";
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* The 404 a browser gets for a path nothing serves, and everyone else's plain
|
|
121
|
+
* `Not Found`. The body is built from deployment configuration alone and names
|
|
122
|
+
* no path, so every unserved path — on the main host or an artifact host —
|
|
123
|
+
* answers with the same bytes for the same `Accept`: a 404 must not tell one
|
|
124
|
+
* route from another. The home link appears only when the operator UI is
|
|
125
|
+
* mounted to land on, and points at the public origin so an artifact host's
|
|
126
|
+
* 404 does not link to another 404.
|
|
127
|
+
*/
|
|
128
|
+
export declare function notFoundResponse(request: Request, opts: {
|
|
129
|
+
branding?: ConnectaBranding | undefined;
|
|
130
|
+
ui?: object | undefined;
|
|
131
|
+
publicUrl?: string | undefined;
|
|
132
|
+
}): Response;
|
|
46
133
|
/**
|
|
47
134
|
* Names of the branding URLs the operator set that failed their gate and were
|
|
48
135
|
* replaced by a default. Lives beside the gates so the startup warning cannot
|
package/dist/branding.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PAGE_CSS, TOKENS_CSS } from "./page-styles.js";
|
|
1
2
|
/** Connecta's default monochrome "C" mark. */
|
|
2
3
|
export const CONNECTA_FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
3
4
|
<style>
|
|
@@ -37,7 +38,7 @@ export function resolveBranding(branding) {
|
|
|
37
38
|
faviconHref: faviconHref && isSafeIconHref(faviconHref)
|
|
38
39
|
? faviconHref
|
|
39
40
|
: DEFAULT_FAVICON_HREF,
|
|
40
|
-
themeColor:
|
|
41
|
+
themeColor: configuredThemeColor(branding) ?? "#ffffff",
|
|
41
42
|
theme: resolveTheme(branding?.theme),
|
|
42
43
|
};
|
|
43
44
|
}
|
|
@@ -148,6 +149,174 @@ export function themeCss(theme) {
|
|
|
148
149
|
].filter(Boolean);
|
|
149
150
|
return declarations.length ? `:root{${declarations.join(";")}}` : "";
|
|
150
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* The resolved scheme as the root element's attribute. The dark palette keys
|
|
154
|
+
* off `html[data-scheme]`, so "system" leaves it off and the media query
|
|
155
|
+
* decides.
|
|
156
|
+
*/
|
|
157
|
+
export function schemeAttribute(theme) {
|
|
158
|
+
return theme.colorScheme === "system"
|
|
159
|
+
? ""
|
|
160
|
+
: ` data-scheme="${theme.colorScheme}"`;
|
|
161
|
+
}
|
|
162
|
+
/** Text or attribute value, escaped for any position in an HTML document. */
|
|
163
|
+
export function escapeHtml(value) {
|
|
164
|
+
return value
|
|
165
|
+
.replaceAll("&", "&")
|
|
166
|
+
.replaceAll('"', """)
|
|
167
|
+
.replaceAll("<", "<")
|
|
168
|
+
.replaceAll(">", ">");
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* The masthead's `--surface` in each palette, for the browser chrome a
|
|
172
|
+
* `theme-color` paints. A configured `themeColor` wins; without one a dark
|
|
173
|
+
* page should not sit under a white status bar.
|
|
174
|
+
*/
|
|
175
|
+
const SURFACE = { light: "#ffffff", dark: "#151a21" };
|
|
176
|
+
/** The operator's own `themeColor`, read the one way both callers read it. */
|
|
177
|
+
function configuredThemeColor(branding) {
|
|
178
|
+
return trimmedString(branding?.themeColor);
|
|
179
|
+
}
|
|
180
|
+
function themeColorMeta(branding, theme) {
|
|
181
|
+
const configured = configuredThemeColor(branding);
|
|
182
|
+
if (configured)
|
|
183
|
+
return `<meta name="theme-color" content="${escapeHtml(configured)}">`;
|
|
184
|
+
if (theme.colorScheme !== "system") {
|
|
185
|
+
return `<meta name="theme-color" content="${SURFACE[theme.colorScheme]}">`;
|
|
186
|
+
}
|
|
187
|
+
return `<meta name="theme-color" content="${SURFACE.light}" media="(prefers-color-scheme: light)">\n` +
|
|
188
|
+
`<meta name="theme-color" content="${SURFACE.dark}" media="(prefers-color-scheme: dark)">`;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* The one page layout every server-rendered page shares: the operator shell,
|
|
192
|
+
* the OAuth callback, and browser-facing 404s. Doctype and meta, the favicon a
|
|
193
|
+
* deployment actually serves, the resolved scheme, the token stylesheet with
|
|
194
|
+
* the theme block after it, and the masthead.
|
|
195
|
+
*
|
|
196
|
+
* `/favicon.svg` and `/favicon.ico` are operator UI routes. Without the UI they
|
|
197
|
+
* are 404s, so the default links are left out rather than pointing at nothing
|
|
198
|
+
* (a browser then asks for `/favicon.ico` on its own and gets the same 404). A
|
|
199
|
+
* configured `favicon.href` is the operator's own icon and is linked either way.
|
|
200
|
+
*
|
|
201
|
+
* Every interpolated branding value is escaped here; `body`, `head`,
|
|
202
|
+
* `mastheadEnd`, and `tail` are the caller's markup and are not.
|
|
203
|
+
*/
|
|
204
|
+
export function renderPage(branding, layout) {
|
|
205
|
+
const brand = resolveBranding(branding);
|
|
206
|
+
// Top-left corner. With an owner set it reads "<owner> <product>"; without
|
|
207
|
+
// one the product label stands alone. Either half links out when the
|
|
208
|
+
// matching URL is configured.
|
|
209
|
+
const label = (className, text, href) => href
|
|
210
|
+
? `<a class="${className} navlink" href="${escapeHtml(href)}">${escapeHtml(text)}</a>`
|
|
211
|
+
: `<span class="${className}">${escapeHtml(text)}</span>`;
|
|
212
|
+
const owner = brand.ownerName
|
|
213
|
+
? label("brand", brand.ownerName, brand.ownerUrl)
|
|
214
|
+
: label("brand", brand.productName, brand.productUrl);
|
|
215
|
+
const product = brand.ownerName
|
|
216
|
+
? label("product", brand.productName, brand.productUrl)
|
|
217
|
+
: "";
|
|
218
|
+
const customIcon = brand.faviconHref !== DEFAULT_FAVICON_HREF;
|
|
219
|
+
// `isSafeIconHref` already limited a relative href to one leading slash, so
|
|
220
|
+
// resolving it can only land on `iconOrigin` itself.
|
|
221
|
+
const iconHref = (href) => layout.iconOrigin && href.startsWith("/")
|
|
222
|
+
? new URL(href, layout.iconOrigin).toString()
|
|
223
|
+
: href;
|
|
224
|
+
const icons = [
|
|
225
|
+
...(customIcon || layout.uiMounted
|
|
226
|
+
? [`<link rel="icon" href="${escapeHtml(iconHref(brand.faviconHref))}" type="image/svg+xml">`]
|
|
227
|
+
: []),
|
|
228
|
+
...(layout.uiMounted
|
|
229
|
+
? [`<link rel="shortcut icon" href="${escapeHtml(iconHref("/favicon.ico"))}">`]
|
|
230
|
+
: []),
|
|
231
|
+
];
|
|
232
|
+
const skip = layout.skipTo
|
|
233
|
+
? `<a class="skip-link" href="#${escapeHtml(layout.skipTo.id)}">${escapeHtml(layout.skipTo.label)}</a>\n`
|
|
234
|
+
: "";
|
|
235
|
+
const styles = (layout.styles ?? TOKENS_CSS + PAGE_CSS) + (layout.extraStyles ?? "");
|
|
236
|
+
return `<!doctype html>
|
|
237
|
+
<html lang="en"${schemeAttribute(brand.theme)}>
|
|
238
|
+
<head>
|
|
239
|
+
<meta charset="utf-8">
|
|
240
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
241
|
+
${themeColorMeta(branding, brand.theme)}
|
|
242
|
+
<meta name="description" content="${escapeHtml(brand.description)}">
|
|
243
|
+
${icons.join("\n")}
|
|
244
|
+
<title>${escapeHtml(layout.title)}</title>
|
|
245
|
+
${layout.head ?? ""}
|
|
246
|
+
<style>${styles}${themeCss(brand.theme)}</style>
|
|
247
|
+
</head>
|
|
248
|
+
<body>
|
|
249
|
+
${skip}<header class="masthead shell">
|
|
250
|
+
<div class="masthead-inner">
|
|
251
|
+
<div class="mast-nav">
|
|
252
|
+
${owner}${product ? `\n ${product}` : ""}
|
|
253
|
+
</div>${layout.mastheadEnd ? `\n ${layout.mastheadEnd}` : ""}
|
|
254
|
+
</div>
|
|
255
|
+
</header>
|
|
256
|
+
${layout.body}${layout.tail ? `\n${layout.tail}` : ""}
|
|
257
|
+
</body>
|
|
258
|
+
</html>`;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Status marks for one-message pages. Decorative: the label and heading beside
|
|
262
|
+
* each carry the meaning, so the shape repeats it and color is never alone.
|
|
263
|
+
*/
|
|
264
|
+
export const STATUS_ICONS = {
|
|
265
|
+
ok: '<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 10.5l3.2 3.2L15 6.8"/></svg>',
|
|
266
|
+
problem: '<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M10 5.5v5.5"/><path d="M10 14.6v.1"/></svg>',
|
|
267
|
+
declined: '<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M6 10h8"/></svg>',
|
|
268
|
+
missing: '<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="9" cy="9" r="4.5"/><path d="M12.4 12.4L16 16"/></svg>',
|
|
269
|
+
};
|
|
270
|
+
/**
|
|
271
|
+
* A request a person's browser made for a page, as opposed to a client asking
|
|
272
|
+
* for data: navigations send `text/html` in `Accept`, `fetch()` and MCP
|
|
273
|
+
* clients do not.
|
|
274
|
+
*/
|
|
275
|
+
function wantsHtml(request) {
|
|
276
|
+
return /\btext\/html\b/i.test(request.headers.get("Accept") ?? "");
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* The 404 a browser gets for a path nothing serves, and everyone else's plain
|
|
280
|
+
* `Not Found`. The body is built from deployment configuration alone and names
|
|
281
|
+
* no path, so every unserved path — on the main host or an artifact host —
|
|
282
|
+
* answers with the same bytes for the same `Accept`: a 404 must not tell one
|
|
283
|
+
* route from another. The home link appears only when the operator UI is
|
|
284
|
+
* mounted to land on, and points at the public origin so an artifact host's
|
|
285
|
+
* 404 does not link to another 404.
|
|
286
|
+
*/
|
|
287
|
+
export function notFoundResponse(request, opts) {
|
|
288
|
+
if (!wantsHtml(request)) {
|
|
289
|
+
return new Response("Not Found", { status: 404, headers: { Vary: "Accept" } });
|
|
290
|
+
}
|
|
291
|
+
const brand = resolveBranding(opts.branding);
|
|
292
|
+
const uiMounted = Boolean(opts.ui);
|
|
293
|
+
const homeHref = opts.publicUrl ? new URL("/", opts.publicUrl).toString() : "/";
|
|
294
|
+
const home = uiMounted
|
|
295
|
+
? `<div class="status-actions"><a class="btn" href="${escapeHtml(homeHref)}">Go to ${escapeHtml(brand.productName)}</a></div>`
|
|
296
|
+
: "";
|
|
297
|
+
const body = `<main class="page shell">
|
|
298
|
+
<section class="status-page" data-not-found>
|
|
299
|
+
<div class="status-head">
|
|
300
|
+
<span class="status-mark" aria-hidden="true">${STATUS_ICONS.missing}</span>
|
|
301
|
+
<p class="status-label">404</p>
|
|
302
|
+
</div>
|
|
303
|
+
<h1>Page not found</h1>
|
|
304
|
+
<p class="status-copy">Nothing is served at this address. Check the link, or ask whoever runs ${escapeHtml(brand.productName)} for the right one.</p>
|
|
305
|
+
${home}
|
|
306
|
+
</section>
|
|
307
|
+
</main>`;
|
|
308
|
+
return new Response(renderPage(opts.branding, {
|
|
309
|
+
title: `Page not found — ${brand.pageTitle}`,
|
|
310
|
+
uiMounted,
|
|
311
|
+
// Same bytes on either host: the icons, like the home link, come from
|
|
312
|
+
// the public origin.
|
|
313
|
+
iconOrigin: opts.publicUrl,
|
|
314
|
+
body,
|
|
315
|
+
}), {
|
|
316
|
+
status: 404,
|
|
317
|
+
headers: { "Content-Type": "text/html; charset=utf-8", Vary: "Accept" },
|
|
318
|
+
});
|
|
319
|
+
}
|
|
151
320
|
/**
|
|
152
321
|
* Whether the operator meant to supply a value here — the question every
|
|
153
322
|
* dropped-value warning asks before naming a field, and one definition so the
|
package/dist/catalog-service.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Effect, Result } from "effect";
|
|
2
2
|
import { compactDiscoverySchema, compactDescriptionSchema, lexicalCorpusStatistics, lexicalQueryTerms, lexicalSearchQuery, matchesLexicalTerm, rankTools, schemaObjectKeys, summarizeDiscoveryDescription, summarizeDescription, typescriptSignature, } from "./catalog.js";
|
|
3
3
|
import { resolveDiscoveryConcurrency } from "./concurrency.js";
|
|
4
4
|
import { boundedEchoText, classifyCallError, framingError, } from "./errors.js";
|
|
5
5
|
import { connectorGuide, connectorGuideRequired, connectorGuideSummary, connectorSkillName, } from "./skills.js";
|
|
6
6
|
import { runEdge, withDeadlineEffect } from "./runtime/run.js";
|
|
7
|
+
import { SharedRead } from "./runtime/shared-read.js";
|
|
7
8
|
import { DEFAULT_PROBE_TIMEOUT_MS, normalizeTimeoutMs, } from "./timeout.js";
|
|
8
|
-
import { isExplicitlyReadOnly } from "./tool-safety.js";
|
|
9
|
+
import { isApprovalExempt, isExplicitlyReadOnly, NO_EXEMPTIONS, } from "./tool-safety.js";
|
|
9
10
|
export const DEFAULT_SEARCH_LIMIT = 8;
|
|
10
11
|
export const MAX_SEARCH_LIMIT = 100;
|
|
11
12
|
export const MAX_DESCRIBE_ADDRESSES = 100;
|
|
@@ -202,12 +203,13 @@ export class CatalogService {
|
|
|
202
203
|
searchRoute;
|
|
203
204
|
readOptions;
|
|
204
205
|
requestSignal;
|
|
205
|
-
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
//
|
|
206
|
+
approval;
|
|
207
|
+
// The request-scoped catalog cache: one shared read per connector asked
|
|
208
|
+
// about, started by the first asker and joined by every later one. A
|
|
209
|
+
// success stays for the rest of the request, so a search and the call after
|
|
210
|
+
// it see one catalog; a failure, or a read cancelled because every asker
|
|
211
|
+
// left it, is dropped so the next ask reads again. The service lives and
|
|
212
|
+
// dies with one request, so no read here is ever awaited from another.
|
|
211
213
|
//
|
|
212
214
|
// Registry.getTools also joins concurrent reads in one request scope, which
|
|
213
215
|
// the operator UI and status reads rely on. This map overlaps it only for
|
|
@@ -218,6 +220,7 @@ export class CatalogService {
|
|
|
218
220
|
constructor(registry, baseUrl, options = {}) {
|
|
219
221
|
this.registry = registry;
|
|
220
222
|
this.baseUrl = baseUrl;
|
|
223
|
+
this.approval = options.approval ?? NO_EXEMPTIONS;
|
|
221
224
|
this.requestScope = options.requestScope ?? {};
|
|
222
225
|
this.probeTimeoutMs =
|
|
223
226
|
normalizeTimeoutMs(options.probeTimeoutMs) ?? DEFAULT_PROBE_TIMEOUT_MS;
|
|
@@ -256,28 +259,27 @@ export class CatalogService {
|
|
|
256
259
|
return runEdge(this.catalog(id, callOptions));
|
|
257
260
|
}
|
|
258
261
|
// One connector's catalog from the request-scoped cache, reading it through
|
|
259
|
-
// the registry when this request has not yet.
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
//
|
|
262
|
+
// the registry when this request has not yet. No asker's options govern the
|
|
263
|
+
// read: it carries its own signal and the catalog deadline, the probe
|
|
264
|
+
// timeout, whichever asker starts it. Each asker waits under its own signal
|
|
265
|
+
// and deadline, so a short-deadline call that starts the read and a search
|
|
266
|
+
// that joins it each end on their own terms, and the read is cancelled only
|
|
267
|
+
// once every asker has gone (#571).
|
|
263
268
|
catalog(id, callOptions) {
|
|
264
269
|
return Effect.suspend(() => {
|
|
265
|
-
|
|
266
|
-
if (
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
Deferred.doneUnsafe(read, Exit.fail(cause));
|
|
279
|
-
});
|
|
280
|
-
return Deferred.await(read);
|
|
270
|
+
let read = this.catalogs.get(id);
|
|
271
|
+
if (!read) {
|
|
272
|
+
const started = new SharedRead((signal) => this.registry.getTools(id, this.baseUrl, this.requestScope, { signal, timeoutMs: this.probeTimeoutMs }, this.readOptions), (succeeded) => {
|
|
273
|
+
// Evicted before the read resumes anyone, so an asker that
|
|
274
|
+
// retries on hearing of the failure starts a fresh read.
|
|
275
|
+
if (!succeeded && this.catalogs.get(id) === started) {
|
|
276
|
+
this.catalogs.delete(id);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
this.catalogs.set(id, started);
|
|
280
|
+
read = started;
|
|
281
|
+
}
|
|
282
|
+
return read.join(callOptions.signal);
|
|
281
283
|
});
|
|
282
284
|
}
|
|
283
285
|
// Every named catalog under its own probe deadline, `concurrency` at a time,
|
|
@@ -286,7 +288,7 @@ export class CatalogService {
|
|
|
286
288
|
// A probe also ends with its request, so no listing outlives the caller
|
|
287
289
|
// that asked for it.
|
|
288
290
|
discoveryCatalogs(ids, route) {
|
|
289
|
-
return Effect.forEach(ids, (id) => Effect.result(withDeadlineEffect((signal) => this.catalog(id, { signal
|
|
291
|
+
return Effect.forEach(ids, (id) => Effect.result(withDeadlineEffect((signal) => this.catalog(id, { signal }), {
|
|
290
292
|
...(this.requestSignal ? { signal: this.requestSignal } : {}),
|
|
291
293
|
timeoutMs: this.probeTimeoutMs,
|
|
292
294
|
timeoutError: new Error(`${route} probe of "${id}" timed out after ${this.probeTimeoutMs}ms`),
|
|
@@ -536,6 +538,11 @@ export class CatalogService {
|
|
|
536
538
|
...(match.tool.annotations
|
|
537
539
|
? { annotations: match.tool.annotations }
|
|
538
540
|
: {}),
|
|
541
|
+
// Config lets a program call this write without pausing (#566).
|
|
542
|
+
// A marker, not a class: the tool stays approval-required.
|
|
543
|
+
...(isApprovalExempt(this.approval, match.connector, match.tool.name, match.tool)
|
|
544
|
+
? { approval: "exempt" }
|
|
545
|
+
: {}),
|
|
539
546
|
...(requiredReasons
|
|
540
547
|
? {
|
|
541
548
|
guideRequired: true,
|
|
@@ -847,6 +854,9 @@ export class CatalogService {
|
|
|
847
854
|
: {}),
|
|
848
855
|
...(output.source ? { outputSchemaSource: output.source } : {}),
|
|
849
856
|
...(tool.annotations ? { annotations: tool.annotations } : {}),
|
|
857
|
+
...(isApprovalExempt(this.approval, addressResolution.connector, tool.name, tool)
|
|
858
|
+
? { approval: "exempt" }
|
|
859
|
+
: {}),
|
|
850
860
|
};
|
|
851
861
|
});
|
|
852
862
|
}
|
package/dist/catalog.js
CHANGED
|
@@ -14,6 +14,12 @@ class SchemaWork {
|
|
|
14
14
|
remaining = MAX_SCHEMA_WORK;
|
|
15
15
|
truncated = false;
|
|
16
16
|
refs = new Map();
|
|
17
|
+
/**
|
|
18
|
+
* Renderings whose top level is a bare `|` or `&`, recorded as the walk
|
|
19
|
+
* produces them. Compact carries raw keys and `//` prose, so unlike the
|
|
20
|
+
* TypeScript dialect its text cannot be rescanned for a top-level operator.
|
|
21
|
+
*/
|
|
22
|
+
ungrouped = new Set();
|
|
17
23
|
constructor(byteLimit = MAX_COMPACT_DISCOVERY_SCHEMA_BYTES) {
|
|
18
24
|
this.byteLimit = byteLimit;
|
|
19
25
|
}
|
|
@@ -435,8 +441,11 @@ function renderEnum(values, work, byteLimit, onTruncated) {
|
|
|
435
441
|
const full = prefix.join(" | ");
|
|
436
442
|
if (schemaEncoder.encode(full).length > limit)
|
|
437
443
|
break;
|
|
438
|
-
if (index === values.length - 1)
|
|
444
|
+
if (index === values.length - 1) {
|
|
445
|
+
if (index > 0)
|
|
446
|
+
work.ungrouped.add(full);
|
|
439
447
|
return full;
|
|
448
|
+
}
|
|
440
449
|
const omitted = values.length - prefix.length;
|
|
441
450
|
const candidate = `(${full} | ${marker(omitted)})`;
|
|
442
451
|
if (schemaEncoder.encode(candidate).length <= limit)
|
|
@@ -582,6 +591,12 @@ function renderSchema(schema, defs, seen, depth, options) {
|
|
|
582
591
|
function renderSchemaNode(schema, defs, seen, depth, options) {
|
|
583
592
|
const ts = options.typescript === true;
|
|
584
593
|
const group = ts ? groupedType : grouped;
|
|
594
|
+
const ungrouped = options.work.ungrouped;
|
|
595
|
+
// A `[]` suffix binds tighter than `|` and `&`, so an element rendered at
|
|
596
|
+
// operator level needs parentheses or it reads as its last member's array.
|
|
597
|
+
const element = (rendered) => ts
|
|
598
|
+
? groupedType(rendered)
|
|
599
|
+
: ungrouped.has(rendered) ? `(${rendered})` : rendered;
|
|
585
600
|
if (depth > 4) {
|
|
586
601
|
if (!ts)
|
|
587
602
|
return "…";
|
|
@@ -607,9 +622,17 @@ function renderSchemaNode(schema, defs, seen, depth, options) {
|
|
|
607
622
|
: "unknown";
|
|
608
623
|
return rendered === "unknown" ? rendered : `${rendered} | null`;
|
|
609
624
|
}
|
|
610
|
-
const constrain = (rendered) =>
|
|
611
|
-
|
|
612
|
-
|
|
625
|
+
const constrain = (rendered) => {
|
|
626
|
+
if (!options.renderConstraints)
|
|
627
|
+
return rendered;
|
|
628
|
+
const result = renderConstraints(rendered, s, options.constraintByteLimit, options.onConstraintTruncated);
|
|
629
|
+
// Constraints parenthesize a union before their comment but leave an
|
|
630
|
+
// intersection bare, and a comment does not group what precedes it.
|
|
631
|
+
if (ungrouped.has(rendered) && !result.startsWith(`(${rendered})`)) {
|
|
632
|
+
ungrouped.add(result);
|
|
633
|
+
}
|
|
634
|
+
return result;
|
|
635
|
+
};
|
|
613
636
|
// Conditions cannot be expressed by a single static shape. Preserve the
|
|
614
637
|
// base and send callers to the exact schema instead of hiding the rules.
|
|
615
638
|
if (s.dependentSchemas !== undefined || s.if !== undefined ||
|
|
@@ -623,7 +646,10 @@ function renderSchemaNode(schema, defs, seen, depth, options) {
|
|
|
623
646
|
const rendered = declaresShape(base)
|
|
624
647
|
? renderSchema(base, defs, seen, depth, options)
|
|
625
648
|
: "unknown";
|
|
626
|
-
|
|
649
|
+
const result = `${rendered} /* conditional */`;
|
|
650
|
+
if (ungrouped.has(rendered))
|
|
651
|
+
ungrouped.add(result);
|
|
652
|
+
return result;
|
|
627
653
|
}
|
|
628
654
|
// allOf composes rather than replaces: it is checked before every other
|
|
629
655
|
// shape keyword, and renders the schema's own shape alongside its members instead
|
|
@@ -653,7 +679,10 @@ function renderSchemaNode(schema, defs, seen, depth, options) {
|
|
|
653
679
|
}
|
|
654
680
|
if (parts.length === 0)
|
|
655
681
|
return "unknown";
|
|
656
|
-
|
|
682
|
+
const rendered = parts.finish();
|
|
683
|
+
if (parts.length > 1)
|
|
684
|
+
ungrouped.add(rendered);
|
|
685
|
+
return rendered;
|
|
657
686
|
}
|
|
658
687
|
const reference = s.$ref ?? s.$dynamicRef;
|
|
659
688
|
if (typeof reference === "string") {
|
|
@@ -693,6 +722,8 @@ function renderSchemaNode(schema, defs, seen, depth, options) {
|
|
|
693
722
|
parts.add(renderSchema(member, defs, seen, depth + 1, options));
|
|
694
723
|
}
|
|
695
724
|
const rendered = parts.finish() || "unknown";
|
|
725
|
+
if (parts.length > 1)
|
|
726
|
+
ungrouped.add(rendered);
|
|
696
727
|
return constrain(rendered);
|
|
697
728
|
}
|
|
698
729
|
if (Array.isArray(s.enum)) {
|
|
@@ -743,7 +774,7 @@ function renderSchemaNode(schema, defs, seen, depth, options) {
|
|
|
743
774
|
const rest = s.items === undefined || s.items === true
|
|
744
775
|
? "unknown"
|
|
745
776
|
: renderSchema(s.items, defs, seen, depth + 1, options);
|
|
746
|
-
parts.add(`...${
|
|
777
|
+
parts.add(`...${element(rest)}[]`);
|
|
747
778
|
}
|
|
748
779
|
return parts.finish();
|
|
749
780
|
}
|
|
@@ -751,7 +782,7 @@ function renderSchemaNode(schema, defs, seen, depth, options) {
|
|
|
751
782
|
const items = s.items
|
|
752
783
|
? renderSchema(s.items, defs, seen, depth + 1, options)
|
|
753
784
|
: "unknown";
|
|
754
|
-
return
|
|
785
|
+
return `${element(items)}[]`;
|
|
755
786
|
}
|
|
756
787
|
if (type === "object" || s.properties) {
|
|
757
788
|
const props = (s.properties ?? {});
|
|
@@ -809,6 +840,8 @@ function renderSchemaNode(schema, defs, seen, depth, options) {
|
|
|
809
840
|
parts.add(options.work.text(String(item)));
|
|
810
841
|
}
|
|
811
842
|
const rendered = parts.finish();
|
|
843
|
+
if (parts.length > 1)
|
|
844
|
+
ungrouped.add(rendered);
|
|
812
845
|
return constrain(rendered);
|
|
813
846
|
}
|
|
814
847
|
if (options.renderConstraints && constraintEntries(s).length > 0) {
|
|
@@ -959,6 +992,7 @@ function boundedCompactSchema(schema, description, typescript = false) {
|
|
|
959
992
|
// are memoized only within each pass because their text includes constraints.
|
|
960
993
|
if (error === schemaSizeExceeded && !description) {
|
|
961
994
|
work.refs.clear();
|
|
995
|
+
work.ungrouped.clear();
|
|
962
996
|
try {
|
|
963
997
|
return {
|
|
964
998
|
text: renderSchema(schema, schema, new Set(), 0, {
|
|
@@ -9,20 +9,33 @@ export declare const POOL_NAME_RE: RegExp;
|
|
|
9
9
|
/**
|
|
10
10
|
* One derived view: which connectors, and for connectors granted by address
|
|
11
11
|
* only, which tools. A connector absent from `toolAccess` is visible whole.
|
|
12
|
+
* `guardedToolAccess` is a subset of `toolAccess` checked against the loaded
|
|
13
|
+
* catalog each time tools are discovered or invoked.
|
|
12
14
|
*/
|
|
13
15
|
export interface ConnectorAccess {
|
|
14
16
|
connectorIds: "all" | readonly string[];
|
|
15
17
|
toolAccess?: ToolAccess;
|
|
18
|
+
/** Exact tool grants that also require a current explicit read-only annotation. */
|
|
19
|
+
guardedToolAccess?: ToolAccess;
|
|
16
20
|
}
|
|
21
|
+
interface ReadOnlyToolGrant {
|
|
22
|
+
tool: string;
|
|
23
|
+
requireReadOnly: true;
|
|
24
|
+
}
|
|
25
|
+
export type ConnectorGrant = string | ReadOnlyToolGrant;
|
|
17
26
|
/**
|
|
18
27
|
* Normalize a grant list. A bare connector id grants every tool on that
|
|
19
28
|
* connector; a `connector.tool` address grants one tool. Grants are additive,
|
|
20
29
|
* so a bare id beside addresses for the same connector means the whole
|
|
21
|
-
* connector.
|
|
22
|
-
*
|
|
23
|
-
*
|
|
30
|
+
* connector. Identity resolution can opt into `{ tool, requireReadOnly: true }`
|
|
31
|
+
* for a fixed address that also needs a current explicit read-only annotation;
|
|
32
|
+
* pool declarations remain strings. Anything else throws, and the caller
|
|
33
|
+
* decides whether that is a construction failure or a 403: a grant that
|
|
34
|
+
* cannot be parsed must never fail open.
|
|
24
35
|
*/
|
|
25
|
-
export declare function parseConnectorAccess(value: unknown
|
|
36
|
+
export declare function parseConnectorAccess(value: unknown, options?: {
|
|
37
|
+
allowReadOnly?: boolean;
|
|
38
|
+
}): ConnectorAccess;
|
|
26
39
|
/**
|
|
27
40
|
* The view a pool endpoint serves: the pool's grants, never wider than the
|
|
28
41
|
* identity's own. A connector or tool outside either side is gone; a
|
|
@@ -30,3 +43,4 @@ export declare function parseConnectorAccess(value: unknown): ConnectorAccess;
|
|
|
30
43
|
* only narrow what the identity resolver already allowed.
|
|
31
44
|
*/
|
|
32
45
|
export declare function intersectAccess(ceiling: ConnectorAccess, pool: ConnectorAccess): ConnectorAccess;
|
|
46
|
+
export {};
|