@zackbart/connecta 0.28.1 → 0.29.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 +788 -5
- package/README.md +62 -42
- package/bin/connecta.mjs +110 -139
- package/bin/meta-tool-names.d.mts +2 -0
- package/bin/meta-tool-names.mjs +9 -0
- package/bin/version.mjs +3 -0
- package/dist/access-tokens.js +53 -63
- package/dist/activity-request.js +8 -0
- package/dist/activity.d.ts +76 -42
- package/dist/activity.js +110 -42
- package/dist/auth/clerk-sdk/client.d.ts +65 -0
- package/dist/auth/clerk-sdk/client.js +5 -0
- package/dist/auth/clerk-sdk/generated.js +1845 -0
- package/dist/auth/clerk-transport.js +19 -0
- package/dist/auth/clerk.d.ts +17 -7
- package/dist/auth/clerk.js +189 -107
- package/dist/auth/cloudflare-access.js +25 -26
- package/dist/auth/downstream-client-metadata.d.ts +29 -0
- package/dist/auth/downstream-client-metadata.js +133 -0
- package/dist/auth/downstream-oauth.js +1584 -1580
- package/dist/auth/downstream-revocation.js +106 -0
- package/dist/auth/oauth-token-response.js +231 -0
- package/dist/auth/oauth-v2-migration.js +116 -0
- package/dist/auth/static-oauth.js +93 -71
- package/dist/auth-elicitation.js +283 -0
- package/dist/branding.d.ts +8 -16
- package/dist/branding.js +26 -59
- package/dist/byte-read-response.js +39 -0
- package/dist/call-admission.js +12 -16
- package/dist/catalog-cache.js +739 -0
- package/dist/catalog-drift.d.ts +50 -15
- package/dist/catalog-drift.js +365 -128
- package/dist/catalog-fingerprint.js +2 -4
- package/dist/catalog-freshness.js +13 -0
- package/dist/catalog-limits.js +2 -2
- package/dist/catalog-service.js +368 -224
- package/dist/catalog.js +43 -84
- package/dist/concurrency.js +3 -4
- package/dist/config-defaults.d.ts +39 -0
- package/dist/config-defaults.js +46 -0
- package/dist/config-schema.d.ts +181 -0
- package/dist/config-schema.js +435 -0
- package/dist/config-value-sources.js +102 -0
- package/dist/config.d.ts +631 -0
- package/dist/config.js +503 -0
- package/dist/connector-access.d.ts +1 -0
- package/dist/connector-access.js +13 -15
- package/dist/connector-caller.d.ts +8 -0
- package/dist/connector-caller.js +15 -6
- package/dist/connector-scope.js +1 -1
- package/dist/connectors/api-connector.d.ts +59 -7
- package/dist/connectors/api-connector.js +106 -19
- package/dist/connectors/api.js +4 -1
- package/dist/connectors/guarded-fetch.d.ts +144 -0
- package/dist/connectors/guarded-fetch.js +84 -35
- package/dist/connectors/negotiation-cache.js +46 -0
- package/dist/connectors/option-shapes.js +27 -0
- package/dist/connectors/remote-mcp.d.ts +30 -4
- package/dist/connectors/remote-mcp.js +1774 -320
- package/dist/connectors/resource-uri.js +335 -0
- package/dist/credential-contract.d.ts +10 -0
- package/dist/credential-rules.js +1 -3
- package/dist/credential-url.js +108 -0
- package/dist/credentials.d.ts +6 -1
- package/dist/credentials.js +27 -18
- package/dist/d1.d.ts +130 -0
- package/dist/d1.js +290 -0
- package/dist/describe-config.d.ts +165 -0
- package/dist/describe-config.js +315 -0
- package/dist/described.js +106 -0
- package/dist/downstream-input-context.js +119 -0
- package/dist/downstream-input.js +323 -0
- package/dist/errors.d.ts +97 -20
- package/dist/errors.js +158 -80
- package/dist/execute.js +453 -305
- package/dist/executor-admission.d.ts +1 -0
- package/dist/executor-admission.js +11 -15
- package/dist/executor-contract.d.ts +20 -0
- package/dist/executor-contract.js +48 -0
- package/dist/executor-result.js +24 -9
- package/dist/executors/quickjs-child.js +3 -4
- package/dist/executors/quickjs-runtime.js +147 -59
- package/dist/executors/quickjs.js +29 -35
- package/dist/favicon.js +1 -2
- package/dist/fix-prompt.js +1 -3
- package/dist/guest-runtime.js +188 -0
- package/dist/guest-types.d.ts +176 -0
- package/dist/guest-types.js +1 -0
- package/dist/html-security.js +24 -0
- package/dist/identity.js +1 -3
- package/dist/inbound-credential.js +52 -0
- package/dist/index.d.ts +24 -256
- package/dist/index.js +65 -478
- package/dist/invocation-auth.js +69 -0
- package/dist/invocation.js +230 -73
- package/dist/json-schema.d.ts +1 -1
- package/dist/mcp-client-context.js +35 -0
- package/dist/mcp-result.js +11 -7
- package/dist/meta-output.js +76 -0
- package/dist/meta-tool-names.d.ts +2 -0
- package/dist/meta-tool-names.js +3 -0
- package/dist/meta-tools.js +285 -209
- package/dist/module-contracts.d.ts +8 -23
- package/dist/node.d.ts +0 -2
- package/dist/node.js +2 -2
- package/dist/oauth-callback-outcome.js +1 -3
- package/dist/oauth-handoff.js +117 -0
- package/dist/oauth-sealing.js +20 -0
- package/dist/operator-record.d.ts +137 -0
- package/dist/operator-record.js +420 -0
- package/dist/operator-ui/assets.js +31 -0
- package/dist/operator-ui/contract.d.ts +72 -0
- package/dist/operator-ui/contract.js +1 -0
- package/dist/operator-ui/fix-prompts.d.ts +18 -0
- package/dist/operator-ui/generated.js +5 -4
- package/dist/operator-ui/model.d.ts +12 -87
- package/dist/operator-ui/model.js +2 -9
- package/dist/operator-ui/setup-commands.js +1 -3
- package/dist/operator-ui/view.d.ts +323 -0
- package/dist/operator-ui/view.js +62 -189
- package/dist/page-styles.js +2 -3
- package/dist/program-source.js +14 -2
- package/dist/{exempt-writes.js → program-writes.js} +55 -18
- package/dist/provider.d.ts +142 -0
- package/dist/provider.js +187 -0
- package/dist/providers/_shared/google/result-size.js +57 -0
- package/dist/providers/_shared/google/service-account.d.ts +58 -0
- package/dist/providers/_shared/google/service-account.js +500 -0
- package/dist/providers/_shared/google/workspace.d.ts +276 -0
- package/dist/providers/_shared/google/workspace.js +518 -0
- package/dist/providers/{basecamp.d.ts → basecamp/index.d.ts} +6 -28
- package/dist/providers/basecamp/index.js +997 -0
- package/dist/providers/basecamp/skill.generated.js +9 -0
- package/dist/providers/{breeze.d.ts → breeze/index.d.ts} +2 -2
- package/dist/providers/{breeze.js → breeze/index.js} +184 -140
- package/dist/providers/breeze/skill.generated.js +9 -0
- package/dist/providers/{ccb.d.ts → ccb/index.d.ts} +2 -2
- package/dist/providers/{ccb.js → ccb/index.js} +271 -116
- package/dist/providers/ccb/skill.generated.js +9 -0
- package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
- package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
- package/dist/providers/cloudflare/skill.generated.js +10 -0
- package/dist/providers/docs/index.d.ts +54 -0
- package/dist/providers/docs/index.js +1237 -0
- package/dist/providers/docs/skill.generated.js +9 -0
- package/dist/providers/drive/index.d.ts +53 -0
- package/dist/providers/drive/index.js +1523 -0
- package/dist/providers/drive/skill.generated.js +9 -0
- package/dist/providers/forms/index.d.ts +54 -0
- package/dist/providers/forms/index.js +1217 -0
- package/dist/providers/forms/skill.generated.js +11 -0
- package/dist/providers/github/auth.d.ts +32 -0
- package/dist/providers/github/auth.js +361 -0
- package/dist/providers/github/hosted.js +218 -0
- package/dist/providers/github/index.d.ts +13 -0
- package/dist/providers/github/index.js +140 -0
- package/dist/providers/github/key.js +100 -0
- package/dist/providers/github/rest.js +321 -0
- package/dist/providers/github/scope.d.ts +29 -0
- package/dist/providers/github/scope.js +124 -0
- package/dist/providers/github/skill.generated.js +8 -0
- package/dist/providers/gmail/index.d.ts +51 -0
- package/dist/providers/gmail/index.js +1436 -0
- package/dist/providers/gmail/skill.generated.js +9 -0
- package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
- package/dist/providers/linear/index.js +352 -0
- package/dist/providers/linear/skill.generated.js +8 -0
- package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
- package/dist/providers/mixpanel/index.js +416 -0
- package/dist/providers/mixpanel/skill.generated.js +9 -0
- package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
- package/dist/providers/{notion.js → notion/index.js} +226 -239
- package/dist/providers/notion/skill.generated.js +10 -0
- package/dist/providers/{overflow.d.ts → overflow/index.d.ts} +2 -2
- package/dist/providers/{overflow.js → overflow/index.js} +74 -111
- package/dist/providers/overflow/skill.generated.js +8 -0
- package/dist/providers/{planning-center.d.ts → planning-center/index.d.ts} +2 -2
- package/dist/providers/{planning-center.js → planning-center/index.js} +264 -104
- package/dist/providers/planning-center/skill.generated.js +10 -0
- package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
- package/dist/providers/revenuecat/index.js +557 -0
- package/dist/providers/revenuecat/skill.generated.js +11 -0
- package/dist/providers/sheets/index.d.ts +54 -0
- package/dist/providers/sheets/index.js +1237 -0
- package/dist/providers/sheets/skill.generated.js +10 -0
- package/dist/providers/slides/index.d.ts +53 -0
- package/dist/providers/slides/index.js +2645 -0
- package/dist/providers/slides/skill.generated.js +9 -0
- package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
- package/dist/providers/stripe/index.js +266 -0
- package/dist/providers/stripe/skill.generated.js +11 -0
- package/dist/providers/{tithely.d.ts → tithely/index.d.ts} +2 -2
- package/dist/providers/{tithely.js → tithely/index.js} +60 -91
- package/dist/providers/tithely/skill.generated.js +8 -0
- package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
- package/dist/providers/vercel/index.js +1038 -0
- package/dist/providers/vercel/mcp-ownership.js +212 -0
- package/dist/providers/vercel/skill.generated.js +11 -0
- package/dist/registry.d.ts +108 -171
- package/dist/registry.js +513 -925
- package/dist/request-state.js +21 -0
- package/dist/resource-read.js +100 -0
- package/dist/result-shapes.js +17 -49
- package/dist/retired-toolkits.js +1 -1
- package/dist/routes/access-tokens.js +3 -6
- package/dist/routes/activity-context.js +21 -0
- package/dist/routes/activity-disclosure.js +40 -0
- package/dist/routes/activity.js +59 -21
- package/dist/routes/actor-labels.js +3 -50
- package/dist/routes/connect.js +221 -0
- package/dist/routes/credentials.js +15 -27
- package/dist/routes/mcp.js +356 -130
- package/dist/routes/oauth-client-metadata.js +15 -0
- package/dist/routes/oauth-management.js +45 -107
- package/dist/routes/oauth.js +110 -57
- package/dist/routes/operator.js +3 -3
- package/dist/routes/shared.d.ts +25 -64
- package/dist/routes/shared.js +160 -80
- package/dist/routes/skills.js +30 -0
- package/dist/routes/ui-config.js +225 -0
- package/dist/routes/ui-data.js +40 -35
- package/dist/routes/ui.js +66 -56
- package/dist/runtime/admission.js +4 -2
- package/dist/runtime/call-admission.js +3 -1
- package/dist/runtime/connector-scope.js +1 -4
- package/dist/runtime/run.js +10 -39
- package/dist/runtime/services.js +12 -117
- package/dist/runtime/shared-read.js +1 -3
- package/dist/runtime/storage.js +8 -1
- package/dist/sent-secrets.d.ts +52 -0
- package/dist/sent-secrets.js +474 -0
- package/dist/server.js +28 -71
- package/dist/skills.js +406 -174
- package/dist/sqlite.d.ts +48 -0
- package/dist/sqlite.js +166 -0
- package/dist/storage/keys.js +428 -0
- package/dist/storage/memory.js +7 -0
- package/dist/storage/sql.d.ts +66 -0
- package/dist/storage/sql.js +534 -0
- package/dist/timeout.js +4 -2
- package/dist/tool-name.js +17 -0
- package/dist/tool-safety.d.ts +9 -35
- package/dist/tool-safety.js +20 -40
- package/dist/types.d.ts +320 -54
- package/dist/types.js +0 -1
- package/dist/ui.d.ts +7 -19
- package/dist/ui.js +76 -90
- package/dist/url-safety.js +3 -6
- package/dist/usage-guide.js +137 -0
- package/dist/validate.d.ts +7 -0
- package/dist/validate.js +309 -47
- package/dist/version.d.ts +1 -7
- package/dist/version.js +3 -7
- package/dist/worker.d.ts +11 -0
- package/dist/worker.js +434 -0
- package/documentation/architecture.md +664 -231
- package/documentation/auth.md +936 -546
- package/documentation/code-mode.md +501 -312
- package/documentation/meta-tools.md +449 -200
- package/documentation/operator-tests.md +93 -0
- package/documentation/provider-migration-0.29.md +60 -0
- package/examples/worker/AGENTS.md +34 -12
- package/examples/worker/README.md +286 -255
- package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
- package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
- package/examples/worker/src/connecta.config.ts +151 -0
- package/examples/worker/src/index.ts +14 -197
- package/examples/worker/wrangler.jsonc +15 -35
- package/package.json +120 -54
- package/templates/node/.dockerignore +1 -2
- package/templates/node/.env.example +20 -21
- package/templates/node/AGENTS.md +21 -17
- package/templates/node/Dockerfile +3 -5
- package/templates/node/README.md +133 -58
- package/templates/node/docker-compose.yml +11 -12
- package/templates/node/package.json +4 -2
- package/templates/node/src/connecta.config.ts +119 -0
- package/templates/node/src/index.ts +9 -154
- package/templates/node/src/provision-token.ts +17 -0
- package/AGENTS.md +0 -170
- package/dist/artifacts/connector.d.ts +0 -58
- package/dist/artifacts/connector.js +0 -730
- package/dist/artifacts/document.js +0 -86
- package/dist/artifacts/guide.js +0 -140
- package/dist/artifacts/html-scan.js +0 -329
- package/dist/artifacts/json.js +0 -78
- package/dist/artifacts/kv-store.d.ts +0 -20
- package/dist/artifacts/kv-store.js +0 -140
- package/dist/artifacts/markdown.js +0 -478
- package/dist/artifacts/module.d.ts +0 -37
- package/dist/artifacts/module.js +0 -66
- package/dist/artifacts/operations.d.ts +0 -197
- package/dist/artifacts/operations.js +0 -1002
- package/dist/artifacts/refresh.d.ts +0 -55
- package/dist/artifacts/refresh.js +0 -236
- package/dist/artifacts/routes.js +0 -206
- package/dist/artifacts/types.d.ts +0 -214
- package/dist/artifacts/types.js +0 -22
- package/dist/artifacts/validate.d.ts +0 -62
- package/dist/artifacts/validate.js +0 -651
- package/dist/artifacts.d.ts +0 -6
- package/dist/artifacts.js +0 -9
- package/dist/auth/bearer.d.ts +0 -11
- package/dist/auth/bearer.js +0 -50
- package/dist/providers/basecamp.js +0 -445
- package/dist/providers/linear.js +0 -217
- package/dist/providers/mixpanel.js +0 -247
- package/dist/providers/revenuecat.js +0 -317
- package/dist/providers/stripe.js +0 -253
- package/dist/providers/vercel.js +0 -1253
- package/dist/storage/file.d.ts +0 -15
- package/dist/storage/file.js +0 -455
- package/ethos.md +0 -93
- package/examples/worker/src/cloudflare-kv.ts +0 -43
- package/examples/worker/src/d1-activity-row.ts +0 -155
- package/examples/worker/src/d1-activity.ts +0 -147
- package/examples/worker/src/d1-storage.ts +0 -138
- package/examples/worker/src/r2-artifact-blobs.ts +0 -29
- package/templates/node/src/file-activity.ts +0 -182
package/dist/catalog-drift.js
CHANGED
|
@@ -1,55 +1,128 @@
|
|
|
1
1
|
// Web-API only, like the rest of the core: a manifest comparison that ran on
|
|
2
2
|
// Node but not on Workers would leave half the deployments unable to tell a
|
|
3
3
|
// stale allowlist from a current one.
|
|
4
|
-
import {
|
|
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;
|
|
4
|
+
import { classifyTool } from "./tool-safety.js";
|
|
5
|
+
import { failureRecord, logFailure } from "./operator-record.js";
|
|
10
6
|
/** A count, or 0 when the seam returned something that is not one. */
|
|
11
7
|
function boundedCount(value) {
|
|
12
|
-
return typeof value === "number" && Number.isFinite(value)
|
|
13
|
-
? Math.max(0, Math.trunc(value))
|
|
14
|
-
: 0;
|
|
8
|
+
return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : 0;
|
|
15
9
|
}
|
|
10
|
+
/** An ISO-8601 date-time with a zone, as `Date.parse` reads it on every runtime. */
|
|
11
|
+
const ISO_INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
16
12
|
/**
|
|
17
|
-
*
|
|
13
|
+
* The report's time, re-serialized from the instant it names, so the string
|
|
14
|
+
* that leaves is always connecta's own ISO-8601 UTC form, never the seam's.
|
|
15
|
+
*/
|
|
16
|
+
function observationTime(value) {
|
|
17
|
+
if (typeof value !== "string" || !ISO_INSTANT.test(value))
|
|
18
|
+
return undefined;
|
|
19
|
+
const time = Date.parse(value);
|
|
20
|
+
return Number.isFinite(time) ? new Date(time).toISOString() : undefined;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Rebuild a drift report as bounded counts and a canonical timestamp.
|
|
18
24
|
*
|
|
19
25
|
* `Connector.catalogDrift()` sits on the open plugin seam, and what it returns
|
|
20
|
-
* lands in the body of unauthenticated `/health
|
|
21
|
-
*
|
|
22
|
-
* `observedAt` at runtime, so "counts
|
|
23
|
-
*
|
|
24
|
-
*
|
|
26
|
+
* lands in the body of unauthenticated `/health`, on connector status, in the
|
|
27
|
+
* operator page, and in `connecta doctor`. TypeScript constrains neither an
|
|
28
|
+
* extra enumerable property nor what `observedAt` holds at runtime, so "counts
|
|
29
|
+
* and a time and nothing else" is *made* true here — at the boundary where
|
|
30
|
+
* third-party output becomes a response — rather than trusted. A report whose
|
|
31
|
+
* `observedAt` names no instant is no observation connecta can date, and is
|
|
32
|
+
* dropped whole (INV-6): a plugin forwarding a downstream's report could
|
|
33
|
+
* otherwise put any text there. The activity path reconstructs its five
|
|
34
|
+
* fields for the same reason.
|
|
25
35
|
*/
|
|
26
36
|
export function boundedCatalogDrift(report) {
|
|
27
37
|
if (!report || typeof report !== "object")
|
|
28
38
|
return undefined;
|
|
39
|
+
const observedAt = observationTime(report.observedAt);
|
|
40
|
+
if (observedAt === undefined)
|
|
41
|
+
return undefined;
|
|
29
42
|
return {
|
|
30
|
-
observedAt
|
|
43
|
+
observedAt,
|
|
31
44
|
unclassifiedTools: boundedCount(report.unclassifiedTools),
|
|
32
45
|
unservedTools: boundedCount(report.unservedTools),
|
|
33
46
|
annotationConflicts: boundedCount(report.annotationConflicts),
|
|
34
47
|
schemaChanges: boundedCount(report.schemaChanges),
|
|
48
|
+
...(boundedCount(report.droppedTools ?? 0) > 0 ? { droppedTools: boundedCount(report.droppedTools ?? 0) } : {}),
|
|
35
49
|
};
|
|
36
50
|
}
|
|
37
51
|
const encoder = new TextEncoder();
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Most JSON values one tool's schemas may hold before its digest refuses to
|
|
54
|
+
* compute. A digest vouches for a whole schema or for nothing, so a schema
|
|
55
|
+
* past the bound is not hashed in part: the computation throws, and the
|
|
56
|
+
* caller serves every digested review unverified (INV-1).
|
|
57
|
+
*/
|
|
58
|
+
const MAX_SCHEMA_DIGEST_NODES = 100_000;
|
|
59
|
+
/** Object keys in the order `JSON.stringify` emits a key-sorted object. */
|
|
60
|
+
function canonicalKeys(value) {
|
|
61
|
+
const entries = Object.entries(value).filter(([, item]) => item !== undefined && typeof item !== "function" && typeof item !== "symbol");
|
|
62
|
+
entries.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
63
|
+
// A plain object enumerates integer-like keys first, in numeric order.
|
|
64
|
+
// Recorded digests were taken from a key-sorted object, so reproduce its
|
|
65
|
+
// enumeration rather than the sort alone.
|
|
66
|
+
const sorted = Object.fromEntries(entries);
|
|
67
|
+
return Object.keys(sorted).map((key) => [key, sorted[key]]);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Deterministic JSON with object keys sorted, so key order is not a schema
|
|
71
|
+
* change. Iterative and complete: every leaf at every depth reaches the
|
|
72
|
+
* digest, and a schema deeper than the host stack cannot exhaust it. Throws
|
|
73
|
+
* past {@link MAX_SCHEMA_DIGEST_NODES}, and on any value JSON cannot carry.
|
|
74
|
+
*/
|
|
75
|
+
function canonicalJson(root) {
|
|
76
|
+
const out = [];
|
|
77
|
+
const stack = [{ value: root }];
|
|
78
|
+
let nodes = 0;
|
|
79
|
+
while (stack.length > 0) {
|
|
80
|
+
const task = stack.pop();
|
|
81
|
+
if ("text" in task) {
|
|
82
|
+
out.push(task.text);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (++nodes > MAX_SCHEMA_DIGEST_NODES) {
|
|
86
|
+
throw new Error(`schema has more than ${MAX_SCHEMA_DIGEST_NODES} values; refusing to digest it in part.`);
|
|
87
|
+
}
|
|
88
|
+
const value = task.value;
|
|
89
|
+
if (value === null || value === undefined) {
|
|
90
|
+
out.push("null");
|
|
91
|
+
}
|
|
92
|
+
else if (typeof value === "string" || typeof value === "boolean") {
|
|
93
|
+
out.push(JSON.stringify(value));
|
|
94
|
+
}
|
|
95
|
+
else if (typeof value === "number") {
|
|
96
|
+
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.
|
|
100
|
+
out.push("null");
|
|
101
|
+
}
|
|
102
|
+
else if (typeof value !== "object") {
|
|
103
|
+
throw new Error(`schema holds a ${typeof value}, which JSON cannot carry.`);
|
|
104
|
+
}
|
|
105
|
+
else if (Array.isArray(value)) {
|
|
106
|
+
out.push("[");
|
|
107
|
+
stack.push({ text: "]" });
|
|
108
|
+
for (let index = value.length - 1; index >= 0; index -= 1) {
|
|
109
|
+
stack.push({ value: value[index] });
|
|
110
|
+
if (index > 0)
|
|
111
|
+
stack.push({ text: "," });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
out.push("{");
|
|
116
|
+
stack.push({ text: "}" });
|
|
117
|
+
const entries = canonicalKeys(value);
|
|
118
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
119
|
+
const [key, item] = entries[index];
|
|
120
|
+
stack.push({ value: item });
|
|
121
|
+
stack.push({ text: `${index > 0 ? "," : ""}${JSON.stringify(key)}:` });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
46
124
|
}
|
|
47
|
-
|
|
48
|
-
return value;
|
|
49
|
-
const entries = Object.entries(value)
|
|
50
|
-
.filter(([, item]) => item !== undefined)
|
|
51
|
-
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
52
|
-
return Object.fromEntries(entries.map(([key, item]) => [key, canonicalize(item, depth + 1)]));
|
|
125
|
+
return out.join("");
|
|
53
126
|
}
|
|
54
127
|
/**
|
|
55
128
|
* Digest the schemas of one downstream tool.
|
|
@@ -60,16 +133,17 @@ function canonicalize(value, depth = 0) {
|
|
|
60
133
|
* maintainer's attention on a downstream that reordered its JSON keys.
|
|
61
134
|
* Description and annotations are excluded — a reworded description is P1's
|
|
62
135
|
* business, and an annotation change is already its own drift category.
|
|
136
|
+
*
|
|
137
|
+
* Rejects rather than digesting part of a schema: a digest that ignored some
|
|
138
|
+
* leaf would let a reviewed read keep its verdict after that leaf changed.
|
|
63
139
|
*/
|
|
64
140
|
export async function vettedSchemaDigest(tool) {
|
|
65
|
-
const bytes = encoder.encode(
|
|
141
|
+
const bytes = encoder.encode(canonicalJson({
|
|
66
142
|
inputSchema: tool.inputSchema ?? null,
|
|
67
143
|
outputSchema: tool.outputSchema ?? null,
|
|
68
|
-
}))
|
|
144
|
+
}));
|
|
69
145
|
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
|
|
70
|
-
return `sha256:${[...digest]
|
|
71
|
-
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
72
|
-
.join("")}`;
|
|
146
|
+
return `sha256:${[...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
73
147
|
}
|
|
74
148
|
/**
|
|
75
149
|
* Build a provider's manifest from the lists it already maintains.
|
|
@@ -106,61 +180,161 @@ export function vettedCatalog(input) {
|
|
|
106
180
|
}
|
|
107
181
|
return { version: 1, tools };
|
|
108
182
|
}
|
|
183
|
+
const VERDICTS = {
|
|
184
|
+
read: "read-only",
|
|
185
|
+
write: "additive",
|
|
186
|
+
destructive: "destructive",
|
|
187
|
+
};
|
|
188
|
+
const PUBLIC_VERDICTS = {
|
|
189
|
+
"read-only": "read",
|
|
190
|
+
additive: "write",
|
|
191
|
+
destructive: "destructive",
|
|
192
|
+
};
|
|
193
|
+
const SCHEMA_DIGEST = /^sha256:[0-9a-f]{64}$/;
|
|
194
|
+
const REVIEWED_TOOL_KEYS = new Set(["verdict", "reason", "schemaDigest"]);
|
|
195
|
+
function isRecord(value) {
|
|
196
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
197
|
+
}
|
|
109
198
|
/**
|
|
110
|
-
*
|
|
199
|
+
* Validate a public {@link ToolClassification} once, reading every value a
|
|
200
|
+
* single time, into the manifest the registry classifies with and a
|
|
201
|
+
* deep-frozen copy of the record.
|
|
111
202
|
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
203
|
+
* Every structural mistake throws, naming `owner`, because a classification
|
|
204
|
+
* that half-applies is worse than one that refuses to boot (INV-11). Unknown
|
|
205
|
+
* keys throw too: a misspelled `verdict` must not quietly leave a write
|
|
206
|
+
* unclassified.
|
|
207
|
+
*/
|
|
208
|
+
function parseClassification(classification, owner) {
|
|
209
|
+
function fail(detail) {
|
|
210
|
+
throw new Error(`[connecta] ${owner} classify ${detail}`);
|
|
211
|
+
}
|
|
212
|
+
if (!isRecord(classification))
|
|
213
|
+
fail("must be an object with a tools record.");
|
|
214
|
+
for (const key of Object.keys(classification)) {
|
|
215
|
+
if (key !== "tools" && key !== "unlisted")
|
|
216
|
+
fail(`has unknown key "${key}"; only "tools" and "unlisted" are accepted.`);
|
|
217
|
+
}
|
|
218
|
+
const unlisted = classification.unlisted;
|
|
219
|
+
if (unlisted !== undefined && unlisted !== "hide")
|
|
220
|
+
fail('unlisted must be "hide" when set.');
|
|
221
|
+
const input = classification.tools;
|
|
222
|
+
if (!isRecord(input))
|
|
223
|
+
fail("tools must be a record of tool name to verdict.");
|
|
224
|
+
const tools = new Map();
|
|
225
|
+
const frozen = {};
|
|
226
|
+
for (const [name, entry] of Object.entries(input)) {
|
|
227
|
+
if (!name || name.trim() !== name) {
|
|
228
|
+
fail(`tool name "${name}" is empty or has surrounding whitespace.`);
|
|
229
|
+
}
|
|
230
|
+
const record = typeof entry === "string" ? { verdict: entry } : isRecord(entry) ? { ...entry } : {};
|
|
231
|
+
for (const key of Object.keys(record)) {
|
|
232
|
+
if (!REVIEWED_TOOL_KEYS.has(key))
|
|
233
|
+
fail(`tool "${name}" has unknown key "${key}".`);
|
|
234
|
+
}
|
|
235
|
+
const { verdict: publicVerdict, reason, schemaDigest: digest } = record;
|
|
236
|
+
const verdict = typeof publicVerdict === "string" && Object.hasOwn(VERDICTS, publicVerdict) ? VERDICTS[publicVerdict] : undefined;
|
|
237
|
+
if (verdict === undefined) {
|
|
238
|
+
fail(`tool "${name}" needs verdict "read", "write", or "destructive".`);
|
|
239
|
+
}
|
|
240
|
+
if (reason !== undefined && (typeof reason !== "string" || !reason.trim())) {
|
|
241
|
+
fail(`tool "${name}" reason must be a non-empty string.`);
|
|
242
|
+
}
|
|
243
|
+
if (digest !== undefined && (typeof digest !== "string" || !SCHEMA_DIGEST.test(digest))) {
|
|
244
|
+
fail(`tool "${name}" schemaDigest must be "sha256:" and 64 lowercase hex digits.`);
|
|
245
|
+
}
|
|
246
|
+
tools.set(name, {
|
|
247
|
+
verdict,
|
|
248
|
+
...(typeof digest === "string" ? { schemaDigest: digest } : {}),
|
|
249
|
+
});
|
|
250
|
+
frozen[name] =
|
|
251
|
+
typeof entry === "string"
|
|
252
|
+
? PUBLIC_VERDICTS[verdict]
|
|
253
|
+
: Object.freeze({
|
|
254
|
+
verdict: PUBLIC_VERDICTS[verdict],
|
|
255
|
+
...(typeof reason === "string" ? { reason } : {}),
|
|
256
|
+
...(typeof digest === "string" ? { schemaDigest: digest } : {}),
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
catalog: { version: 1, tools, ...(unlisted ? { unlisted } : {}) },
|
|
261
|
+
frozen: Object.freeze({ tools: Object.freeze(frozen), ...(unlisted ? { unlisted } : {}) }),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
/** Validate a {@link ToolClassification} and build the manifest it describes. */
|
|
265
|
+
export function reviewedCatalog(classification, owner) {
|
|
266
|
+
return parseClassification(classification, owner).catalog;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Validate a {@link ToolClassification} and return a deep-frozen copy of it:
|
|
270
|
+
* what a connector carries as `classification`, so neither the caller's
|
|
271
|
+
* original object nor a write through the connector can change a verdict
|
|
272
|
+
* after review.
|
|
273
|
+
*/
|
|
274
|
+
export function reviewedClassification(classification, owner) {
|
|
275
|
+
return parseClassification(classification, owner).frozen;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* One reviewed tool as served: a fresh object, never one a connector, a
|
|
279
|
+
* cache, or an earlier read holds.
|
|
280
|
+
*
|
|
281
|
+
* A listed read fills downstream silence, and an explicit write annotation on
|
|
282
|
+
* it wins: `destructiveHint: true` or `readOnlyHint: false` on a classified
|
|
283
|
+
* read is the downstream telling us the review is stale. A listed write or
|
|
284
|
+
* destructive tool stays a write whatever the downstream claims; there
|
|
285
|
+
* connecta knows what the tool does, and a claim to the contrary is a
|
|
286
|
+
* downstream bug rather than news
|
|
120
287
|
* ([#310](https://github.com/zackbart/connecta/issues/310),
|
|
121
|
-
* [#315](https://github.com/zackbart/connecta/issues/315)).
|
|
288
|
+
* [#315](https://github.com/zackbart/connecta/issues/315)). A tool no review
|
|
289
|
+
* lists keeps only an explicit read annotation, the downstream's own word
|
|
290
|
+
* being the only evidence there is. A reviewed tool whose recorded schema
|
|
291
|
+
* digest the live tool no longer matches, or could not be checked against,
|
|
292
|
+
* keeps no reviewed verdict and is a write.
|
|
122
293
|
*/
|
|
123
|
-
function
|
|
294
|
+
function servedTool(catalog, fact, lapsed, override) {
|
|
295
|
+
const definition = structuredClone(fact);
|
|
296
|
+
const reviewed = catalog.tools.get(fact.name);
|
|
297
|
+
definition.classification = classifyTool(fact, override, reviewed
|
|
298
|
+
? {
|
|
299
|
+
verdict: PUBLIC_VERDICTS[reviewed.verdict],
|
|
300
|
+
stale: lapsed.has(fact.name),
|
|
301
|
+
}
|
|
302
|
+
: undefined);
|
|
124
303
|
const downstream = definition.annotations ?? {};
|
|
125
304
|
const record = catalog.tools.get(definition.name);
|
|
305
|
+
const annotate = (annotations) => ({
|
|
306
|
+
...definition,
|
|
307
|
+
annotations: override === undefined
|
|
308
|
+
? annotations
|
|
309
|
+
: { ...annotations, readOnlyHint: override === "read", destructiveHint: override === "write" },
|
|
310
|
+
});
|
|
311
|
+
if (record?.verdict === "destructive") {
|
|
312
|
+
return annotate({ ...downstream, readOnlyHint: false, destructiveHint: true });
|
|
313
|
+
}
|
|
314
|
+
if (record && lapsed.has(definition.name)) {
|
|
315
|
+
// A review vouches for the schema it read. When the live schema no longer
|
|
316
|
+
// matches that digest, or the digest could not be checked, the verdict is
|
|
317
|
+
// about some other tool: the read becomes a write until a release reviews
|
|
318
|
+
// it again (INV-1).
|
|
319
|
+
return annotate({ ...downstream, readOnlyHint: false });
|
|
320
|
+
}
|
|
126
321
|
if (record?.verdict === "read-only") {
|
|
127
|
-
if (downstream.destructiveHint === true ||
|
|
128
|
-
downstream
|
|
129
|
-
return definition;
|
|
322
|
+
if (downstream.destructiveHint === true || downstream.readOnlyHint === false) {
|
|
323
|
+
return annotate(downstream);
|
|
130
324
|
}
|
|
131
|
-
return {
|
|
132
|
-
...
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
destructiveHint: downstream.destructiveHint ?? false,
|
|
137
|
-
},
|
|
138
|
-
};
|
|
325
|
+
return annotate({
|
|
326
|
+
...downstream,
|
|
327
|
+
readOnlyHint: true,
|
|
328
|
+
destructiveHint: downstream.destructiveHint ?? false,
|
|
329
|
+
});
|
|
139
330
|
}
|
|
140
|
-
if (record?.verdict === "
|
|
141
|
-
return {
|
|
142
|
-
...definition,
|
|
143
|
-
annotations: {
|
|
144
|
-
...downstream,
|
|
145
|
-
readOnlyHint: false,
|
|
146
|
-
destructiveHint: true,
|
|
147
|
-
},
|
|
148
|
-
};
|
|
331
|
+
if (record?.verdict === "additive") {
|
|
332
|
+
return annotate({ ...downstream, readOnlyHint: false });
|
|
149
333
|
}
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
// read-only keeps that annotation — on a name no release has reviewed, the
|
|
155
|
-
// downstream's own word is the only evidence there is, and rewriting it
|
|
156
|
-
// would be an overrule rather than a fill-in.
|
|
157
|
-
return {
|
|
158
|
-
...definition,
|
|
159
|
-
annotations: {
|
|
160
|
-
...downstream,
|
|
161
|
-
readOnlyHint: downstream.readOnlyHint ?? false,
|
|
162
|
-
},
|
|
163
|
-
};
|
|
334
|
+
// Fill-in only: a silent tool is not read-only, so drift fails closed onto
|
|
335
|
+
// `call_destructive_tool`, and it gets no `destructiveHint` it has not
|
|
336
|
+
// earned. An explicit read claim on a name no review lists is believed.
|
|
337
|
+
return annotate({ ...downstream, readOnlyHint: downstream.readOnlyHint ?? false });
|
|
164
338
|
}
|
|
165
339
|
/**
|
|
166
340
|
* Whether the downstream's own annotation contradicts what a release reviewed.
|
|
@@ -172,22 +346,35 @@ function applyVettedSafety(catalog, definition) {
|
|
|
172
346
|
function contradicts(record, definition) {
|
|
173
347
|
const downstream = definition.annotations ?? {};
|
|
174
348
|
if (record.verdict === "read-only") {
|
|
175
|
-
return
|
|
349
|
+
return downstream.readOnlyHint === false || downstream.destructiveHint === true;
|
|
176
350
|
}
|
|
177
351
|
return downstream.readOnlyHint === true;
|
|
178
352
|
}
|
|
179
353
|
/**
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
* strip one. Reads nothing but the tools it was handed — the caller already
|
|
185
|
-
* fetched them to serve a request, and this function never fetches anything.
|
|
354
|
+
* Names of served tools whose schemas no longer match the digest a release
|
|
355
|
+
* recorded for them. A manifest that recorded no digest for a tool cannot have
|
|
356
|
+
* an opinion about its schema, so it does not pay for a hash either. Throws
|
|
357
|
+
* when a digest cannot be computed; the caller decides what that means.
|
|
186
358
|
*/
|
|
187
|
-
|
|
359
|
+
async function changedSchemas(catalog, tools) {
|
|
360
|
+
const changed = new Set();
|
|
361
|
+
for (const definition of tools) {
|
|
362
|
+
const recorded = catalog.tools.get(definition.name)?.schemaDigest;
|
|
363
|
+
if (recorded !== undefined && recorded !== (await vettedSchemaDigest(definition))) {
|
|
364
|
+
changed.add(definition.name);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return changed;
|
|
368
|
+
}
|
|
369
|
+
/** Every served tool whose review recorded a digest: what cannot be verified. */
|
|
370
|
+
function digestedTools(catalog, tools) {
|
|
371
|
+
return new Set(tools
|
|
372
|
+
.filter((definition) => catalog.tools.get(definition.name)?.schemaDigest !== undefined)
|
|
373
|
+
.map((definition) => definition.name));
|
|
374
|
+
}
|
|
375
|
+
function countDrift(catalog, tools, changed) {
|
|
188
376
|
let unclassifiedTools = 0;
|
|
189
377
|
let annotationConflicts = 0;
|
|
190
|
-
let schemaChanges = 0;
|
|
191
378
|
const served = new Set();
|
|
192
379
|
for (const definition of tools) {
|
|
193
380
|
served.add(definition.name);
|
|
@@ -198,12 +385,6 @@ export async function detectCatalogDrift(catalog, tools) {
|
|
|
198
385
|
}
|
|
199
386
|
if (contradicts(record, definition))
|
|
200
387
|
annotationConflicts += 1;
|
|
201
|
-
// A manifest that recorded no digest for this tool cannot have an opinion
|
|
202
|
-
// about its schema, so it does not pay for a hash either.
|
|
203
|
-
if (record.schemaDigest !== undefined &&
|
|
204
|
-
record.schemaDigest !== (await vettedSchemaDigest(definition))) {
|
|
205
|
-
schemaChanges += 1;
|
|
206
|
-
}
|
|
207
388
|
}
|
|
208
389
|
let unservedTools = 0;
|
|
209
390
|
for (const name of catalog.tools.keys()) {
|
|
@@ -214,43 +395,99 @@ export async function detectCatalogDrift(catalog, tools) {
|
|
|
214
395
|
unclassifiedTools,
|
|
215
396
|
unservedTools,
|
|
216
397
|
annotationConflicts,
|
|
217
|
-
schemaChanges,
|
|
398
|
+
schemaChanges: changed.size,
|
|
218
399
|
};
|
|
219
400
|
}
|
|
220
401
|
/**
|
|
221
|
-
*
|
|
222
|
-
* catalog is normalized with, and the drift check that rides the same listing.
|
|
402
|
+
* Compare a live catalog with the manifest and count what moved.
|
|
223
403
|
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
|
|
404
|
+
* Counts only, and by construction: there is nowhere here to put a tool name,
|
|
405
|
+
* a schema, or a downstream string, so no later surface has to remember to
|
|
406
|
+
* strip one. Reads nothing but the tools it was handed — the caller already
|
|
407
|
+
* fetched them to serve a request, and this function never fetches anything.
|
|
408
|
+
*/
|
|
409
|
+
export async function detectCatalogDrift(catalog, tools) {
|
|
410
|
+
return countDrift(catalog, tools, await changedSchemas(catalog, tools));
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Each connector object's review, validated the first time a registry reads
|
|
414
|
+
* it. Read once per object, so a getter or a record mutated later cannot
|
|
415
|
+
* hand a later registry (a principal's, say) a different review.
|
|
416
|
+
*/
|
|
417
|
+
const reviews = new WeakMap();
|
|
418
|
+
/**
|
|
419
|
+
* The review `connector.classification` declares, or undefined when it
|
|
420
|
+
* declares none. Throws on a malformed one (INV-11).
|
|
421
|
+
*/
|
|
422
|
+
export function catalogReviewOf(connector) {
|
|
423
|
+
let review = reviews.get(connector);
|
|
424
|
+
if (review === undefined) {
|
|
425
|
+
const classification = connector.classification;
|
|
426
|
+
review = classification === undefined ? null : reviewedCatalog(classification, `connector "${connector.id}"`);
|
|
427
|
+
reviews.set(connector, review);
|
|
428
|
+
}
|
|
429
|
+
return review ?? undefined;
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Classify downstream facts with a review: what the registry serves on every
|
|
433
|
+
* read, from whichever cache layer the facts came from.
|
|
434
|
+
*
|
|
435
|
+
* Every tool returned is a fresh object, so nothing a caller does to one can
|
|
436
|
+
* reach the facts, `memo`, or a later read. `memo` holds digest verification
|
|
437
|
+
* per facts array; the registry passes one only for deep-frozen arrays it
|
|
438
|
+
* owns. A failed verification is not kept, so the next read tries again, and
|
|
439
|
+
* meanwhile no digested review vouches for a read (INV-1).
|
|
440
|
+
*/
|
|
441
|
+
export async function classifyCatalog(catalog, connectorId, facts, logger, memo, overrides) {
|
|
442
|
+
let lapsed = memo?.get(facts);
|
|
443
|
+
if (!lapsed) {
|
|
444
|
+
try {
|
|
445
|
+
lapsed = await changedSchemas(catalog, facts);
|
|
446
|
+
memo?.set(facts, lapsed);
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
lapsed = digestedTools(catalog, facts);
|
|
450
|
+
logFailure(logger, "schema digest check failed; serving digested reviews as writes", failureRecord({ connector: connectorId }, error));
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
const unverified = lapsed;
|
|
454
|
+
return facts
|
|
455
|
+
.filter((fact) => catalog.unlisted !== "hide" || catalog.tools.has(fact.name))
|
|
456
|
+
.map((fact) => servedTool(catalog, fact, unverified, overrides?.[fact.name]));
|
|
457
|
+
}
|
|
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
|
+
*/
|
|
463
|
+
const observations = new WeakMap();
|
|
464
|
+
/**
|
|
465
|
+
* Compare a listing a refresh just received with the connector's review, and
|
|
466
|
+
* keep the counts as its latest observation.
|
|
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),
|
|
229
473
|
* [#343](https://github.com/zackbart/connecta/issues/343)).
|
|
230
474
|
*/
|
|
231
|
-
export function
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
return downstream.map((definition) => applyVettedSafety(catalog, definition));
|
|
251
|
-
},
|
|
252
|
-
catalogDrift() {
|
|
253
|
-
return observed;
|
|
254
|
-
},
|
|
255
|
-
};
|
|
475
|
+
export async function observeReviewedDrift(connector, catalog, listed, logger) {
|
|
476
|
+
try {
|
|
477
|
+
const changed = await changedSchemas(catalog, listed);
|
|
478
|
+
observations.set(connector, {
|
|
479
|
+
observedAt: new Date().toISOString(),
|
|
480
|
+
...countDrift(catalog, listed, changed),
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
// A drift check is a report about a catalog, never a condition for
|
|
485
|
+
// serving one. Keep the last good observation rather than replacing it
|
|
486
|
+
// with a lie, and let the refresh through; classification fails closed.
|
|
487
|
+
logFailure(logger, "catalog drift check failed", failureRecord({ connector: connector.id }, error));
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
/** The latest drift {@link observeReviewedDrift} kept for `connector`. */
|
|
491
|
+
export function observedCatalogDrift(connector) {
|
|
492
|
+
return observations.get(connector);
|
|
256
493
|
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
const encoder = new TextEncoder();
|
|
2
2
|
async function fingerprintBytes(bytes) {
|
|
3
|
-
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
|
|
4
|
-
const hex = [...digest]
|
|
5
|
-
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
6
|
-
.join("");
|
|
3
|
+
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new Uint8Array(bytes)));
|
|
4
|
+
const hex = [...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
7
5
|
return `sha256:${bytes.byteLength}:${hex}`;
|
|
8
6
|
}
|
|
9
7
|
/** Fingerprint an already serialized catalog without parsing/reserializing it. */
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Host-only provenance follows catalog copies, never connector definitions or
|
|
2
|
+
// serialized listings. Unknown provenance cannot authorize post-entry recovery.
|
|
3
|
+
const deadlines = new WeakMap();
|
|
4
|
+
export function markCatalogFreshness(tools, freshUntil) {
|
|
5
|
+
deadlines.set(tools, freshUntil);
|
|
6
|
+
return tools;
|
|
7
|
+
}
|
|
8
|
+
export function carryCatalogFreshness(source, copy) {
|
|
9
|
+
return markCatalogFreshness(copy, deadlines.get(source) ?? 0);
|
|
10
|
+
}
|
|
11
|
+
export function catalogIsFresh(tools) {
|
|
12
|
+
return (deadlines.get(tools) ?? 0) > Date.now();
|
|
13
|
+
}
|
package/dist/catalog-limits.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/** Complete-catalog ceiling shared by dynamically listed connector catalogs. */
|
|
2
2
|
export const MAX_CATALOG_TOOLS = 100_000;
|
|
3
3
|
/**
|
|
4
|
-
* Large enough to
|
|
5
|
-
*
|
|
4
|
+
* Large enough to span many stored chunks while keeping the serialized form
|
|
5
|
+
* at one quarter of a 128 MiB Worker heap.
|
|
6
6
|
*/
|
|
7
7
|
export const MAX_SERIALIZED_CATALOG_BYTES = 32 * 1024 * 1024;
|
|
8
8
|
/** Persisted values stay comfortably below platform per-value limits. */
|