@zackbart/connecta 0.4.0 → 0.5.0
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 +252 -0
- package/README.md +72 -17
- package/SECURITY.md +10 -6
- package/dist/activity.d.ts +8 -0
- package/dist/activity.d.ts.map +1 -1
- package/dist/activity.js +1 -0
- package/dist/activity.js.map +1 -1
- package/dist/connectors/api.d.ts +23 -0
- package/dist/connectors/api.d.ts.map +1 -1
- package/dist/connectors/api.js +13 -1
- package/dist/connectors/api.js.map +1 -1
- package/dist/connectors/remote-mcp.d.ts +27 -1
- package/dist/connectors/remote-mcp.d.ts.map +1 -1
- package/dist/connectors/remote-mcp.js +31 -0
- package/dist/connectors/remote-mcp.js.map +1 -1
- package/dist/credentials.d.ts +2 -1
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +4 -2
- package/dist/credentials.js.map +1 -1
- package/dist/execute.d.ts +4 -4
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js.map +1 -1
- package/dist/executors/quickjs.d.ts.map +1 -1
- package/dist/executors/quickjs.js +32 -4
- package/dist/executors/quickjs.js.map +1 -1
- package/dist/index.d.ts +51 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +67 -1
- package/dist/index.js.map +1 -1
- package/dist/meta-tools.d.ts +25 -4
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +138 -24
- package/dist/meta-tools.js.map +1 -1
- package/dist/registry.d.ts +183 -2
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +293 -27
- package/dist/registry.js.map +1 -1
- package/dist/server.d.ts +9 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +96 -12
- package/dist/server.js.map +1 -1
- package/dist/skills.d.ts +52 -1
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +161 -1
- package/dist/skills.js.map +1 -1
- package/dist/storage/file.d.ts.map +1 -1
- package/dist/storage/file.js +19 -3
- package/dist/storage/file.js.map +1 -1
- package/dist/toolkits.d.ts +44 -0
- package/dist/toolkits.d.ts.map +1 -0
- package/dist/toolkits.js +134 -0
- package/dist/toolkits.js.map +1 -0
- package/dist/types.d.ts +20 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/ui.d.ts +29 -1
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +100 -15
- package/dist/ui.js.map +1 -1
- package/dist/validate.d.ts +33 -1
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +32 -2
- package/dist/validate.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -2
- package/src/activity.ts +9 -0
- package/src/connectors/api.ts +36 -1
- package/src/connectors/remote-mcp.ts +67 -0
- package/src/credentials.ts +6 -3
- package/src/execute.ts +4 -4
- package/src/executors/quickjs.ts +32 -4
- package/src/index.ts +141 -2
- package/src/meta-tools.ts +215 -40
- package/src/registry.ts +416 -29
- package/src/server.ts +130 -12
- package/src/skills.ts +184 -1
- package/src/storage/file.ts +18 -2
- package/src/toolkits.ts +215 -0
- package/src/types.ts +20 -1
- package/src/ui.ts +103 -14
- package/src/validate.ts +60 -2
- package/src/version.ts +1 -1
package/src/types.ts
CHANGED
|
@@ -135,6 +135,23 @@ export interface Connector {
|
|
|
135
135
|
/** How call_tool wraps results. "mcp" passes the content array through; anything else is JSON-wrapped. */
|
|
136
136
|
kind?: "mcp" | "api";
|
|
137
137
|
description?: string;
|
|
138
|
+
/**
|
|
139
|
+
* Max inline result size (bytes) for this connector's tools before
|
|
140
|
+
* call_tool/batch_call truncate and stash the full text for get_result
|
|
141
|
+
* paging. Overrides the deployment-wide `ConnectaConfig.maxResultBytes`;
|
|
142
|
+
* omit to inherit it (which itself defaults to 50_000). Must be a whole
|
|
143
|
+
* number of bytes >= 1; anything else warns at startup and is ignored, so
|
|
144
|
+
* the connector inherits the deployment-wide cap.
|
|
145
|
+
*/
|
|
146
|
+
maxResultBytes?: number;
|
|
147
|
+
/**
|
|
148
|
+
* Optional agent-facing usage guide (markdown) for this connector — preferred
|
|
149
|
+
* tools, address quirks, pagination conventions, rate-limit etiquette, good
|
|
150
|
+
* query patterns. Listed by the `skills` meta-tool as `connector:<id>` and
|
|
151
|
+
* returned verbatim by `skills({ name: "connector:<id>" })`. Keep it concise
|
|
152
|
+
* and imperative; it is read by agents, not operators.
|
|
153
|
+
*/
|
|
154
|
+
usageGuide?: string;
|
|
138
155
|
/** Optional operator-managed credential slot rendered inside this connector's /ui card. */
|
|
139
156
|
credential?: ConnectorCredentialConfig;
|
|
140
157
|
/** Optional server-side check used by /ui's Test action. */
|
|
@@ -262,7 +279,9 @@ export interface ConnectaBranding {
|
|
|
262
279
|
* `/favicon.svg`, `ico` at `/favicon.ico`; omit either to keep the default
|
|
263
280
|
* for that format. Use `href` instead to point the page at an icon you host
|
|
264
281
|
* elsewhere (it replaces the `/favicon.svg` link in the page head; the
|
|
265
|
-
* `/favicon.*` routes still serve whatever `svg`/`ico` provide).
|
|
282
|
+
* `/favicon.*` routes still serve whatever `svg`/`ico` provide). `href` must
|
|
283
|
+
* be an absolute `http(s)` URL or a root-relative path; anything else falls
|
|
284
|
+
* back to the default mark.
|
|
266
285
|
*/
|
|
267
286
|
favicon?: {
|
|
268
287
|
svg?: string;
|
package/src/ui.ts
CHANGED
|
@@ -24,31 +24,74 @@ interface ResolvedBranding {
|
|
|
24
24
|
themeColor: string;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
export const DEFAULT_FAVICON_HREF = "/favicon.svg";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Branding arrives from operator config, which is untyped at a JS call site, so
|
|
31
|
+
* every field is treated as `unknown`: a non-string is read as unset rather than
|
|
32
|
+
* throwing on `.trim()`. Rendering must degrade to defaults for a malformed
|
|
33
|
+
* value, never fail — `createConnecta` calls this during construction.
|
|
34
|
+
*/
|
|
35
|
+
function trimmedString(value: unknown): string | undefined {
|
|
36
|
+
return typeof value === "string" ? value.trim() || undefined : undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
27
39
|
export function resolveBranding(
|
|
28
40
|
branding?: ConnectaBranding,
|
|
29
41
|
): ResolvedBranding {
|
|
30
|
-
const productName = branding?.productName
|
|
31
|
-
const ownerName = branding?.ownerName
|
|
42
|
+
const productName = trimmedString(branding?.productName) ?? "Connecta";
|
|
43
|
+
const ownerName = trimmedString(branding?.ownerName);
|
|
44
|
+
// Operator branding URLs become masthead/callback hrefs, so a non-http(s)
|
|
45
|
+
// scheme (javascript:, data:) is dropped the same as an unset URL — the
|
|
46
|
+
// callers already render a <span> instead of an <a> when it is absent.
|
|
47
|
+
const productUrl = trimmedString(branding?.productUrl);
|
|
48
|
+
const ownerUrl = trimmedString(branding?.ownerUrl);
|
|
49
|
+
const faviconHref = trimmedString(branding?.favicon?.href);
|
|
32
50
|
return {
|
|
33
51
|
productName,
|
|
34
|
-
...(
|
|
35
|
-
? { productUrl: branding.productUrl.trim() }
|
|
36
|
-
: {}),
|
|
52
|
+
...(productUrl && isSafeHttpUrl(productUrl) ? { productUrl } : {}),
|
|
37
53
|
...(ownerName ? { ownerName } : {}),
|
|
38
|
-
...(
|
|
39
|
-
? { ownerUrl: branding.ownerUrl.trim() }
|
|
40
|
-
: {}),
|
|
54
|
+
...(ownerUrl && isSafeHttpUrl(ownerUrl) ? { ownerUrl } : {}),
|
|
41
55
|
description:
|
|
42
|
-
branding?.description
|
|
56
|
+
trimmedString(branding?.description) ??
|
|
43
57
|
`Manage the services this ${productName} instance makes available to agents.`,
|
|
44
58
|
pageTitle:
|
|
45
|
-
branding?.pageTitle
|
|
59
|
+
trimmedString(branding?.pageTitle) ??
|
|
46
60
|
(ownerName ? `${productName} — ${ownerName}` : productName),
|
|
47
|
-
faviconHref:
|
|
48
|
-
|
|
61
|
+
faviconHref:
|
|
62
|
+
faviconHref && isSafeIconHref(faviconHref)
|
|
63
|
+
? faviconHref
|
|
64
|
+
: DEFAULT_FAVICON_HREF,
|
|
65
|
+
themeColor: trimmedString(branding?.themeColor) ?? "#ffffff",
|
|
49
66
|
};
|
|
50
67
|
}
|
|
51
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Names of the branding URLs the operator set that failed their gate and were
|
|
71
|
+
* replaced by a default. Lives beside the gates so the startup warning cannot
|
|
72
|
+
* drift from them, and takes `unknown` fields for the same reason
|
|
73
|
+
* `resolveBranding` does — a warning helper must never throw.
|
|
74
|
+
*/
|
|
75
|
+
export function droppedBrandingUrls(branding?: ConnectaBranding): string[] {
|
|
76
|
+
if (!branding) return [];
|
|
77
|
+
const resolved = resolveBranding(branding);
|
|
78
|
+
// A non-string still counts as "set": the operator meant to supply a URL, and
|
|
79
|
+
// that intent is exactly what the warning reports on. A blank string does not.
|
|
80
|
+
const isSet = (value: unknown) =>
|
|
81
|
+
typeof value === "string"
|
|
82
|
+
? trimmedString(value) !== undefined
|
|
83
|
+
: value !== undefined && value !== null;
|
|
84
|
+
const faviconHref = branding.favicon?.href;
|
|
85
|
+
return [
|
|
86
|
+
...(isSet(branding.productUrl) && !resolved.productUrl ? ["productUrl"] : []),
|
|
87
|
+
...(isSet(branding.ownerUrl) && !resolved.ownerUrl ? ["ownerUrl"] : []),
|
|
88
|
+
...(isSet(faviconHref) &&
|
|
89
|
+
trimmedString(faviconHref) !== resolved.faviconHref
|
|
90
|
+
? ["favicon.href"]
|
|
91
|
+
: []),
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
|
|
52
95
|
/**
|
|
53
96
|
* A JS string literal safe to inline in a <script> block. JSON.stringify alone
|
|
54
97
|
* leaves `/` untouched, so a value containing "</script>" would close the
|
|
@@ -73,6 +116,48 @@ export function isSafeHttpUrl(url: unknown): boolean {
|
|
|
73
116
|
}
|
|
74
117
|
}
|
|
75
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Only the second check's base; any origin works because the check is whether
|
|
121
|
+
* the href stays on whatever origin it is resolved against. It is deliberately
|
|
122
|
+
* never the sole gate: a value whose own authority equals this host (say
|
|
123
|
+
* `//connecta.invalid/x`) would resolve to this exact origin and pass, so the
|
|
124
|
+
* structural check below runs first and is what actually rejects `//host`.
|
|
125
|
+
*/
|
|
126
|
+
const SAME_ORIGIN_PROBE = "https://connecta.invalid";
|
|
127
|
+
|
|
128
|
+
/** Removed anywhere in a URL by the parser, so a gate must ignore them too. */
|
|
129
|
+
const URL_STRIPPED_CHARS = /[\t\n\r]/g;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* True for values allowed in the page's `<link rel="icon" href>`: an absolute
|
|
133
|
+
* `http(s)` URL (an icon the operator hosts elsewhere) or a path rooted at this
|
|
134
|
+
* origin. The relative carve-out is deliberate rather than accidental — the
|
|
135
|
+
* default href is the relative `/favicon.svg`, which `isSafeHttpUrl` alone would
|
|
136
|
+
* reject — and it is kept narrow on both ends.
|
|
137
|
+
*
|
|
138
|
+
* Root-relative only, because `/ui` and `/oauth/callback/<id>` sit at different
|
|
139
|
+
* depths and a document-relative path would resolve differently on each.
|
|
140
|
+
*
|
|
141
|
+
* "Root-relative" is enforced structurally: exactly one leading `/` followed by
|
|
142
|
+
* a character that is neither `/` nor `\`. Both of those would make the value an
|
|
143
|
+
* authority (`//host`, and `/\host` because the URL parser folds `\` to `/` in
|
|
144
|
+
* special schemes), pointing at an origin this server does not control. The test
|
|
145
|
+
* runs on a copy with tab/newline/CR removed, since the parser strips those
|
|
146
|
+
* anywhere and `/\t/host` would otherwise slip through as single-slash. The
|
|
147
|
+
* origin comparison that follows is defense in depth, not the authority check —
|
|
148
|
+
* on its own it would accept an authority that happened to equal the probe host.
|
|
149
|
+
*/
|
|
150
|
+
export function isSafeIconHref(href: unknown): boolean {
|
|
151
|
+
if (typeof href !== "string") return false;
|
|
152
|
+
if (isSafeHttpUrl(href)) return true;
|
|
153
|
+
if (!/^\/(?![/\\])/.test(href.replace(URL_STRIPPED_CHARS, ""))) return false;
|
|
154
|
+
try {
|
|
155
|
+
return new URL(href, SAME_ORIGIN_PROBE).origin === SAME_ORIGIN_PROBE;
|
|
156
|
+
} catch {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
76
161
|
export interface UiTool {
|
|
77
162
|
name: string;
|
|
78
163
|
address: string;
|
|
@@ -302,10 +387,14 @@ export function renderUiHtml(
|
|
|
302
387
|
uiAuth?: UiAuthConfig,
|
|
303
388
|
mcpUrl = "/mcp",
|
|
304
389
|
branding?: ConnectaBranding,
|
|
390
|
+
nonce?: string,
|
|
305
391
|
): string {
|
|
306
392
|
const auth = uiAuth ?? { kind: "bearer" as const };
|
|
307
393
|
const brand = resolveBranding(branding);
|
|
308
394
|
const title = brand.pageTitle;
|
|
395
|
+
// When the /ui response ships a nonce-based CSP, every <script> it emits must
|
|
396
|
+
// carry that nonce to run; without a nonce the markup is unchanged.
|
|
397
|
+
const nonceAttr = nonce ? ` nonce="${nonce}"` : "";
|
|
309
398
|
// Top-left corner. With an owner set it reads "<owner> <product>"; without
|
|
310
399
|
// one the product label stands alone. Either half links out when the
|
|
311
400
|
// matching URL is configured.
|
|
@@ -323,7 +412,7 @@ export function renderUiHtml(
|
|
|
323
412
|
: "";
|
|
324
413
|
const clerkScript =
|
|
325
414
|
uiAuth?.kind === "clerk"
|
|
326
|
-
? `<script defer crossorigin="anonymous" data-clerk-publishable-key="${escapeHtmlAttr(uiAuth.publishableKey)}" src="${escapeHtmlAttr(uiAuth.frontendApiUrl)}/npm/@clerk/clerk-js@6/dist/clerk.browser.js"></script>`
|
|
415
|
+
? `<script${nonceAttr} defer crossorigin="anonymous" data-clerk-publishable-key="${escapeHtmlAttr(uiAuth.publishableKey)}" src="${escapeHtmlAttr(uiAuth.frontendApiUrl)}/npm/@clerk/clerk-js@6/dist/clerk.browser.js"></script>`
|
|
327
416
|
: "";
|
|
328
417
|
|
|
329
418
|
return `<!doctype html>
|
|
@@ -683,7 +772,7 @@ ${clerkScript}
|
|
|
683
772
|
</section>
|
|
684
773
|
</main>
|
|
685
774
|
|
|
686
|
-
<script>
|
|
775
|
+
<script${nonceAttr}>
|
|
687
776
|
const AUTH = ${jsonForInlineScript(auth)};
|
|
688
777
|
const MCP_URL = ${jsonForInlineScript(mcpUrl)};
|
|
689
778
|
const filterUiConnectors = ${filterUiConnectors.toString()};
|
package/src/validate.ts
CHANGED
|
@@ -13,6 +13,28 @@ export interface ValidateToolInputOptions {
|
|
|
13
13
|
* unusable. Default console.
|
|
14
14
|
*/
|
|
15
15
|
logger?: Logger;
|
|
16
|
+
/**
|
|
17
|
+
* Fail-closed on a schema the validator cannot evaluate (default false =
|
|
18
|
+
* today's fail-open behavior). When true, a schema that cannot be compiled —
|
|
19
|
+
* or that only fails on first use, e.g. an unresolvable `$ref` — yields a
|
|
20
|
+
* non-retryable `invalid_args` error instead of passing the raw arguments
|
|
21
|
+
* through, so unvalidated input is never silently admitted. The happy path
|
|
22
|
+
* (a schema that compiles and validates) is unaffected.
|
|
23
|
+
*/
|
|
24
|
+
failClosed?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface PrecompileValidatorOptions {
|
|
28
|
+
/**
|
|
29
|
+
* Tool address used in the warning text, conventionally
|
|
30
|
+
* `"connectorId.toolName"`.
|
|
31
|
+
*/
|
|
32
|
+
address: string;
|
|
33
|
+
/**
|
|
34
|
+
* Destination for the warning emitted when the schema cannot be compiled.
|
|
35
|
+
* Default console.
|
|
36
|
+
*/
|
|
37
|
+
logger?: Logger;
|
|
16
38
|
}
|
|
17
39
|
|
|
18
40
|
// Lazy validator cache keyed by the schema object itself; null marks a schema
|
|
@@ -21,6 +43,13 @@ export interface ValidateToolInputOptions {
|
|
|
21
43
|
// connector are collectable, the same pattern compactSchema uses.
|
|
22
44
|
const validators = new WeakMap<JsonSchema, Validator | null>();
|
|
23
45
|
|
|
46
|
+
function unevaluableSchema(address: string): ConnectorCallError {
|
|
47
|
+
return new ConnectorCallError(
|
|
48
|
+
"invalid_args",
|
|
49
|
+
`Cannot validate arguments for "${address}": its inputSchema could not be evaluated`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
24
53
|
function disableValidation(
|
|
25
54
|
schema: JsonSchema,
|
|
26
55
|
address: string,
|
|
@@ -47,7 +76,9 @@ function disableValidation(
|
|
|
47
76
|
*
|
|
48
77
|
* A schema the validator cannot compile (or that only fails on first use, e.g.
|
|
49
78
|
* an unresolvable `$ref`) is warned about once and then passed through — a
|
|
50
|
-
* broken schema should not break an otherwise working tool.
|
|
79
|
+
* broken schema should not break an otherwise working tool. Pass
|
|
80
|
+
* `failClosed: true` to instead reject such calls with `invalid_args`, for
|
|
81
|
+
* callers that would rather refuse a call than forward unvalidated arguments.
|
|
51
82
|
*
|
|
52
83
|
* The compiled validator is cached by **schema object identity**, so pass a
|
|
53
84
|
* stable object: hold the parsed manifest and hand the same schema back on
|
|
@@ -74,12 +105,18 @@ export function validateToolInput(
|
|
|
74
105
|
validator = null;
|
|
75
106
|
}
|
|
76
107
|
}
|
|
108
|
+
// A schema the validator could not compile (or that a prior call disabled):
|
|
109
|
+
// pass through by default, refuse when the caller opted into fail-closed.
|
|
110
|
+
if (validator === null) {
|
|
111
|
+
return opts.failClosed ? unevaluableSchema(opts.address) : null;
|
|
112
|
+
}
|
|
77
113
|
let result;
|
|
78
114
|
try {
|
|
79
|
-
result = validator
|
|
115
|
+
result = validator.validate(args);
|
|
80
116
|
} catch (err) {
|
|
81
117
|
// e.g. an unresolvable $ref — surfaces on first validate, not compile.
|
|
82
118
|
disableValidation(schema, opts.address, logger, err);
|
|
119
|
+
return opts.failClosed ? unevaluableSchema(opts.address) : null;
|
|
83
120
|
}
|
|
84
121
|
if (result && !result.valid) {
|
|
85
122
|
const units = result.errors.filter((u) => u.instanceLocation !== "#");
|
|
@@ -94,3 +131,24 @@ export function validateToolInput(
|
|
|
94
131
|
}
|
|
95
132
|
return null;
|
|
96
133
|
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Eagerly compile and cache a tool's inputSchema so a schema the validator
|
|
137
|
+
* cannot use surfaces once at connector construction rather than silently on
|
|
138
|
+
* the first call. Reuses the same module-level cache `validateToolInput` reads,
|
|
139
|
+
* so the runtime path hits the cache. Warning-only: it never throws and never
|
|
140
|
+
* changes call behavior. A schema that only fails on first `validate()` (e.g.
|
|
141
|
+
* an unresolvable `$ref`) still slips through here and is caught at call time.
|
|
142
|
+
*/
|
|
143
|
+
export function precompileValidator(
|
|
144
|
+
schema: JsonSchema,
|
|
145
|
+
opts: PrecompileValidatorOptions,
|
|
146
|
+
): void {
|
|
147
|
+
if (validators.has(schema)) return;
|
|
148
|
+
const logger = opts.logger ?? console;
|
|
149
|
+
try {
|
|
150
|
+
validators.set(schema, new Validator(schema as never, "2020-12", false));
|
|
151
|
+
} catch (err) {
|
|
152
|
+
disableValidation(schema, opts.address, logger, err);
|
|
153
|
+
}
|
|
154
|
+
}
|
package/src/version.ts
CHANGED