@zackbart/connecta 0.15.1 → 0.16.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 +40 -20
- package/CHANGELOG.md +653 -0
- package/README.md +25 -5
- package/bin/connecta.mjs +44 -3
- package/dist/access-tokens.d.ts +0 -1
- package/dist/access-tokens.js +0 -1
- package/dist/activity.d.ts +50 -2
- package/dist/activity.js +34 -1
- package/dist/apps-shell.d.ts +0 -1
- package/dist/apps-shell.js +0 -1
- package/dist/auth/bearer.d.ts +0 -1
- package/dist/auth/bearer.js +0 -1
- package/dist/auth/clerk.d.ts +0 -1
- package/dist/auth/clerk.js +45 -9
- package/dist/auth/downstream-oauth.d.ts +0 -1
- package/dist/auth/downstream-oauth.js +0 -1
- package/dist/call-admission.d.ts +0 -1
- package/dist/call-admission.js +0 -1
- package/dist/catalog-drift.d.ts +99 -0
- package/dist/catalog-drift.js +251 -0
- package/dist/catalog-fingerprint.d.ts +0 -1
- package/dist/catalog-fingerprint.js +0 -1
- package/dist/catalog-limits.d.ts +0 -1
- package/dist/catalog-limits.js +0 -1
- package/dist/catalog-service.d.ts +0 -1
- package/dist/catalog-service.js +41 -5
- package/dist/catalog.d.ts +11 -1
- package/dist/catalog.js +20 -1
- package/dist/concurrency.d.ts +0 -1
- package/dist/concurrency.js +0 -1
- package/dist/connector-scope.d.ts +0 -1
- package/dist/connector-scope.js +0 -1
- package/dist/connectors/api.d.ts +29 -21
- package/dist/connectors/api.js +45 -19
- package/dist/connectors/guarded-fetch.d.ts +106 -0
- package/dist/connectors/guarded-fetch.js +289 -0
- package/dist/connectors/remote-mcp.d.ts +0 -1
- package/dist/connectors/remote-mcp.js +0 -1
- package/dist/credentials.d.ts +0 -1
- package/dist/credentials.js +0 -1
- package/dist/errors.d.ts +30 -3
- package/dist/errors.js +1 -1
- package/dist/execute.d.ts +0 -1
- package/dist/execute.js +0 -1
- package/dist/executor-admission.d.ts +11 -1
- package/dist/executor-admission.js +28 -1
- package/dist/executor-result.d.ts +0 -1
- package/dist/executor-result.js +0 -1
- package/dist/executors/quickjs-child.d.ts +0 -1
- package/dist/executors/quickjs-child.js +0 -1
- package/dist/executors/quickjs-protocol.d.ts +0 -1
- package/dist/executors/quickjs-protocol.js +0 -1
- package/dist/executors/quickjs-runtime.d.ts +0 -1
- package/dist/executors/quickjs-runtime.js +0 -1
- package/dist/executors/quickjs.d.ts +0 -1
- package/dist/executors/quickjs.js +2 -1
- package/dist/favicon.d.ts +0 -1
- package/dist/favicon.js +0 -1
- package/dist/index.d.ts +2 -3
- package/dist/index.js +24 -7
- package/dist/invocation.d.ts +0 -1
- package/dist/invocation.js +0 -1
- package/dist/json-schema.d.ts +0 -1
- package/dist/json-schema.js +0 -1
- package/dist/mcp-result.d.ts +0 -1
- package/dist/mcp-result.js +0 -1
- package/dist/meta-tools.d.ts +0 -1
- package/dist/meta-tools.js +0 -1
- package/dist/node.d.ts +0 -1
- package/dist/node.js +0 -1
- package/dist/operator-ui/generated.d.ts +2 -3
- package/dist/operator-ui/generated.js +3 -4
- package/dist/operator-ui/model.d.ts +10 -1
- package/dist/operator-ui/model.js +0 -1
- package/dist/operator-ui/view.d.ts +167 -0
- package/dist/operator-ui/view.js +270 -0
- package/dist/providers/cloudflare.d.ts +0 -1
- package/dist/providers/cloudflare.js +261 -299
- package/dist/providers/linear.d.ts +26 -5
- package/dist/providers/linear.js +32 -61
- package/dist/providers/mixpanel.d.ts +35 -4
- package/dist/providers/mixpanel.js +53 -79
- package/dist/providers/notion.d.ts +0 -1
- package/dist/providers/notion.js +87 -42
- package/dist/providers/stripe.d.ts +13 -1
- package/dist/providers/stripe.js +31 -60
- package/dist/registry.d.ts +34 -2
- package/dist/registry.js +85 -5
- package/dist/retired-toolkits.d.ts +0 -1
- package/dist/retired-toolkits.js +0 -1
- package/dist/routes/access-tokens.d.ts +0 -1
- package/dist/routes/access-tokens.js +0 -1
- package/dist/routes/activity.d.ts +0 -1
- package/dist/routes/activity.js +0 -1
- package/dist/routes/credentials.d.ts +0 -1
- package/dist/routes/credentials.js +0 -1
- package/dist/routes/mcp.d.ts +0 -1
- package/dist/routes/mcp.js +0 -1
- package/dist/routes/oauth.d.ts +0 -1
- package/dist/routes/oauth.js +0 -1
- package/dist/routes/shared.d.ts +2 -1
- package/dist/routes/shared.js +0 -1
- package/dist/routes/ui.d.ts +0 -1
- package/dist/routes/ui.js +0 -1
- package/dist/server.d.ts +0 -1
- package/dist/server.js +11 -1
- package/dist/skills.d.ts +0 -1
- package/dist/skills.js +0 -1
- package/dist/storage/file.d.ts +0 -1
- package/dist/storage/file.js +0 -1
- package/dist/storage/memory.d.ts +0 -1
- package/dist/storage/memory.js +0 -1
- package/dist/timeout.d.ts +0 -1
- package/dist/timeout.js +0 -1
- package/dist/tool-safety.d.ts +0 -1
- package/dist/tool-safety.js +0 -1
- package/dist/types.d.ts +45 -1
- package/dist/types.js +0 -1
- package/dist/ui.d.ts +0 -1
- package/dist/ui.js +13 -137
- package/dist/validate.d.ts +13 -15
- package/dist/validate.js +24 -11
- package/dist/version.d.ts +1 -2
- package/dist/version.js +1 -2
- package/documentation/architecture.md +186 -5
- package/documentation/auth.md +11 -0
- package/documentation/call-admission.md +142 -6
- package/documentation/cloudflare.md +92 -10
- package/documentation/code-first-exploration.md +2 -2
- package/documentation/code-mode.md +14 -14
- package/documentation/connector-guides.md +172 -5
- package/documentation/connectors.md +89 -0
- package/documentation/linear.md +20 -2
- package/documentation/mcp-ui-design.md +2 -2
- package/documentation/meta-tools.md +7 -1
- package/documentation/mixpanel.md +42 -9
- package/documentation/notion.md +23 -2
- package/documentation/operations.md +325 -5
- package/documentation/operator-ui.md +120 -5
- package/documentation/provider-audit.md +160 -0
- package/documentation/provider-conventions.md +667 -0
- package/documentation/request-admission.md +167 -5
- package/documentation/rich-output-design.md +1 -2
- package/documentation/stripe.md +15 -0
- package/documentation/upgrading.md +454 -0
- package/ethos.md +14 -9
- package/examples/worker/README.md +102 -12
- package/examples/worker/src/index.ts +46 -1
- package/examples/worker/wrangler.jsonc +14 -1
- package/package.json +10 -6
- package/templates/node/.dockerignore +10 -0
- package/templates/node/.env.example +40 -3
- package/templates/node/AGENTS.md +16 -1
- package/templates/node/Dockerfile +49 -0
- package/templates/node/README.md +117 -2
- package/templates/node/docker-compose.yml +39 -0
- package/templates/node/package.json +1 -1
- package/templates/node/src/file-activity.ts +182 -0
- package/templates/node/src/index.ts +80 -4
- package/assets/connecta-clay-hero.png +0 -0
- package/dist/access-tokens.d.ts.map +0 -1
- package/dist/access-tokens.js.map +0 -1
- package/dist/activity.d.ts.map +0 -1
- package/dist/activity.js.map +0 -1
- package/dist/apps-shell.d.ts.map +0 -1
- package/dist/apps-shell.js.map +0 -1
- package/dist/auth/bearer.d.ts.map +0 -1
- package/dist/auth/bearer.js.map +0 -1
- package/dist/auth/clerk.d.ts.map +0 -1
- package/dist/auth/clerk.js.map +0 -1
- package/dist/auth/downstream-oauth.d.ts.map +0 -1
- package/dist/auth/downstream-oauth.js.map +0 -1
- package/dist/call-admission.d.ts.map +0 -1
- package/dist/call-admission.js.map +0 -1
- package/dist/catalog-fingerprint.d.ts.map +0 -1
- package/dist/catalog-fingerprint.js.map +0 -1
- package/dist/catalog-limits.d.ts.map +0 -1
- package/dist/catalog-limits.js.map +0 -1
- package/dist/catalog-service.d.ts.map +0 -1
- package/dist/catalog-service.js.map +0 -1
- package/dist/catalog.d.ts.map +0 -1
- package/dist/catalog.js.map +0 -1
- package/dist/concurrency.d.ts.map +0 -1
- package/dist/concurrency.js.map +0 -1
- package/dist/connector-scope.d.ts.map +0 -1
- package/dist/connector-scope.js.map +0 -1
- package/dist/connectors/api.d.ts.map +0 -1
- package/dist/connectors/api.js.map +0 -1
- package/dist/connectors/remote-mcp.d.ts.map +0 -1
- package/dist/connectors/remote-mcp.js.map +0 -1
- package/dist/credentials.d.ts.map +0 -1
- package/dist/credentials.js.map +0 -1
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/execute.d.ts.map +0 -1
- package/dist/execute.js.map +0 -1
- package/dist/executor-admission.d.ts.map +0 -1
- package/dist/executor-admission.js.map +0 -1
- package/dist/executor-result.d.ts.map +0 -1
- package/dist/executor-result.js.map +0 -1
- package/dist/executors/quickjs-child.d.ts.map +0 -1
- package/dist/executors/quickjs-child.js.map +0 -1
- package/dist/executors/quickjs-protocol.d.ts.map +0 -1
- package/dist/executors/quickjs-protocol.js.map +0 -1
- package/dist/executors/quickjs-runtime.d.ts.map +0 -1
- package/dist/executors/quickjs-runtime.js.map +0 -1
- package/dist/executors/quickjs.d.ts.map +0 -1
- package/dist/executors/quickjs.js.map +0 -1
- package/dist/favicon.d.ts.map +0 -1
- package/dist/favicon.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/invocation.d.ts.map +0 -1
- package/dist/invocation.js.map +0 -1
- package/dist/json-schema.d.ts.map +0 -1
- package/dist/json-schema.js.map +0 -1
- package/dist/mcp-result.d.ts.map +0 -1
- package/dist/mcp-result.js.map +0 -1
- package/dist/meta-tools.d.ts.map +0 -1
- package/dist/meta-tools.js.map +0 -1
- package/dist/node.d.ts.map +0 -1
- package/dist/node.js.map +0 -1
- package/dist/operator-ui/generated.d.ts.map +0 -1
- package/dist/operator-ui/generated.js.map +0 -1
- package/dist/operator-ui/model.d.ts.map +0 -1
- package/dist/operator-ui/model.js.map +0 -1
- package/dist/providers/cloudflare.d.ts.map +0 -1
- package/dist/providers/cloudflare.js.map +0 -1
- package/dist/providers/linear.d.ts.map +0 -1
- package/dist/providers/linear.js.map +0 -1
- package/dist/providers/mixpanel.d.ts.map +0 -1
- package/dist/providers/mixpanel.js.map +0 -1
- package/dist/providers/notion.d.ts.map +0 -1
- package/dist/providers/notion.js.map +0 -1
- package/dist/providers/stripe.d.ts.map +0 -1
- package/dist/providers/stripe.js.map +0 -1
- package/dist/registry.d.ts.map +0 -1
- package/dist/registry.js.map +0 -1
- package/dist/retired-toolkits.d.ts.map +0 -1
- package/dist/retired-toolkits.js.map +0 -1
- package/dist/routes/access-tokens.d.ts.map +0 -1
- package/dist/routes/access-tokens.js.map +0 -1
- package/dist/routes/activity.d.ts.map +0 -1
- package/dist/routes/activity.js.map +0 -1
- package/dist/routes/credentials.d.ts.map +0 -1
- package/dist/routes/credentials.js.map +0 -1
- package/dist/routes/mcp.d.ts.map +0 -1
- package/dist/routes/mcp.js.map +0 -1
- package/dist/routes/oauth.d.ts.map +0 -1
- package/dist/routes/oauth.js.map +0 -1
- package/dist/routes/shared.d.ts.map +0 -1
- package/dist/routes/shared.js.map +0 -1
- package/dist/routes/ui.d.ts.map +0 -1
- package/dist/routes/ui.js.map +0 -1
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js.map +0 -1
- package/dist/skills.d.ts.map +0 -1
- package/dist/skills.js.map +0 -1
- package/dist/storage/file.d.ts.map +0 -1
- package/dist/storage/file.js.map +0 -1
- package/dist/storage/memory.d.ts.map +0 -1
- package/dist/storage/memory.js.map +0 -1
- package/dist/timeout.d.ts.map +0 -1
- package/dist/timeout.js.map +0 -1
- package/dist/tool-safety.d.ts.map +0 -1
- package/dist/tool-safety.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/ui.d.ts.map +0 -1
- package/dist/ui.js.map +0 -1
- package/dist/validate.d.ts.map +0 -1
- package/dist/validate.js.map +0 -1
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
- package/examples/node/README.md +0 -52
- package/examples/node/src/index.ts +0 -71
- package/src/access-tokens.ts +0 -289
- package/src/activity.ts +0 -234
- package/src/apps-shell.ts +0 -370
- package/src/auth/bearer.ts +0 -61
- package/src/auth/clerk.ts +0 -553
- package/src/auth/downstream-oauth.ts +0 -575
- package/src/call-admission.ts +0 -521
- package/src/catalog-fingerprint.ts +0 -52
- package/src/catalog-limits.ts +0 -11
- package/src/catalog-service.ts +0 -1153
- package/src/catalog.ts +0 -790
- package/src/concurrency.ts +0 -39
- package/src/connector-scope.ts +0 -66
- package/src/connectors/api.ts +0 -159
- package/src/connectors/remote-mcp.ts +0 -1090
- package/src/credentials.ts +0 -428
- package/src/errors.ts +0 -364
- package/src/execute.ts +0 -1406
- package/src/executor-admission.ts +0 -328
- package/src/executor-result.ts +0 -107
- package/src/executors/quickjs-child.ts +0 -172
- package/src/executors/quickjs-protocol.ts +0 -99
- package/src/executors/quickjs-runtime.ts +0 -470
- package/src/executors/quickjs.ts +0 -776
- package/src/favicon.ts +0 -53
- package/src/index.ts +0 -666
- package/src/invocation.ts +0 -655
- package/src/json-schema.ts +0 -11
- package/src/mcp-result.ts +0 -43
- package/src/meta-tools.ts +0 -1607
- package/src/node.ts +0 -206
- package/src/operator-ui/browser.css +0 -423
- package/src/operator-ui/browser.ts +0 -1281
- package/src/operator-ui/generated.ts +0 -4
- package/src/operator-ui/model.ts +0 -111
- package/src/providers/cloudflare.ts +0 -3999
- package/src/providers/linear.ts +0 -302
- package/src/providers/mixpanel.ts +0 -229
- package/src/providers/notion.ts +0 -1879
- package/src/providers/stripe.ts +0 -307
- package/src/registry.ts +0 -1085
- package/src/retired-toolkits.ts +0 -14
- package/src/routes/access-tokens.ts +0 -115
- package/src/routes/activity.ts +0 -215
- package/src/routes/credentials.ts +0 -254
- package/src/routes/mcp.ts +0 -429
- package/src/routes/oauth.ts +0 -322
- package/src/routes/shared.ts +0 -247
- package/src/routes/ui.ts +0 -152
- package/src/server.ts +0 -195
- package/src/skills.ts +0 -271
- package/src/storage/file.ts +0 -123
- package/src/storage/memory.ts +0 -44
- package/src/timeout.ts +0 -84
- package/src/tool-safety.ts +0 -15
- package/src/types.ts +0 -523
- package/src/ui.ts +0 -744
- package/src/validate.ts +0 -387
- package/src/version.ts +0 -7
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
// Web-API only, like the rest of the core: a manifest comparison that ran on
|
|
2
|
+
// Node but not on Workers would leave half the deployments unable to tell a
|
|
3
|
+
// stale allowlist from a current one.
|
|
4
|
+
import { boundedEchoText } from "./errors.js";
|
|
5
|
+
/**
|
|
6
|
+
* Byte budget for the one string a drift report carries. An ISO timestamp is
|
|
7
|
+
* 24 bytes; anything near this bound is a plugin sending something else.
|
|
8
|
+
*/
|
|
9
|
+
const MAX_OBSERVED_AT_BYTES = 64;
|
|
10
|
+
/** A count, or 0 when the seam returned something that is not one. */
|
|
11
|
+
function boundedCount(value) {
|
|
12
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
13
|
+
? Math.max(0, Math.trunc(value))
|
|
14
|
+
: 0;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Rebuild a drift report as four counts and a bounded timestamp.
|
|
18
|
+
*
|
|
19
|
+
* `Connector.catalogDrift()` sits on the open plugin seam, and what it returns
|
|
20
|
+
* lands in the body of unauthenticated `/health` and on connector status.
|
|
21
|
+
* TypeScript constrains neither an extra enumerable property nor the length of
|
|
22
|
+
* `observedAt` at runtime, so "counts and nothing else" is *made* true here —
|
|
23
|
+
* at the boundary where third-party output becomes a response — rather than
|
|
24
|
+
* trusted. The activity path reconstructs its five fields for the same reason.
|
|
25
|
+
*/
|
|
26
|
+
export function boundedCatalogDrift(report) {
|
|
27
|
+
if (!report || typeof report !== "object")
|
|
28
|
+
return undefined;
|
|
29
|
+
return {
|
|
30
|
+
observedAt: boundedEchoText(typeof report.observedAt === "string" ? report.observedAt : "", MAX_OBSERVED_AT_BYTES),
|
|
31
|
+
unclassifiedTools: boundedCount(report.unclassifiedTools),
|
|
32
|
+
unservedTools: boundedCount(report.unservedTools),
|
|
33
|
+
annotationConflicts: boundedCount(report.annotationConflicts),
|
|
34
|
+
schemaChanges: boundedCount(report.schemaChanges),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const encoder = new TextEncoder();
|
|
38
|
+
/** Deterministic JSON: object keys sorted, so key order is not a schema change. */
|
|
39
|
+
function canonicalize(value) {
|
|
40
|
+
if (Array.isArray(value))
|
|
41
|
+
return value.map(canonicalize);
|
|
42
|
+
if (value === null || typeof value !== "object")
|
|
43
|
+
return value;
|
|
44
|
+
const entries = Object.entries(value)
|
|
45
|
+
.filter(([, item]) => item !== undefined)
|
|
46
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
47
|
+
return Object.fromEntries(entries.map(([key, item]) => [key, canonicalize(item)]));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Digest the schemas of one downstream tool.
|
|
51
|
+
*
|
|
52
|
+
* Canonical rather than literal, unlike the catalog fingerprint in
|
|
53
|
+
* `src/catalog-fingerprint.ts`: that one is deliberately conservative because a
|
|
54
|
+
* spurious cache write is cheap, while a spurious drift finding spends a
|
|
55
|
+
* maintainer's attention on a downstream that reordered its JSON keys.
|
|
56
|
+
* Description and annotations are excluded — a reworded description is P1's
|
|
57
|
+
* business, and an annotation change is already its own drift category.
|
|
58
|
+
*/
|
|
59
|
+
export async function vettedSchemaDigest(tool) {
|
|
60
|
+
const bytes = encoder.encode(JSON.stringify(canonicalize({
|
|
61
|
+
inputSchema: tool.inputSchema ?? null,
|
|
62
|
+
outputSchema: tool.outputSchema ?? null,
|
|
63
|
+
})));
|
|
64
|
+
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
|
|
65
|
+
return `sha256:${[...digest]
|
|
66
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
67
|
+
.join("")}`;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Build a provider's manifest from the lists it already maintains.
|
|
71
|
+
*
|
|
72
|
+
* Throws when a name is classified twice, because a tool that is both a read
|
|
73
|
+
* and a write is a review mistake that must not boot: the annotation the
|
|
74
|
+
* connector would apply depends on which branch runs first, and "safe by
|
|
75
|
+
* default" cannot be decided by ordering.
|
|
76
|
+
*/
|
|
77
|
+
export function vettedCatalog(input) {
|
|
78
|
+
const tools = new Map();
|
|
79
|
+
const digestFor = (name) => input.schemaDigests?.[name];
|
|
80
|
+
for (const name of input.reads) {
|
|
81
|
+
const digest = digestFor(name);
|
|
82
|
+
tools.set(name, {
|
|
83
|
+
verdict: "read-only",
|
|
84
|
+
...(digest !== undefined ? { schemaDigest: digest } : {}),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
for (const [name, verdict] of input.writes) {
|
|
88
|
+
if (tools.has(name)) {
|
|
89
|
+
throw new Error(`vettedCatalog() classified "${name}" as both a read and a write.`);
|
|
90
|
+
}
|
|
91
|
+
const digest = digestFor(name);
|
|
92
|
+
tools.set(name, {
|
|
93
|
+
verdict,
|
|
94
|
+
...(digest !== undefined ? { schemaDigest: digest } : {}),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
for (const name of Object.keys(input.schemaDigests ?? {})) {
|
|
98
|
+
if (!tools.has(name)) {
|
|
99
|
+
throw new Error(`vettedCatalog() recorded a schema digest for unclassified tool "${name}".`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return { version: 1, tools };
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Fill in downstream silence; keep reviewed destructive tools fail-closed.
|
|
106
|
+
*
|
|
107
|
+
* Silence is what a vetted classification is for, and an explicit downstream
|
|
108
|
+
* annotation otherwise wins in both directions. `destructiveHint: true` or
|
|
109
|
+
* `readOnlyHint: false` on a classified read is the downstream telling us this
|
|
110
|
+
* release's allowlist is stale; `readOnlyHint: true` on a name no release has
|
|
111
|
+
* classified says the same thing from the other side. The single place a
|
|
112
|
+
* vetted verdict still overrides the downstream is a name this release
|
|
113
|
+
* reviewed and filed destructive: there connecta knows what the tool does, and
|
|
114
|
+
* a claim to the contrary is a downstream bug rather than news
|
|
115
|
+
* ([#310](https://github.com/zackbart/connecta/issues/310),
|
|
116
|
+
* [#315](https://github.com/zackbart/connecta/issues/315)).
|
|
117
|
+
*/
|
|
118
|
+
function applyVettedSafety(catalog, definition) {
|
|
119
|
+
const downstream = definition.annotations ?? {};
|
|
120
|
+
const record = catalog.tools.get(definition.name);
|
|
121
|
+
if (record?.verdict === "read-only") {
|
|
122
|
+
if (downstream.destructiveHint === true ||
|
|
123
|
+
downstream.readOnlyHint === false) {
|
|
124
|
+
return definition;
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
...definition,
|
|
128
|
+
annotations: {
|
|
129
|
+
...downstream,
|
|
130
|
+
readOnlyHint: true,
|
|
131
|
+
destructiveHint: downstream.destructiveHint ?? false,
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (record?.verdict === "destructive") {
|
|
136
|
+
return {
|
|
137
|
+
...definition,
|
|
138
|
+
annotations: {
|
|
139
|
+
...downstream,
|
|
140
|
+
readOnlyHint: false,
|
|
141
|
+
destructiveHint: true,
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
// Maintained additive creates and tools this release has never seen land
|
|
146
|
+
// here alike. Fill-in only: a silent tool is not read-only, so drift still
|
|
147
|
+
// fails closed onto `call_destructive_tool`, and neither population gets a
|
|
148
|
+
// `destructiveHint` it has not earned. A tool that arrives explicitly
|
|
149
|
+
// read-only keeps that annotation — on a name no release has reviewed, the
|
|
150
|
+
// downstream's own word is the only evidence there is, and rewriting it
|
|
151
|
+
// would be an overrule rather than a fill-in.
|
|
152
|
+
return {
|
|
153
|
+
...definition,
|
|
154
|
+
annotations: {
|
|
155
|
+
...downstream,
|
|
156
|
+
readOnlyHint: downstream.readOnlyHint ?? false,
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Whether the downstream's own annotation contradicts what a release reviewed.
|
|
162
|
+
*
|
|
163
|
+
* Only an *explicit* contradiction counts. Silence is the ordinary case the
|
|
164
|
+
* classification exists to fill, and an unclassified tool is already counted
|
|
165
|
+
* as an addition rather than twice.
|
|
166
|
+
*/
|
|
167
|
+
function contradicts(record, definition) {
|
|
168
|
+
const downstream = definition.annotations ?? {};
|
|
169
|
+
if (record.verdict === "read-only") {
|
|
170
|
+
return (downstream.readOnlyHint === false || downstream.destructiveHint === true);
|
|
171
|
+
}
|
|
172
|
+
return downstream.readOnlyHint === true;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Compare a live catalog with the manifest and count what moved.
|
|
176
|
+
*
|
|
177
|
+
* Counts only, and by construction: there is nowhere here to put a tool name,
|
|
178
|
+
* a schema, or a downstream string, so no later surface has to remember to
|
|
179
|
+
* strip one. Reads nothing but the tools it was handed — the caller already
|
|
180
|
+
* fetched them to serve a request, and this function never fetches anything.
|
|
181
|
+
*/
|
|
182
|
+
export async function detectCatalogDrift(catalog, tools) {
|
|
183
|
+
let unclassifiedTools = 0;
|
|
184
|
+
let annotationConflicts = 0;
|
|
185
|
+
let schemaChanges = 0;
|
|
186
|
+
const served = new Set();
|
|
187
|
+
for (const definition of tools) {
|
|
188
|
+
served.add(definition.name);
|
|
189
|
+
const record = catalog.tools.get(definition.name);
|
|
190
|
+
if (!record) {
|
|
191
|
+
unclassifiedTools += 1;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (contradicts(record, definition))
|
|
195
|
+
annotationConflicts += 1;
|
|
196
|
+
// A manifest that recorded no digest for this tool cannot have an opinion
|
|
197
|
+
// about its schema, so it does not pay for a hash either.
|
|
198
|
+
if (record.schemaDigest !== undefined &&
|
|
199
|
+
record.schemaDigest !== (await vettedSchemaDigest(definition))) {
|
|
200
|
+
schemaChanges += 1;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
let unservedTools = 0;
|
|
204
|
+
for (const name of catalog.tools.keys()) {
|
|
205
|
+
if (!served.has(name))
|
|
206
|
+
unservedTools += 1;
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
unclassifiedTools,
|
|
210
|
+
unservedTools,
|
|
211
|
+
annotationConflicts,
|
|
212
|
+
schemaChanges,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Wrap a hosted-MCP connector in its vetted manifest: the classification the
|
|
217
|
+
* catalog is normalized with, and the drift check that rides the same listing.
|
|
218
|
+
*
|
|
219
|
+
* The check happens where the tools are already in hand and still unmodified —
|
|
220
|
+
* after the downstream answered, before the classification is applied. It adds
|
|
221
|
+
* no request of its own, which is the whole boundary: connecta watches a
|
|
222
|
+
* contract while it is serving a refresh the deployment asked for, and never
|
|
223
|
+
* initiates one to go looking ([#179](https://github.com/zackbart/connecta/issues/179),
|
|
224
|
+
* [#343](https://github.com/zackbart/connecta/issues/343)).
|
|
225
|
+
*/
|
|
226
|
+
export function withVettedCatalog(connector, catalog) {
|
|
227
|
+
let observed;
|
|
228
|
+
return {
|
|
229
|
+
...connector,
|
|
230
|
+
async listTools(ctx) {
|
|
231
|
+
const downstream = await connector.listTools(ctx);
|
|
232
|
+
try {
|
|
233
|
+
observed = {
|
|
234
|
+
observedAt: new Date().toISOString(),
|
|
235
|
+
...(await detectCatalogDrift(catalog, downstream)),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
// A drift check is a report about a catalog, never a condition for
|
|
240
|
+
// serving one. Keep the last good observation rather than replacing it
|
|
241
|
+
// with a lie, and let the refresh through.
|
|
242
|
+
ctx.logger.warn(`[connecta] connector "${connector.id}" catalog drift check failed: ` +
|
|
243
|
+
(error instanceof Error ? error.message : String(error)));
|
|
244
|
+
}
|
|
245
|
+
return downstream.map((definition) => applyVettedSafety(catalog, definition));
|
|
246
|
+
},
|
|
247
|
+
catalogDrift() {
|
|
248
|
+
return observed;
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
}
|
|
@@ -17,4 +17,3 @@ export declare function fingerprintSerializedCatalog(serializedTools: string): P
|
|
|
17
17
|
* any serialized field must change the digest.
|
|
18
18
|
*/
|
|
19
19
|
export declare function snapshotCatalog(tools: ToolDef[]): Promise<CatalogSnapshot>;
|
|
20
|
-
//# sourceMappingURL=catalog-fingerprint.d.ts.map
|
package/dist/catalog-limits.d.ts
CHANGED
|
@@ -7,4 +7,3 @@ export declare const MAX_CATALOG_TOOLS = 100000;
|
|
|
7
7
|
export declare const MAX_SERIALIZED_CATALOG_BYTES: number;
|
|
8
8
|
/** Persisted values stay comfortably below platform per-value limits. */
|
|
9
9
|
export declare const MAX_CATALOG_CHUNK_BYTES: number;
|
|
10
|
-
//# sourceMappingURL=catalog-limits.d.ts.map
|
package/dist/catalog-limits.js
CHANGED
|
@@ -7,4 +7,3 @@ export const MAX_CATALOG_TOOLS = 100_000;
|
|
|
7
7
|
export const MAX_SERIALIZED_CATALOG_BYTES = 32 * 1024 * 1024;
|
|
8
8
|
/** Persisted values stay comfortably below platform per-value limits. */
|
|
9
9
|
export const MAX_CATALOG_CHUNK_BYTES = 1024 * 1024;
|
|
10
|
-
//# sourceMappingURL=catalog-limits.js.map
|
package/dist/catalog-service.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { compactDiscoverySchema, compactSchema, lexicalCorpusStatistics, lexicalQueryTerms, lexicalSearchQuery, rankTools, schemaObjectKeys, summarizeDiscoveryDescription, summarizeDescription, } from "./catalog.js";
|
|
1
|
+
import { compactDiscoverySchema, compactSchema, lexicalCorpusStatistics, lexicalQueryTerms, lexicalSearchQuery, matchesLexicalTerm, rankTools, schemaObjectKeys, summarizeDiscoveryDescription, summarizeDescription, } from "./catalog.js";
|
|
2
2
|
import { mapSettledWithConcurrency, resolveDiscoveryConcurrency, } from "./concurrency.js";
|
|
3
3
|
import { boundedEchoText, classifyCallError, framingError, } from "./errors.js";
|
|
4
4
|
import { connectorGuide, connectorGuideRequired, connectorGuideSummary, connectorSkillName, } from "./skills.js";
|
|
@@ -10,6 +10,12 @@ export const MAX_DESCRIBE_ADDRESSES = 100;
|
|
|
10
10
|
export const MAX_DISCOVERY_RESULT_BYTES = 256_000;
|
|
11
11
|
const MAX_QUERY_TERMS = 8;
|
|
12
12
|
const MAX_QUERY_TERM_LENGTH = 64;
|
|
13
|
+
/**
|
|
14
|
+
* Connector IDs a no-match search will name back. Three is enough to point at
|
|
15
|
+
* the connector the query already named without turning a miss into a listing
|
|
16
|
+
* of the deployment.
|
|
17
|
+
*/
|
|
18
|
+
const MAX_IDENTITY_CONNECTORS = 3;
|
|
13
19
|
const encoder = new TextEncoder();
|
|
14
20
|
/** Clip one echoed query term without splitting a non-BMP code point. */
|
|
15
21
|
function boundedQueryTerm(term) {
|
|
@@ -561,6 +567,36 @@ export class CatalogService {
|
|
|
561
567
|
required: connectorGuideRequired(scopedConnector),
|
|
562
568
|
}
|
|
563
569
|
: undefined;
|
|
570
|
+
// A connector's own id — the address prefix the caller already has — and
|
|
571
|
+
// the title it is displayed under are the most natural first query terms,
|
|
572
|
+
// and neither is a document in the lexical index. Indexing them would move
|
|
573
|
+
// ranking for every query that already matches tools, so instead a search
|
|
574
|
+
// that matched nothing asks the same lexical question of connector
|
|
575
|
+
// identity and corrects its own sentence: the deployment plainly has this
|
|
576
|
+
// capability, and one scoped browse away are its tools. Unscoped only —
|
|
577
|
+
// guidance for an explicit scope already names that connector rather than
|
|
578
|
+
// claiming the deployment has nothing.
|
|
579
|
+
const identityConnectorIds = matches.length === 0 && !isBrowse && !unsearchableQuery && !scopedConnector
|
|
580
|
+
? connectors
|
|
581
|
+
// The full query, not `analyzedTerms`: that cap exists to bound
|
|
582
|
+
// the serialized term fields, and ranking already reads every
|
|
583
|
+
// term. A ninth term is a real search term, and a search that
|
|
584
|
+
// ranked against it must not deny the connector it names.
|
|
585
|
+
.filter((connector) => queryTerms.some((term) => matchesLexicalTerm(connector.id, term) ||
|
|
586
|
+
(connector.title !== undefined &&
|
|
587
|
+
matchesLexicalTerm(connector.title, term))))
|
|
588
|
+
.map((connector) => connector.id)
|
|
589
|
+
: [];
|
|
590
|
+
const namedIdentityConnectors = identityConnectorIds
|
|
591
|
+
.slice(0, MAX_IDENTITY_CONNECTORS)
|
|
592
|
+
.map((id) => `"${id}"`)
|
|
593
|
+
.join(", ");
|
|
594
|
+
const unnamedIdentityConnectors = identityConnectorIds.length - MAX_IDENTITY_CONNECTORS;
|
|
595
|
+
const identityGuidance = identityConnectorIds.length === 0
|
|
596
|
+
? undefined
|
|
597
|
+
: `No matching ${safetyLabel}capability was found${unavailableCatalogs === 0 ? "" : " in the catalogs that answered"}, but the query names configured connector${identityConnectorIds.length === 1 ? "" : "s"} ${namedIdentityConnectors}${unnamedIdentityConnectors > 0
|
|
598
|
+
? ` and ${unnamedIdentityConnectors} more`
|
|
599
|
+
: ""}. Scope by connector and browse with an empty query to list the tools there.${filterRecovery}`;
|
|
564
600
|
// A scope that resolved to nothing is the same silence one step earlier in
|
|
565
601
|
// the lookup: no connector resolved, so no catalog was even attempted, so
|
|
566
602
|
// no catalog failed and the unavailable path below never fires. Echo only
|
|
@@ -604,9 +640,10 @@ export class CatalogService {
|
|
|
604
640
|
: scopedGuide?.required
|
|
605
641
|
? `No matching ${safetyLabel}capability was found on connector "${scopedConnector.id}". Fetch queryAnalysis.guide before calling, then refine terms or browse with an empty query.${filterRecovery}`
|
|
606
642
|
: `No matching ${safetyLabel}capability was found on connector "${scopedConnector.id}". Refine terms or browse it with an empty query.${filterRecovery}`
|
|
607
|
-
:
|
|
608
|
-
|
|
609
|
-
|
|
643
|
+
: (identityGuidance ??
|
|
644
|
+
(unavailableCatalogs === 0
|
|
645
|
+
? `No matching ${safetyLabel}capability is configured in this deployment. Refine terms, scope by connector, or browse with an empty query.${filterRecovery}`
|
|
646
|
+
: `No matching ${safetyLabel}capability was found in the catalogs that answered; ${unavailableCatalogs} connector catalog${unavailableCatalogs === 1 ? " was" : "s were"} unavailable. Refine terms, scope by connector, or browse with an empty query.${filterRecovery}`))))
|
|
610
647
|
: matchMode === "partial"
|
|
611
648
|
? scopedConnector
|
|
612
649
|
? `No single tool on connector "${scopedConnector.id}" matched every term. Split distinct intents into separate searches.`
|
|
@@ -786,4 +823,3 @@ export function flatSearchResult(page) {
|
|
|
786
823
|
...(page.queryAnalysis ? { queryAnalysis: page.queryAnalysis } : {}),
|
|
787
824
|
};
|
|
788
825
|
}
|
|
789
|
-
//# sourceMappingURL=catalog-service.js.map
|
package/dist/catalog.d.ts
CHANGED
|
@@ -18,6 +18,17 @@ export interface RankedTool {
|
|
|
18
18
|
exactName: boolean;
|
|
19
19
|
matchedTermCount: number;
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Whether one query term matches a whole token of arbitrary text, under the
|
|
23
|
+
* same inflection rules the tool index uses.
|
|
24
|
+
*
|
|
25
|
+
* Connector identity — an `id` or a `title` — is deliberately not a document in
|
|
26
|
+
* that index: making it one would move ranking for every query that already
|
|
27
|
+
* matches tools. This lets a caller ask the index's question of a string that
|
|
28
|
+
* never became a document, which is what the no-match analysis needs to tell
|
|
29
|
+
* "nothing like this exists here" from "that word is a connector".
|
|
30
|
+
*/
|
|
31
|
+
export declare function matchesLexicalTerm(text: string, term: string): boolean;
|
|
21
32
|
export interface LexicalCorpusStatistics {
|
|
22
33
|
documentCount: number;
|
|
23
34
|
documentFrequency: ReadonlyMap<string, number>;
|
|
@@ -72,4 +83,3 @@ export interface SchemaObjectKeys {
|
|
|
72
83
|
* fields.
|
|
73
84
|
*/
|
|
74
85
|
export declare function schemaObjectKeys(schema: JsonSchema | undefined): SchemaObjectKeys | undefined;
|
|
75
|
-
//# sourceMappingURL=catalog.d.ts.map
|
package/dist/catalog.js
CHANGED
|
@@ -166,6 +166,26 @@ function matchingTokenCandidates(term) {
|
|
|
166
166
|
}
|
|
167
167
|
return candidates;
|
|
168
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* Whether one query term matches a whole token of arbitrary text, under the
|
|
171
|
+
* same inflection rules the tool index uses.
|
|
172
|
+
*
|
|
173
|
+
* Connector identity — an `id` or a `title` — is deliberately not a document in
|
|
174
|
+
* that index: making it one would move ranking for every query that already
|
|
175
|
+
* matches tools. This lets a caller ask the index's question of a string that
|
|
176
|
+
* never became a document, which is what the no-match analysis needs to tell
|
|
177
|
+
* "nothing like this exists here" from "that word is a connector".
|
|
178
|
+
*/
|
|
179
|
+
export function matchesLexicalTerm(text, term) {
|
|
180
|
+
const tokens = new Set(lexicalTokens(text));
|
|
181
|
+
if (tokens.size === 0)
|
|
182
|
+
return false;
|
|
183
|
+
for (const candidate of matchingTokenCandidates(term)) {
|
|
184
|
+
if (tokens.has(candidate))
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
169
189
|
/**
|
|
170
190
|
* Compute query-specific document frequencies across every available catalog.
|
|
171
191
|
* The caller does this once per search and shares the result with each
|
|
@@ -636,4 +656,3 @@ function objectKeys(schema, defs, seen, depth) {
|
|
|
636
656
|
}
|
|
637
657
|
return undefined;
|
|
638
658
|
}
|
|
639
|
-
//# sourceMappingURL=catalog.js.map
|
package/dist/concurrency.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
export declare function resolveDiscoveryConcurrency(value: number | undefined): number;
|
|
2
2
|
/** Run `fn` over `items` with at most `limit` operations in flight. */
|
|
3
3
|
export declare function mapSettledWithConcurrency<T, R>(items: readonly T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<PromiseSettledResult<R>[]>;
|
|
4
|
-
//# sourceMappingURL=concurrency.d.ts.map
|
package/dist/concurrency.js
CHANGED
|
@@ -13,4 +13,3 @@ export type DeferredWork = (promise: Promise<unknown>) => void;
|
|
|
13
13
|
* again after this returns.
|
|
14
14
|
*/
|
|
15
15
|
export declare function closeConnectorScope(connector: Connector, ctx: ConnectorContext, defer?: DeferredWork): Promise<void>;
|
|
16
|
-
//# sourceMappingURL=connector-scope.d.ts.map
|
package/dist/connector-scope.js
CHANGED
package/dist/connectors/api.d.ts
CHANGED
|
@@ -1,16 +1,30 @@
|
|
|
1
1
|
import type { Connector, ConnectorCallAdmissionPolicy, ConnectorCredentialConfig, ConnectorCredentialValues, ConnectorContext, ConnectorUsageGuide, CredentialTestResult, JsonSchema, ToolAnnotations } from "../types.js";
|
|
2
2
|
export interface ApiTool {
|
|
3
3
|
name: string;
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Required, non-empty. Discovery has nothing else to go on: a nameless
|
|
6
|
+
* capability costs the agent a guess, and a guess costs a wrong call.
|
|
7
|
+
*/
|
|
8
|
+
description: string;
|
|
9
|
+
/**
|
|
10
|
+
* A plain JSON Schema object describing the tool input. Optional, but what
|
|
11
|
+
* you supply must be a schema the validator can compile — `api()` refuses to
|
|
12
|
+
* construct otherwise.
|
|
13
|
+
*/
|
|
6
14
|
inputSchema?: JsonSchema;
|
|
7
15
|
/** A plain JSON Schema object describing the tool's structured output. */
|
|
8
16
|
outputSchema?: JsonSchema;
|
|
9
17
|
/**
|
|
10
|
-
* Standard MCP-style behavior hints
|
|
11
|
-
* admits the tool to call_tool and
|
|
18
|
+
* Standard MCP-style behavior hints, with an explicit `readOnlyHint`
|
|
19
|
+
* required: `true` declares a read and admits the tool to call_tool and
|
|
20
|
+
* execute_code, `false` declares work that must cross
|
|
21
|
+
* `call_destructive_tool` where a host can ask a human. Connecta never
|
|
22
|
+
* infers the classification from a name, a description, a schema, or the
|
|
23
|
+
* other annotations.
|
|
12
24
|
*/
|
|
13
|
-
annotations
|
|
25
|
+
annotations: ToolAnnotations & {
|
|
26
|
+
readOnlyHint: boolean;
|
|
27
|
+
};
|
|
14
28
|
handler: (args: any, ctx: ConnectorContext) => Promise<unknown> | unknown;
|
|
15
29
|
}
|
|
16
30
|
export interface ApiOptions {
|
|
@@ -47,16 +61,6 @@ export interface ApiOptions {
|
|
|
47
61
|
* on loose coercion.
|
|
48
62
|
*/
|
|
49
63
|
validateArgs?: boolean;
|
|
50
|
-
/**
|
|
51
|
-
* Fail-closed on a tool whose `inputSchema` the validator cannot evaluate
|
|
52
|
-
* (default false). The default surfaces such a schema as a one-time warning
|
|
53
|
-
* and then passes the raw arguments through, so a broken schema never breaks
|
|
54
|
-
* an otherwise working tool. Set true to instead reject those calls with a
|
|
55
|
-
* non-retryable `invalid_args` ConnectorCallError, so a schema that cannot be
|
|
56
|
-
* enforced never silently admits unvalidated input. Only consulted when
|
|
57
|
-
* `validateArgs` is not false.
|
|
58
|
-
*/
|
|
59
|
-
strictValidation?: boolean;
|
|
60
64
|
tools: ApiTool[];
|
|
61
65
|
}
|
|
62
66
|
/**
|
|
@@ -64,11 +68,15 @@ export interface ApiOptions {
|
|
|
64
68
|
* Tool inputs are plain JSON Schema objects (bring your own zod-to-json-schema
|
|
65
69
|
* conversion if you prefer zod). call_tool JSON-wraps the handler's return.
|
|
66
70
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
71
|
+
* Every tool declares a description and an explicit `annotations.readOnlyHint`,
|
|
72
|
+
* and any `inputSchema` it carries must compile — a tool that fails the
|
|
73
|
+
* contract throws here rather than reaching a catalog. Arguments are then
|
|
74
|
+
* validated against `inputSchema` before the handler runs (disable with
|
|
75
|
+
* `validateArgs: false`, which opts out of enforcement, not out of the schema
|
|
76
|
+
* being real), and a schema that only reveals itself as unenforceable on first
|
|
77
|
+
* use — an unresolvable `$ref`, say — fails the call rather than passing raw
|
|
78
|
+
* arguments through. Remote MCP inputs are also validated, but in the shared
|
|
79
|
+
* invocation path against the request-local downstream catalog, where a
|
|
80
|
+
* downstream's schema is its own affair and stays fail-open.
|
|
72
81
|
*/
|
|
73
82
|
export declare function api(id: string, opts: ApiOptions): Connector;
|
|
74
|
-
//# sourceMappingURL=api.d.ts.map
|
package/dist/connectors/api.js
CHANGED
|
@@ -1,34 +1,58 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { compileValidator, validateToolInput } from "../validate.js";
|
|
2
|
+
/**
|
|
3
|
+
* Enforce the construction contract for one hand-written tool.
|
|
4
|
+
*
|
|
5
|
+
* Everything here is something only the author can supply and no runtime can
|
|
6
|
+
* guess: what the tool does, and whether calling it needs a human's blessing.
|
|
7
|
+
* Guessing either one is how a deployment boots into the wrong shape, so this
|
|
8
|
+
* throws instead. Note what it does *not* do — it never reads a name, verb, or
|
|
9
|
+
* HTTP method to infer a safety class. An unclassified tool is a bug in the
|
|
10
|
+
* deployment, not a puzzle for connecta to solve.
|
|
11
|
+
*/
|
|
12
|
+
function checkToolContract(id, tool) {
|
|
13
|
+
const address = `${id}.${tool.name}`;
|
|
14
|
+
if (typeof tool.description !== "string" || tool.description.trim() === "") {
|
|
15
|
+
throw new Error(`api() tool "${address}" needs a non-empty description — it is what an ` +
|
|
16
|
+
"agent reads to choose the tool (convention: imperative one-liner, " +
|
|
17
|
+
'e.g. "Send an email via Resend").');
|
|
18
|
+
}
|
|
19
|
+
if (typeof tool.annotations?.readOnlyHint !== "boolean") {
|
|
20
|
+
throw new Error(`api() tool "${address}" needs an explicit annotations.readOnlyHint: ` +
|
|
21
|
+
"true for a read, false for work that must cross " +
|
|
22
|
+
"call_destructive_tool. Connecta never infers the classification " +
|
|
23
|
+
"from a tool name, description, schema, or other annotations.");
|
|
24
|
+
}
|
|
25
|
+
if (tool.inputSchema)
|
|
26
|
+
compileValidator(tool.inputSchema, { address });
|
|
27
|
+
}
|
|
2
28
|
/**
|
|
3
29
|
* A connector defined entirely in code: static tool defs + fetch handlers.
|
|
4
30
|
* Tool inputs are plain JSON Schema objects (bring your own zod-to-json-schema
|
|
5
31
|
* conversion if you prefer zod). call_tool JSON-wraps the handler's return.
|
|
6
32
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
33
|
+
* Every tool declares a description and an explicit `annotations.readOnlyHint`,
|
|
34
|
+
* and any `inputSchema` it carries must compile — a tool that fails the
|
|
35
|
+
* contract throws here rather than reaching a catalog. Arguments are then
|
|
36
|
+
* validated against `inputSchema` before the handler runs (disable with
|
|
37
|
+
* `validateArgs: false`, which opts out of enforcement, not out of the schema
|
|
38
|
+
* being real), and a schema that only reveals itself as unenforceable on first
|
|
39
|
+
* use — an unresolvable `$ref`, say — fails the call rather than passing raw
|
|
40
|
+
* arguments through. Remote MCP inputs are also validated, but in the shared
|
|
41
|
+
* invocation path against the request-local downstream catalog, where a
|
|
42
|
+
* downstream's schema is its own affair and stays fail-open.
|
|
12
43
|
*/
|
|
13
44
|
export function api(id, opts) {
|
|
45
|
+
for (const t of opts.tools)
|
|
46
|
+
checkToolContract(id, t);
|
|
14
47
|
const defs = opts.tools.map((t) => ({
|
|
15
48
|
name: t.name,
|
|
16
|
-
|
|
49
|
+
description: t.description,
|
|
17
50
|
...(t.inputSchema !== undefined ? { inputSchema: t.inputSchema } : {}),
|
|
18
51
|
...(t.outputSchema !== undefined ? { outputSchema: t.outputSchema } : {}),
|
|
19
|
-
|
|
52
|
+
annotations: t.annotations,
|
|
20
53
|
}));
|
|
21
54
|
const byName = new Map(opts.tools.map((t) => [t.name, t]));
|
|
22
55
|
const validateArgs = opts.validateArgs ?? true;
|
|
23
|
-
const strictValidation = opts.strictValidation ?? false;
|
|
24
|
-
if (validateArgs) {
|
|
25
|
-
// Compile each schema now so a validator-hostile inputSchema surfaces once
|
|
26
|
-
// here rather than silently on its first call. Warning-only; never throws.
|
|
27
|
-
for (const t of opts.tools) {
|
|
28
|
-
if (t.inputSchema)
|
|
29
|
-
precompileValidator(t.inputSchema, { address: `${id}.${t.name}` });
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
56
|
return {
|
|
33
57
|
id,
|
|
34
58
|
...(opts.title !== undefined ? { title: opts.title } : {}),
|
|
@@ -64,7 +88,10 @@ export function api(id, opts) {
|
|
|
64
88
|
const invalid = validateToolInput(tool.inputSchema, input, {
|
|
65
89
|
address: `${id}.${name}`,
|
|
66
90
|
logger: ctx.logger,
|
|
67
|
-
|
|
91
|
+
// Always: the schema compiled at construction, so anything that
|
|
92
|
+
// fails here is a schema that cannot be enforced, and a surface we
|
|
93
|
+
// wrote ourselves does not get to admit unvalidated input quietly.
|
|
94
|
+
failClosed: true,
|
|
68
95
|
});
|
|
69
96
|
if (invalid)
|
|
70
97
|
throw invalid;
|
|
@@ -77,4 +104,3 @@ export function api(id, opts) {
|
|
|
77
104
|
},
|
|
78
105
|
};
|
|
79
106
|
}
|
|
80
|
-
//# sourceMappingURL=api.js.map
|