@zackbart/connecta 0.33.0 → 0.34.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/CHANGELOG.md +65 -0
- package/README.md +100 -128
- package/SECURITY.md +1 -1
- package/bin/version.mjs +1 -1
- package/dist/activity.d.ts +3 -0
- package/dist/branding.d.ts +7 -0
- package/dist/branding.js +76 -9
- package/dist/catalog-drift.d.ts +30 -26
- package/dist/catalog-drift.js +169 -84
- package/dist/catalog-service.js +16 -2
- package/dist/config-value-sources.js +6 -1
- package/dist/config.d.ts +13 -2
- package/dist/config.js +36 -2
- package/dist/connector-caller.js +2 -1
- package/dist/connectors/api-connector.d.ts +3 -0
- package/dist/connectors/api-connector.js +5 -1
- package/dist/connectors/option-shapes.js +1 -1
- package/dist/connectors/remote-mcp.d.ts +2 -0
- package/dist/connectors/remote-mcp.js +5 -1
- package/dist/describe-config.d.ts +5 -1
- package/dist/describe-config.js +2 -0
- package/dist/execute.js +1 -11
- package/dist/fix-prompt.js +2 -2
- package/dist/index.js +1 -0
- package/dist/invocation.js +3 -2
- package/dist/membership.d.ts +2 -0
- package/dist/meta-tools.js +3 -2
- package/dist/oauth-callback-outcome.js +2 -2
- package/dist/operator-ui/agents.d.ts +1 -1
- package/dist/operator-ui/agents.js +16 -16
- package/dist/operator-ui/config-summary.js +565 -0
- package/dist/operator-ui/connection-map.js +376 -0
- package/dist/operator-ui/endpoints.js +45 -16
- package/dist/operator-ui/fix-prompts.js +1 -1
- package/dist/operator-ui/generated.js +4 -4
- package/dist/operator-ui/health.js +52 -0
- package/dist/operator-ui/member-page.js +13 -28
- package/dist/operator-ui/overview.js +123 -0
- package/dist/operator-ui/view.d.ts +26 -22
- package/dist/operator-ui/view.js +130 -96
- package/dist/page-styles.js +5 -3
- package/dist/provider.d.ts +6 -1
- package/dist/provider.js +18 -1
- package/dist/providers/_shared/google/workspace.d.ts +9 -4
- package/dist/providers/_shared/google/workspace.js +7 -3
- package/dist/providers/basecamp/index.d.ts +2 -0
- package/dist/providers/breeze/index.d.ts +2 -0
- package/dist/providers/ccb/index.d.ts +2 -0
- package/dist/providers/ccb/index.js +2 -2
- package/dist/providers/cloudflare/index.d.ts +2 -0
- package/dist/providers/cloudflare/index.js +5 -1
- package/dist/providers/cloudflare/openapi.generated.js +3296 -3206
- package/dist/providers/cloudflare/rest.js +19 -0
- package/dist/providers/cloudflare/skill.generated.js +2 -2
- package/dist/providers/cloudflare/value-safety.js +51 -18
- package/dist/providers/docs/index.js +1 -1
- package/dist/providers/docs/skill.generated.js +2 -2
- package/dist/providers/drive/index.js +1 -1
- package/dist/providers/drive/skill.generated.js +2 -2
- package/dist/providers/forms/index.js +1 -1
- package/dist/providers/forms/skill.generated.js +2 -2
- package/dist/providers/gmail/index.js +1 -1
- package/dist/providers/gmail/skill.generated.js +2 -2
- package/dist/providers/intercom/index.d.ts +24 -0
- package/dist/providers/intercom/index.js +121 -0
- package/dist/providers/intercom/skill.generated.js +8 -0
- package/dist/providers/linear/index.js +204 -19
- package/dist/providers/linear/skill.generated.js +2 -2
- package/dist/providers/mixpanel/index.d.ts +2 -0
- package/dist/providers/mixpanel/index.js +21 -17
- package/dist/providers/mixpanel/skill.generated.js +3 -3
- package/dist/providers/notion/index.d.ts +2 -0
- package/dist/providers/notion/index.js +1 -1
- package/dist/providers/notion/skill.generated.js +1 -1
- package/dist/providers/overflow/index.d.ts +2 -0
- package/dist/providers/overflow/index.js +1 -1
- package/dist/providers/overflow/skill.generated.js +2 -2
- package/dist/providers/planning-center/index.d.ts +2 -0
- package/dist/providers/planning-center/index.js +1 -1
- package/dist/providers/planning-center/skill.generated.js +4 -4
- package/dist/providers/revenuecat/index.d.ts +5 -3
- package/dist/providers/revenuecat/index.js +147 -7
- package/dist/providers/revenuecat/skill.generated.js +3 -3
- package/dist/providers/sheets/index.js +1 -1
- package/dist/providers/sheets/skill.generated.js +2 -2
- package/dist/providers/slides/index.js +1 -1
- package/dist/providers/slides/skill.generated.js +2 -2
- package/dist/providers/stripe/index.d.ts +2 -0
- package/dist/providers/stripe/index.js +50 -30
- package/dist/providers/stripe/skill.generated.js +2 -1
- package/dist/providers/tithely/index.d.ts +2 -0
- package/dist/providers/tithely/index.js +1 -1
- package/dist/providers/vercel/index.d.ts +2 -0
- package/dist/providers/vercel/index.js +2 -2
- package/dist/providers/vercel/openapi.generated.js +391 -374
- package/dist/providers/vercel/skill.generated.js +2 -2
- package/dist/providers/vercel/value-safety.js +14 -4
- package/dist/registry.d.ts +10 -14
- package/dist/registry.js +77 -62
- package/dist/routes/mcp.js +20 -13
- package/dist/routes/oauth.js +2 -2
- package/dist/routes/ui-data.js +8 -2
- package/dist/skills.js +18 -2
- package/dist/storage/sql.js +5 -1
- package/dist/types.d.ts +36 -11
- package/dist/ui.js +9 -4
- package/dist/usage-guide.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/README.md +2 -2
- package/documentation/architecture.md +66 -18
- package/documentation/auth.md +183 -67
- package/documentation/code-mode.md +30 -10
- package/documentation/deploying.md +5 -1
- package/documentation/integrating.md +64 -7
- package/documentation/meta-tools.md +38 -24
- package/documentation/migration-0.33.md +21 -13
- package/documentation/migration-0.34.md +67 -0
- package/documentation/operating.md +1 -1
- package/documentation/operator-tests.md +52 -20
- package/documentation/operator-ui.md +341 -104
- package/documentation/provider-auth-migration.md +22 -0
- package/examples/worker/AGENTS.md +15 -9
- package/examples/worker/README.md +41 -14
- package/examples/worker/src/connecta.config.ts +5 -4
- package/examples/worker/wrangler.jsonc +1 -1
- package/package.json +23 -19
- package/templates/node/AGENTS.md +13 -7
- package/templates/node/README.md +14 -11
- package/templates/node/package.json +1 -1
- package/templates/node/src/connecta.config.ts +2 -2
package/dist/catalog-drift.d.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import type { CatalogDriftCounts, CatalogDriftReport, Connector, Logger, ToolClassification, ToolDef } from "./types.js";
|
|
2
2
|
/**
|
|
3
|
-
* Rebuild a drift report as bounded counts and a
|
|
3
|
+
* Rebuild a drift report as bounded counts, closed verification facts, and a timestamp.
|
|
4
4
|
*
|
|
5
5
|
* `Connector.catalogDrift()` sits on the open plugin seam, and what it returns
|
|
6
|
-
* lands
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* and a time and nothing else" is *made* true here — at the boundary where
|
|
6
|
+
* lands on authenticated connector status and the operator page. TypeScript
|
|
7
|
+
* constrains neither extra enumerable properties nor `observedAt` at runtime.
|
|
8
|
+
* Counts, closed verification facts, and a time are rebuilt at the boundary where
|
|
10
9
|
* third-party output becomes a response — rather than trusted. A report whose
|
|
11
10
|
* `observedAt` names no instant is no observation connecta can date, and is
|
|
12
11
|
* dropped whole (INV-6): a plugin forwarding a downstream's report could
|
|
@@ -29,11 +28,12 @@ interface VettedToolRecord {
|
|
|
29
28
|
* Digest of the input and output schemas that release read, or undefined
|
|
30
29
|
* when no release has recorded them. Undefined is not "unchanged": a
|
|
31
30
|
* manifest with no digest cannot report a schema change, and says so by
|
|
32
|
-
* counting none. The
|
|
33
|
-
*
|
|
31
|
+
* counting none and reporting the unpinned coverage. The provider check never
|
|
32
|
+
* creates or updates schema digests; live `tools/list` remains the schema
|
|
34
33
|
* agents receive ([#351](https://github.com/zackbart/connecta/issues/351)).
|
|
35
34
|
*/
|
|
36
35
|
schemaDigest?: string;
|
|
36
|
+
optional?: boolean;
|
|
37
37
|
}
|
|
38
38
|
/**
|
|
39
39
|
* The vetted manifest one hosted-MCP proxy ships: every tool name a release
|
|
@@ -59,6 +59,8 @@ export interface VettedCatalogInput {
|
|
|
59
59
|
* read them; an invented digest reports drift that never happened.
|
|
60
60
|
*/
|
|
61
61
|
schemaDigests?: Readonly<Record<string, string>>;
|
|
62
|
+
/** Reviewed names that may be absent under plan/permission gating. */
|
|
63
|
+
optionalTools?: ReadonlySet<string>;
|
|
62
64
|
}
|
|
63
65
|
/**
|
|
64
66
|
* Digest the schemas of one downstream tool.
|
|
@@ -67,13 +69,17 @@ export interface VettedCatalogInput {
|
|
|
67
69
|
* `src/catalog-fingerprint.ts`: that one is deliberately conservative because a
|
|
68
70
|
* spurious cache write is cheap, while a spurious drift finding spends a
|
|
69
71
|
* maintainer's attention on a downstream that reordered its JSON keys.
|
|
72
|
+
* New pins use `sha256-sets-v1:` and sort required, enum, and type arrays only
|
|
73
|
+
* at schema keyword locations. Tuple arrays and JSON data keep their order.
|
|
74
|
+
* Legacy `sha256:` pins use the original literal-array form, so an upgrade
|
|
75
|
+
* cannot silently invalidate a reviewed provider manifest.
|
|
70
76
|
* Description and annotations are excluded — a reworded description is P1's
|
|
71
77
|
* business, and an annotation change is already its own drift category.
|
|
72
78
|
*
|
|
73
79
|
* Rejects rather than digesting part of a schema: a digest that ignored some
|
|
74
80
|
* leaf would let a reviewed read keep its verdict after that leaf changed.
|
|
75
81
|
*/
|
|
76
|
-
export declare function vettedSchemaDigest(tool: ToolDef): Promise<string>;
|
|
82
|
+
export declare function vettedSchemaDigest(tool: ToolDef, format?: "sets-v1" | "legacy"): Promise<string>;
|
|
77
83
|
/**
|
|
78
84
|
* Build a provider's manifest from the lists it already maintains.
|
|
79
85
|
*
|
|
@@ -92,6 +98,11 @@ export declare function reviewedCatalog(classification: ToolClassification, owne
|
|
|
92
98
|
* after review.
|
|
93
99
|
*/
|
|
94
100
|
export declare function reviewedClassification(classification: ToolClassification, owner: string): ToolClassification;
|
|
101
|
+
/** One whole-schema verification shared by reporting and serving this listing. */
|
|
102
|
+
export interface CatalogSchemaVerification {
|
|
103
|
+
changed: ReadonlySet<string>;
|
|
104
|
+
available: boolean;
|
|
105
|
+
}
|
|
95
106
|
/**
|
|
96
107
|
* Compare a live catalog with the manifest and count what moved.
|
|
97
108
|
*
|
|
@@ -110,25 +121,18 @@ export declare function catalogReviewOf(connector: Connector): VettedCatalog | u
|
|
|
110
121
|
* Classify downstream facts with a review: what the registry serves on every
|
|
111
122
|
* read, from whichever cache layer the facts came from.
|
|
112
123
|
*
|
|
113
|
-
* Every tool returned is
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* meanwhile no digested review vouches for a read (INV-1).
|
|
124
|
+
* Every tool returned is fresh. The registry supplies the same whole-schema
|
|
125
|
+
* verification it observed for this listing, including failures. Standalone
|
|
126
|
+
* callers verify here. No failed verification survives onto a later listing,
|
|
127
|
+
* and meanwhile no digested review vouches for a read (INV-1).
|
|
118
128
|
*/
|
|
119
|
-
export declare function classifyCatalog(catalog: VettedCatalog, connectorId: string, facts: readonly ToolDef[], logger: Logger,
|
|
129
|
+
export declare function classifyCatalog(catalog: VettedCatalog, connectorId: string, facts: readonly ToolDef[], logger: Logger, overrides?: Readonly<Record<string, "read" | "write">>, verification?: CatalogSchemaVerification): Promise<ToolDef[]>;
|
|
120
130
|
/**
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
* The check happens where the tools are already in hand, while serving a
|
|
125
|
-
* refresh the deployment asked for. It adds no request of its own, which is
|
|
126
|
-
* the whole boundary: connecta watches a contract while it is serving a
|
|
127
|
-
* refresh, and never initiates one to go looking
|
|
128
|
-
* ([#179](https://github.com/zackbart/connecta/issues/179),
|
|
129
|
-
* [#343](https://github.com/zackbart/connecta/issues/343)).
|
|
131
|
+
* Observe tools already fetched by a listing. No probe, persistence, or catalog
|
|
132
|
+
* payload: registry-owned partition tokens isolate deployment and subject.
|
|
133
|
+
* A digest failure replaces old success, while classification fails closed.
|
|
130
134
|
*/
|
|
131
|
-
export declare function observeReviewedDrift(connector: Connector, catalog: VettedCatalog, listed: readonly ToolDef[], logger: Logger): Promise<
|
|
132
|
-
/**
|
|
133
|
-
export declare function observedCatalogDrift(connector: Connector): CatalogDriftReport | undefined;
|
|
135
|
+
export declare function observeReviewedDrift(connector: Connector, catalog: VettedCatalog | undefined, listed: readonly ToolDef[], logger: Logger, partition?: object, droppedTools?: number): Promise<CatalogSchemaVerification | undefined>;
|
|
136
|
+
/** Look up only this runtime/catalog partition; never start a listing. */
|
|
137
|
+
export declare function observedCatalogDrift(connector: Connector, partition?: object): CatalogDriftReport | undefined;
|
|
134
138
|
export {};
|
package/dist/catalog-drift.js
CHANGED
|
@@ -20,13 +20,12 @@ function observationTime(value) {
|
|
|
20
20
|
return Number.isFinite(time) ? new Date(time).toISOString() : undefined;
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
|
-
* Rebuild a drift report as bounded counts and a
|
|
23
|
+
* Rebuild a drift report as bounded counts, closed verification facts, and a timestamp.
|
|
24
24
|
*
|
|
25
25
|
* `Connector.catalogDrift()` sits on the open plugin seam, and what it returns
|
|
26
|
-
* lands
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* and a time and nothing else" is *made* true here — at the boundary where
|
|
26
|
+
* lands on authenticated connector status and the operator page. TypeScript
|
|
27
|
+
* constrains neither extra enumerable properties nor `observedAt` at runtime.
|
|
28
|
+
* Counts, closed verification facts, and a time are rebuilt at the boundary where
|
|
30
29
|
* third-party output becomes a response — rather than trusted. A report whose
|
|
31
30
|
* `observedAt` names no instant is no observation connecta can date, and is
|
|
32
31
|
* dropped whole (INV-6): a plugin forwarding a downstream's report could
|
|
@@ -41,6 +40,18 @@ export function boundedCatalogDrift(report) {
|
|
|
41
40
|
return undefined;
|
|
42
41
|
return {
|
|
43
42
|
observedAt,
|
|
43
|
+
...(typeof report.reviewedBaseline === "boolean" ? { reviewedBaseline: report.reviewedBaseline } : {}),
|
|
44
|
+
...(report.verification === "complete" || report.verification === "unavailable"
|
|
45
|
+
? { verification: report.verification }
|
|
46
|
+
: {}),
|
|
47
|
+
...(report.schemaCoverage && typeof report.schemaCoverage === "object"
|
|
48
|
+
? {
|
|
49
|
+
schemaCoverage: {
|
|
50
|
+
pinned: boundedCount(report.schemaCoverage.pinned),
|
|
51
|
+
unpinned: boundedCount(report.schemaCoverage.unpinned),
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
: {}),
|
|
44
55
|
unclassifiedTools: boundedCount(report.unclassifiedTools),
|
|
45
56
|
unservedTools: boundedCount(report.unservedTools),
|
|
46
57
|
annotationConflicts: boundedCount(report.annotationConflicts),
|
|
@@ -66,59 +77,98 @@ function canonicalKeys(value) {
|
|
|
66
77
|
const sorted = Object.fromEntries(entries);
|
|
67
78
|
return Object.keys(sorted).map((key) => [key, sorted[key]]);
|
|
68
79
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
80
|
+
const SCHEMA_MAPS = new Set([
|
|
81
|
+
"properties",
|
|
82
|
+
"patternProperties",
|
|
83
|
+
"$defs",
|
|
84
|
+
"definitions",
|
|
85
|
+
"dependentSchemas",
|
|
86
|
+
"dependencies",
|
|
87
|
+
]);
|
|
88
|
+
const SCHEMA_ARRAYS = new Set(["allOf", "anyOf", "oneOf", "prefixItems"]);
|
|
89
|
+
const SCHEMA_CHILDREN = new Set([
|
|
90
|
+
"items",
|
|
91
|
+
"additionalItems",
|
|
92
|
+
"additionalProperties",
|
|
93
|
+
"unevaluatedProperties",
|
|
94
|
+
"unevaluatedItems",
|
|
95
|
+
"contains",
|
|
96
|
+
"propertyNames",
|
|
97
|
+
"not",
|
|
98
|
+
"if",
|
|
99
|
+
"then",
|
|
100
|
+
"else",
|
|
101
|
+
"contentSchema",
|
|
102
|
+
]);
|
|
103
|
+
function childMode(mode, key, value) {
|
|
104
|
+
if (mode === "root" || mode === "map")
|
|
105
|
+
return "schema";
|
|
106
|
+
if (mode !== "schema")
|
|
107
|
+
return "data";
|
|
108
|
+
if (key === "required" || key === "enum" || key === "type")
|
|
109
|
+
return "set";
|
|
110
|
+
if (SCHEMA_MAPS.has(key))
|
|
111
|
+
return "map";
|
|
112
|
+
if (SCHEMA_ARRAYS.has(key) || (key === "items" && Array.isArray(value)))
|
|
113
|
+
return "schema-array";
|
|
114
|
+
return SCHEMA_CHILDREN.has(key) ? "schema" : "data";
|
|
115
|
+
}
|
|
116
|
+
function canonicalJson(root, sets) {
|
|
76
117
|
const out = [];
|
|
77
|
-
const stack = [{ value: root }];
|
|
118
|
+
const stack = [{ value: root, mode: sets ? "root" : "data", out }];
|
|
78
119
|
let nodes = 0;
|
|
79
120
|
while (stack.length > 0) {
|
|
80
121
|
const task = stack.pop();
|
|
122
|
+
const out = task.out;
|
|
81
123
|
if ("text" in task) {
|
|
82
124
|
out.push(task.text);
|
|
83
125
|
continue;
|
|
84
126
|
}
|
|
85
|
-
if (
|
|
86
|
-
|
|
127
|
+
if ("members" in task) {
|
|
128
|
+
out.push("[", task.members
|
|
129
|
+
.map((member) => member.join(""))
|
|
130
|
+
.sort()
|
|
131
|
+
.join(","), "]");
|
|
132
|
+
continue;
|
|
87
133
|
}
|
|
88
|
-
|
|
89
|
-
|
|
134
|
+
if (++nodes > MAX_SCHEMA_DIGEST_NODES)
|
|
135
|
+
throw new Error(`schema has more than ${MAX_SCHEMA_DIGEST_NODES} values; refusing to digest it in part.`);
|
|
136
|
+
const { value, mode } = task;
|
|
137
|
+
if (value === null || value === undefined)
|
|
90
138
|
out.push("null");
|
|
91
|
-
|
|
92
|
-
else if (typeof value === "string" || typeof value === "boolean") {
|
|
139
|
+
else if (typeof value === "string" || typeof value === "boolean")
|
|
93
140
|
out.push(JSON.stringify(value));
|
|
94
|
-
|
|
95
|
-
else if (typeof value === "number") {
|
|
141
|
+
else if (typeof value === "number")
|
|
96
142
|
out.push(Number.isFinite(value) ? JSON.stringify(value) : "null");
|
|
97
|
-
|
|
98
|
-
else if (typeof value === "function" || typeof value === "symbol") {
|
|
99
|
-
// Only array items reach here; object keys holding these are dropped.
|
|
143
|
+
else if (typeof value === "function" || typeof value === "symbol")
|
|
100
144
|
out.push("null");
|
|
101
|
-
|
|
102
|
-
else if (typeof value !== "object") {
|
|
145
|
+
else if (typeof value !== "object")
|
|
103
146
|
throw new Error(`schema holds a ${typeof value}, which JSON cannot carry.`);
|
|
104
|
-
}
|
|
105
147
|
else if (Array.isArray(value)) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
148
|
+
if (mode === "set") {
|
|
149
|
+
const members = Array.from({ length: value.length }, () => []);
|
|
150
|
+
stack.push({ members, out });
|
|
151
|
+
for (let index = value.length - 1; index >= 0; index--)
|
|
152
|
+
stack.push({ value: value[index], mode: "data", out: members[index] });
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
out.push("[");
|
|
156
|
+
stack.push({ text: "]", out });
|
|
157
|
+
for (let index = value.length - 1; index >= 0; index--) {
|
|
158
|
+
stack.push({ value: value[index], mode: mode === "schema-array" ? "schema" : "data", out });
|
|
159
|
+
if (index > 0)
|
|
160
|
+
stack.push({ text: ",", out });
|
|
161
|
+
}
|
|
112
162
|
}
|
|
113
163
|
}
|
|
114
164
|
else {
|
|
115
165
|
out.push("{");
|
|
116
|
-
stack.push({ text: "}" });
|
|
166
|
+
stack.push({ text: "}", out });
|
|
117
167
|
const entries = canonicalKeys(value);
|
|
118
|
-
for (let index = entries.length - 1; index >= 0; index
|
|
168
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
119
169
|
const [key, item] = entries[index];
|
|
120
|
-
stack.push({ value: item });
|
|
121
|
-
stack.push({ text: `${index > 0 ? "," : ""}${JSON.stringify(key)}
|
|
170
|
+
stack.push({ value: item, mode: childMode(mode, key, item), out });
|
|
171
|
+
stack.push({ text: `${index > 0 ? "," : ""}${JSON.stringify(key)}:`, out });
|
|
122
172
|
}
|
|
123
173
|
}
|
|
124
174
|
}
|
|
@@ -131,19 +181,23 @@ function canonicalJson(root) {
|
|
|
131
181
|
* `src/catalog-fingerprint.ts`: that one is deliberately conservative because a
|
|
132
182
|
* spurious cache write is cheap, while a spurious drift finding spends a
|
|
133
183
|
* maintainer's attention on a downstream that reordered its JSON keys.
|
|
184
|
+
* New pins use `sha256-sets-v1:` and sort required, enum, and type arrays only
|
|
185
|
+
* at schema keyword locations. Tuple arrays and JSON data keep their order.
|
|
186
|
+
* Legacy `sha256:` pins use the original literal-array form, so an upgrade
|
|
187
|
+
* cannot silently invalidate a reviewed provider manifest.
|
|
134
188
|
* Description and annotations are excluded — a reworded description is P1's
|
|
135
189
|
* business, and an annotation change is already its own drift category.
|
|
136
190
|
*
|
|
137
191
|
* Rejects rather than digesting part of a schema: a digest that ignored some
|
|
138
192
|
* leaf would let a reviewed read keep its verdict after that leaf changed.
|
|
139
193
|
*/
|
|
140
|
-
export async function vettedSchemaDigest(tool) {
|
|
194
|
+
export async function vettedSchemaDigest(tool, format = "sets-v1") {
|
|
141
195
|
const bytes = encoder.encode(canonicalJson({
|
|
142
196
|
inputSchema: tool.inputSchema ?? null,
|
|
143
197
|
outputSchema: tool.outputSchema ?? null,
|
|
144
|
-
}));
|
|
198
|
+
}, format === "sets-v1"));
|
|
145
199
|
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
|
|
146
|
-
return
|
|
200
|
+
return `${format === "sets-v1" ? "sha256-sets-v1" : "sha256"}:${[...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
147
201
|
}
|
|
148
202
|
/**
|
|
149
203
|
* Build a provider's manifest from the lists it already maintains.
|
|
@@ -160,6 +214,7 @@ export function vettedCatalog(input) {
|
|
|
160
214
|
const digest = digestFor(name);
|
|
161
215
|
tools.set(name, {
|
|
162
216
|
verdict: "read-only",
|
|
217
|
+
...(input.optionalTools?.has(name) ? { optional: true } : {}),
|
|
163
218
|
...(digest !== undefined ? { schemaDigest: digest } : {}),
|
|
164
219
|
});
|
|
165
220
|
}
|
|
@@ -170,6 +225,7 @@ export function vettedCatalog(input) {
|
|
|
170
225
|
const digest = digestFor(name);
|
|
171
226
|
tools.set(name, {
|
|
172
227
|
verdict,
|
|
228
|
+
...(input.optionalTools?.has(name) ? { optional: true } : {}),
|
|
173
229
|
...(digest !== undefined ? { schemaDigest: digest } : {}),
|
|
174
230
|
});
|
|
175
231
|
}
|
|
@@ -178,6 +234,10 @@ export function vettedCatalog(input) {
|
|
|
178
234
|
throw new Error(`vettedCatalog() recorded a schema digest for unclassified tool "${name}".`);
|
|
179
235
|
}
|
|
180
236
|
}
|
|
237
|
+
for (const name of input.optionalTools ?? []) {
|
|
238
|
+
if (!tools.has(name))
|
|
239
|
+
throw new Error(`vettedCatalog() marked unclassified tool "${name}" optional.`);
|
|
240
|
+
}
|
|
181
241
|
return { version: 1, tools };
|
|
182
242
|
}
|
|
183
243
|
const VERDICTS = {
|
|
@@ -190,8 +250,8 @@ const PUBLIC_VERDICTS = {
|
|
|
190
250
|
additive: "write",
|
|
191
251
|
destructive: "destructive",
|
|
192
252
|
};
|
|
193
|
-
const SCHEMA_DIGEST = /^sha256
|
|
194
|
-
const REVIEWED_TOOL_KEYS = new Set(["verdict", "reason", "schemaDigest"]);
|
|
253
|
+
const SCHEMA_DIGEST = /^sha256(?:-sets-v1)?:[0-9a-f]{64}$/;
|
|
254
|
+
const REVIEWED_TOOL_KEYS = new Set(["verdict", "reason", "schemaDigest", "optional"]);
|
|
195
255
|
function isRecord(value) {
|
|
196
256
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
197
257
|
}
|
|
@@ -232,7 +292,7 @@ function parseClassification(classification, owner) {
|
|
|
232
292
|
if (!REVIEWED_TOOL_KEYS.has(key))
|
|
233
293
|
fail(`tool "${name}" has unknown key "${key}".`);
|
|
234
294
|
}
|
|
235
|
-
const { verdict: publicVerdict, reason, schemaDigest: digest } = record;
|
|
295
|
+
const { verdict: publicVerdict, reason, schemaDigest: digest, optional } = record;
|
|
236
296
|
const verdict = typeof publicVerdict === "string" && Object.hasOwn(VERDICTS, publicVerdict) ? VERDICTS[publicVerdict] : undefined;
|
|
237
297
|
if (verdict === undefined) {
|
|
238
298
|
fail(`tool "${name}" needs verdict "read", "write", or "destructive".`);
|
|
@@ -241,10 +301,13 @@ function parseClassification(classification, owner) {
|
|
|
241
301
|
fail(`tool "${name}" reason must be a non-empty string.`);
|
|
242
302
|
}
|
|
243
303
|
if (digest !== undefined && (typeof digest !== "string" || !SCHEMA_DIGEST.test(digest))) {
|
|
244
|
-
fail(`tool "${name}" schemaDigest must be "sha256:" and 64 lowercase hex digits.`);
|
|
304
|
+
fail(`tool "${name}" schemaDigest must be "sha256:" or "sha256-sets-v1:" and 64 lowercase hex digits.`);
|
|
245
305
|
}
|
|
306
|
+
if (optional !== undefined && typeof optional !== "boolean")
|
|
307
|
+
fail(`tool "${name}" optional must be a boolean.`);
|
|
246
308
|
tools.set(name, {
|
|
247
309
|
verdict,
|
|
310
|
+
...(typeof optional === "boolean" ? { optional } : {}),
|
|
248
311
|
...(typeof digest === "string" ? { schemaDigest: digest } : {}),
|
|
249
312
|
});
|
|
250
313
|
frozen[name] =
|
|
@@ -253,6 +316,7 @@ function parseClassification(classification, owner) {
|
|
|
253
316
|
: Object.freeze({
|
|
254
317
|
verdict: PUBLIC_VERDICTS[verdict],
|
|
255
318
|
...(typeof reason === "string" ? { reason } : {}),
|
|
319
|
+
...(typeof optional === "boolean" ? { optional } : {}),
|
|
256
320
|
...(typeof digest === "string" ? { schemaDigest: digest } : {}),
|
|
257
321
|
});
|
|
258
322
|
}
|
|
@@ -360,7 +424,8 @@ async function changedSchemas(catalog, tools) {
|
|
|
360
424
|
const changed = new Set();
|
|
361
425
|
for (const definition of tools) {
|
|
362
426
|
const recorded = catalog.tools.get(definition.name)?.schemaDigest;
|
|
363
|
-
if (recorded !== undefined &&
|
|
427
|
+
if (recorded !== undefined &&
|
|
428
|
+
recorded !== (await vettedSchemaDigest(definition, recorded.startsWith("sha256:") ? "legacy" : "sets-v1"))) {
|
|
364
429
|
changed.add(definition.name);
|
|
365
430
|
}
|
|
366
431
|
}
|
|
@@ -387,8 +452,8 @@ function countDrift(catalog, tools, changed) {
|
|
|
387
452
|
annotationConflicts += 1;
|
|
388
453
|
}
|
|
389
454
|
let unservedTools = 0;
|
|
390
|
-
for (const name of catalog.tools
|
|
391
|
-
if (!served.has(name))
|
|
455
|
+
for (const [name, record] of catalog.tools) {
|
|
456
|
+
if (!record.optional && !served.has(name))
|
|
392
457
|
unservedTools += 1;
|
|
393
458
|
}
|
|
394
459
|
return {
|
|
@@ -432,18 +497,20 @@ export function catalogReviewOf(connector) {
|
|
|
432
497
|
* Classify downstream facts with a review: what the registry serves on every
|
|
433
498
|
* read, from whichever cache layer the facts came from.
|
|
434
499
|
*
|
|
435
|
-
* Every tool returned is
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
* meanwhile no digested review vouches for a read (INV-1).
|
|
500
|
+
* Every tool returned is fresh. The registry supplies the same whole-schema
|
|
501
|
+
* verification it observed for this listing, including failures. Standalone
|
|
502
|
+
* callers verify here. No failed verification survives onto a later listing,
|
|
503
|
+
* and meanwhile no digested review vouches for a read (INV-1).
|
|
440
504
|
*/
|
|
441
|
-
export async function classifyCatalog(catalog, connectorId, facts, logger,
|
|
442
|
-
let lapsed =
|
|
505
|
+
export async function classifyCatalog(catalog, connectorId, facts, logger, overrides, verification) {
|
|
506
|
+
let lapsed = verification
|
|
507
|
+
? verification.available
|
|
508
|
+
? verification.changed
|
|
509
|
+
: digestedTools(catalog, facts)
|
|
510
|
+
: undefined;
|
|
443
511
|
if (!lapsed) {
|
|
444
512
|
try {
|
|
445
513
|
lapsed = await changedSchemas(catalog, facts);
|
|
446
|
-
memo?.set(facts, lapsed);
|
|
447
514
|
}
|
|
448
515
|
catch (error) {
|
|
449
516
|
lapsed = digestedTools(catalog, facts);
|
|
@@ -455,39 +522,57 @@ export async function classifyCatalog(catalog, connectorId, facts, logger, memo,
|
|
|
455
522
|
.filter((fact) => catalog.unlisted !== "hide" || catalog.tools.has(fact.name))
|
|
456
523
|
.map((fact) => servedTool(catalog, fact, unverified, overrides?.[fact.name]));
|
|
457
524
|
}
|
|
458
|
-
/**
|
|
459
|
-
* The drift each reviewed connector object showed on its last listing in this
|
|
460
|
-
* runtime. Keyed by the connector rather than by registry, so a principal's
|
|
461
|
-
* registry and the root report one observation, as they serve one connector.
|
|
462
|
-
*/
|
|
525
|
+
/** Counts only, retained weakly by connector and an explicit runtime/catalog partition. */
|
|
463
526
|
const observations = new WeakMap();
|
|
464
527
|
/**
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
*
|
|
468
|
-
* The check happens where the tools are already in hand, while serving a
|
|
469
|
-
* refresh the deployment asked for. It adds no request of its own, which is
|
|
470
|
-
* the whole boundary: connecta watches a contract while it is serving a
|
|
471
|
-
* refresh, and never initiates one to go looking
|
|
472
|
-
* ([#179](https://github.com/zackbart/connecta/issues/179),
|
|
473
|
-
* [#343](https://github.com/zackbart/connecta/issues/343)).
|
|
528
|
+
* Observe tools already fetched by a listing. No probe, persistence, or catalog
|
|
529
|
+
* payload: registry-owned partition tokens isolate deployment and subject.
|
|
530
|
+
* A digest failure replaces old success, while classification fails closed.
|
|
474
531
|
*/
|
|
475
|
-
export async function observeReviewedDrift(connector, catalog, listed, logger) {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
532
|
+
export async function observeReviewedDrift(connector, catalog, listed, logger, partition = connector, droppedTools = 0) {
|
|
533
|
+
let verification;
|
|
534
|
+
let report = {
|
|
535
|
+
observedAt: new Date().toISOString(),
|
|
536
|
+
reviewedBaseline: catalog !== undefined,
|
|
537
|
+
unclassifiedTools: 0,
|
|
538
|
+
unservedTools: 0,
|
|
539
|
+
annotationConflicts: 0,
|
|
540
|
+
schemaChanges: 0,
|
|
541
|
+
...(droppedTools > 0 ? { droppedTools } : {}),
|
|
542
|
+
};
|
|
543
|
+
if (catalog) {
|
|
544
|
+
const reviewed = listed.filter((tool) => catalog.tools.has(tool.name));
|
|
545
|
+
const pinned = digestedTools(catalog, reviewed).size;
|
|
546
|
+
const schemaCoverage = { pinned, unpinned: reviewed.length - pinned };
|
|
547
|
+
try {
|
|
548
|
+
verification = { changed: await changedSchemas(catalog, listed), available: true };
|
|
549
|
+
report = {
|
|
550
|
+
...report,
|
|
551
|
+
...countDrift(catalog, listed, verification.changed),
|
|
552
|
+
schemaCoverage,
|
|
553
|
+
verification: "complete",
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
catch (error) {
|
|
557
|
+
verification = { changed: new Set(), available: false };
|
|
558
|
+
report = {
|
|
559
|
+
...report,
|
|
560
|
+
...countDrift(catalog, listed, verification.changed),
|
|
561
|
+
schemaCoverage,
|
|
562
|
+
verification: "unavailable",
|
|
563
|
+
};
|
|
564
|
+
logFailure(logger, "catalog drift check failed", failureRecord({ connector: connector.id }, error));
|
|
565
|
+
}
|
|
482
566
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
logFailure(logger, "catalog drift check failed", failureRecord({ connector: connector.id }, error));
|
|
567
|
+
let partitions = observations.get(connector);
|
|
568
|
+
if (!partitions) {
|
|
569
|
+
partitions = new WeakMap();
|
|
570
|
+
observations.set(connector, partitions);
|
|
488
571
|
}
|
|
572
|
+
partitions.set(partition, report);
|
|
573
|
+
return verification;
|
|
489
574
|
}
|
|
490
|
-
/**
|
|
491
|
-
export function observedCatalogDrift(connector) {
|
|
492
|
-
return observations.get(connector);
|
|
575
|
+
/** Look up only this runtime/catalog partition; never start a listing. */
|
|
576
|
+
export function observedCatalogDrift(connector, partition = connector) {
|
|
577
|
+
return observations.get(connector)?.get(partition);
|
|
493
578
|
}
|
package/dist/catalog-service.js
CHANGED
|
@@ -465,7 +465,7 @@ export class CatalogService {
|
|
|
465
465
|
// that asked for it. On a read-only endpoint a write is absent from search
|
|
466
466
|
// and describe, as if the connector never had it. Guides are not filtered.
|
|
467
467
|
discoveryCatalogs(ids, route) {
|
|
468
|
-
return Effect.forEach(ids, (id) => Effect.result(withDeadlineEffect((signal) => this.readOnly
|
|
468
|
+
return Effect.forEach(ids, (id) => Effect.result(withDeadlineEffect((signal) => this.readOnly || this.registry.getConnector(id)?.readOnly === true
|
|
469
469
|
? Effect.map(this.catalog(id, { signal }), (tools) => tools.filter((tool) => tool.classification === "read"))
|
|
470
470
|
: this.catalog(id, { signal }), {
|
|
471
471
|
...(this.requestSignal ? { signal: this.requestSignal } : {}),
|
|
@@ -582,7 +582,7 @@ export class CatalogService {
|
|
|
582
582
|
}
|
|
583
583
|
return withAuthorizationHandoff(this.requestScope, error);
|
|
584
584
|
}
|
|
585
|
-
async search(args) {
|
|
585
|
+
async search(args, onCatalogReference) {
|
|
586
586
|
if (args.query !== undefined && typeof args.query !== "string") {
|
|
587
587
|
throw new DiscoveryPolicyError("invalid_args", "query must be a string. Omit it or use an empty string to browse the catalog.");
|
|
588
588
|
}
|
|
@@ -915,6 +915,20 @@ export class CatalogService {
|
|
|
915
915
|
: "No single tool matched every term in the catalogs that answered. Split distinct intents into separate searches.";
|
|
916
916
|
};
|
|
917
917
|
const guidance = searchGuidance();
|
|
918
|
+
// Counts, ranking, and term analysis use the whole searched corpus, not
|
|
919
|
+
// just this page's tools. Bind every searched connector, including failed
|
|
920
|
+
// catalogs and off-page matches, so paging cannot reveal a hidden
|
|
921
|
+
// connector's contribution. Keep provenance host-only.
|
|
922
|
+
for (const connector of connectors)
|
|
923
|
+
onCatalogReference?.(connector.id);
|
|
924
|
+
for (const entry of entries)
|
|
925
|
+
onCatalogReference?.(entry.connector.id, entry.tool.name);
|
|
926
|
+
for (const connector of absence?.configuredConnectors ?? [])
|
|
927
|
+
onCatalogReference?.(connector);
|
|
928
|
+
// An unknown scope's guidance lists the visible roster without searching it.
|
|
929
|
+
if (reportsQueryAnalysis && args.connector && !scopedConnector)
|
|
930
|
+
for (const connector of visibleConnectors)
|
|
931
|
+
onCatalogReference?.(connector.id);
|
|
918
932
|
return {
|
|
919
933
|
catalogErrors,
|
|
920
934
|
...(absence ? { absence } : {}),
|
|
@@ -74,7 +74,12 @@ export function describeConfigSources(snapshot, raw) {
|
|
|
74
74
|
? provided(raw.pools?.[key]?.readOnly ?? raw.readOnly)
|
|
75
75
|
: field === "owner"
|
|
76
76
|
? provided(raw.pools?.[key]?.owner)
|
|
77
|
-
:
|
|
77
|
+
: field === "description"
|
|
78
|
+
? provided(raw.pools?.[key]?.description)
|
|
79
|
+
: provided(raw.pools);
|
|
80
|
+
else if (group === "modules" && key === "activity" && field === "store" && parts[3] !== "kind")
|
|
81
|
+
// A store option such as retentionDays is set only when the store says so.
|
|
82
|
+
source = factorySource(factorySources(raw.activity?.store), parts.slice(3)) ?? "default";
|
|
78
83
|
else if (group === "modules") {
|
|
79
84
|
source = provided(key === "ui" ? raw.ui : key === "vault" ? raw.vault : key === "activity" ? raw.activity : raw.accessTokens);
|
|
80
85
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -181,10 +181,19 @@ declare const pool: {
|
|
|
181
181
|
* The Google Workspace address agents in this pool act as, on a Workspace
|
|
182
182
|
* connector whose `subject` is a mapping function. Humans always act as
|
|
183
183
|
* themselves through that mapping, and a fixed `subject` address applies to
|
|
184
|
-
* everyone. Without an owner,
|
|
185
|
-
*
|
|
184
|
+
* everyone. Without an owner, a mapping-subject Workspace connector is
|
|
185
|
+
* absent for this pool's agents, as a personal connector is; people in the
|
|
186
|
+
* pool still see it.
|
|
186
187
|
*/
|
|
187
188
|
owner: import("./config-schema.js").Field<string, string | undefined, true>;
|
|
189
|
+
/**
|
|
190
|
+
* What this pool is for, in plain words for the model: appended to the MCP
|
|
191
|
+
* `initialize` instructions at `/mcp/<name>` and `/agent/<name>`, so a
|
|
192
|
+
* caller learns its scope ("Team tier: shared tools only."). Trimmed, 1 to
|
|
193
|
+
* 500 characters, no line breaks or control characters. It grants and hides nothing; plain
|
|
194
|
+
* `/mcp` never shows it.
|
|
195
|
+
*/
|
|
196
|
+
description: import("./config-schema.js").Field<string, string | undefined, true>;
|
|
188
197
|
};
|
|
189
198
|
declare const connectaConfig: {
|
|
190
199
|
connectors: import("./config-schema.js").Field<Connector[], readonly Connector[], false>;
|
|
@@ -210,6 +219,7 @@ declare const connectaConfig: {
|
|
|
210
219
|
pools: import("./config-schema.js").Field<Record<string, {
|
|
211
220
|
readOnly?: boolean | undefined;
|
|
212
221
|
owner?: string | undefined;
|
|
222
|
+
description?: string | undefined;
|
|
213
223
|
tools: readonly string[];
|
|
214
224
|
members: readonly MemberRef[];
|
|
215
225
|
}>, Readonly<Record<string, {
|
|
@@ -217,6 +227,7 @@ declare const connectaConfig: {
|
|
|
217
227
|
readonly tools: readonly string[];
|
|
218
228
|
readonly members: readonly MemberRef[];
|
|
219
229
|
readonly owner: string | undefined;
|
|
230
|
+
readonly description: string | undefined;
|
|
220
231
|
}>> | undefined, true>;
|
|
221
232
|
/** Exact connector id -> tool name -> verdict. Unknown names fail at catalog publication. */
|
|
222
233
|
classification: import("./config-schema.js").Field<Record<string, Record<string, "read" | "write">>, Readonly<Record<string, Readonly<Record<string, "read" | "write">>>> | undefined, true>;
|