@zackbart/connecta 0.18.2 → 0.19.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 +100 -4
- package/README.md +4 -0
- package/dist/catalog-service.d.ts +20 -13
- package/dist/catalog-service.js +123 -116
- package/dist/catalog.js +29 -46
- package/dist/connector-scope.js +2 -7
- package/dist/connectors/api.d.ts +4 -16
- package/dist/connectors/api.js +19 -46
- package/dist/connectors/guarded-fetch.d.ts +9 -23
- package/dist/connectors/guarded-fetch.js +38 -76
- package/dist/connectors/remote-mcp.js +36 -79
- package/dist/errors.d.ts +6 -27
- package/dist/errors.js +8 -5
- package/dist/execute.d.ts +24 -22
- package/dist/execute.js +98 -145
- package/dist/executor-result.d.ts +1 -0
- package/dist/executor-result.js +4 -11
- package/dist/executors/quickjs-child.js +1 -3
- package/dist/executors/quickjs-runtime.js +1 -3
- package/dist/executors/quickjs.js +1 -3
- package/dist/index.js +27 -57
- package/dist/invocation.js +114 -178
- package/dist/meta-tools.d.ts +15 -28
- package/dist/meta-tools.js +33 -89
- package/dist/providers/cloudflare.d.ts +2 -18
- package/dist/providers/cloudflare.js +1460 -2451
- package/dist/providers/linear.d.ts +4 -41
- package/dist/providers/linear.js +8 -39
- package/dist/providers/mixpanel.d.ts +3 -25
- package/dist/providers/mixpanel.js +7 -22
- package/dist/providers/notion.d.ts +1 -15
- package/dist/providers/notion.js +44 -173
- package/dist/providers/revenuecat.d.ts +4 -57
- package/dist/providers/revenuecat.js +10 -93
- package/dist/providers/stripe.d.ts +1 -12
- package/dist/providers/stripe.js +7 -45
- package/dist/registry.d.ts +16 -34
- package/dist/registry.js +18 -103
- package/dist/result-shapes.d.ts +13 -0
- package/dist/result-shapes.js +331 -0
- package/dist/routes/mcp.js +1 -1
- package/dist/routes/oauth.js +3 -3
- package/dist/routes/shared.d.ts +15 -15
- package/dist/routes/shared.js +1 -3
- package/dist/skills.js +3 -3
- package/dist/timeout.d.ts +8 -7
- package/dist/timeout.js +47 -38
- package/dist/types.d.ts +3 -3
- package/dist/ui.d.ts +1 -25
- package/dist/ui.js +18 -45
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +5 -2
- package/documentation/call-admission.md +1 -1
- package/documentation/cloudflare.md +1 -1
- package/documentation/code-mode.md +13 -13
- package/documentation/connectors.md +34 -1
- package/documentation/linear.md +1 -1
- package/documentation/meta-tools.md +17 -3
- package/documentation/mixpanel.md +1 -1
- package/documentation/notion.md +1 -1
- package/documentation/operations.md +20 -15
- package/documentation/provider-conventions.md +1 -1
- package/documentation/revenuecat.md +1 -1
- package/documentation/stripe.md +1 -1
- package/documentation/upgrading.md +24 -4
- package/ethos.md +74 -120
- package/package.json +3 -4
- package/templates/node/package.json +1 -1
- package/documentation/code-first-exploration.md +0 -292
- package/documentation/mcp-2026-07-28.md +0 -46
- package/documentation/mcp-ui-design.md +0 -382
- package/documentation/program-ui-read-calls.md +0 -213
- package/documentation/provider-audit.md +0 -198
- package/documentation/rich-output-design.md +0 -211
package/dist/registry.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { closeConnectorScope, } from "./connector-scope.js";
|
|
2
2
|
import { recordCatalogDriftActivity, } from "./activity.js";
|
|
3
3
|
import { storedCredentialShape, } from "./credentials.js";
|
|
4
|
-
import { ConnectorCallError } from "./errors.js";
|
|
4
|
+
import { ConnectorCallError, msg } from "./errors.js";
|
|
5
5
|
import { ConnectorCallAdmissionController, } from "./call-admission.js";
|
|
6
6
|
import { boundedCatalogDrift } from "./catalog-drift.js";
|
|
7
7
|
import { fingerprintSerializedCatalog, snapshotCatalog, } from "./catalog-fingerprint.js";
|
|
8
8
|
import { MAX_CATALOG_CHUNK_BYTES, MAX_CATALOG_TOOLS, MAX_SERIALIZED_CATALOG_BYTES, } from "./catalog-limits.js";
|
|
9
9
|
import { mapSettledWithConcurrency } from "./concurrency.js";
|
|
10
|
+
import { ObservedOutputSchemas } from "./result-shapes.js";
|
|
10
11
|
import { GUIDE_SUMMARY_LENGTH, normalizeGuideSummary, } from "./skills.js";
|
|
11
|
-
import {
|
|
12
|
+
import { withDeadline } from "./timeout.js";
|
|
12
13
|
const ID_RE = /^[a-z0-9_-]+$/;
|
|
13
14
|
const DEFAULT_TTL_SECONDS = 300;
|
|
14
15
|
const DEFAULT_STALE_SECONDS = 3600;
|
|
@@ -67,37 +68,6 @@ export function resolveMaxResultBytes(value, inherited) {
|
|
|
67
68
|
: inherited;
|
|
68
69
|
}
|
|
69
70
|
const CATALOG_CHUNK_IO_CONCURRENCY = 4;
|
|
70
|
-
/**
|
|
71
|
-
* Recent real-call outcomes per connector.
|
|
72
|
-
*/
|
|
73
|
-
class HealthLog {
|
|
74
|
-
observations = new Map();
|
|
75
|
-
recordSuccess(id, latencyMs) {
|
|
76
|
-
const previous = this.observations.get(id);
|
|
77
|
-
const observation = {
|
|
78
|
-
...previous,
|
|
79
|
-
lastSuccessAt: new Date().toISOString(),
|
|
80
|
-
lastLatencyMs: latencyMs,
|
|
81
|
-
consecutiveFailures: 0,
|
|
82
|
-
};
|
|
83
|
-
delete observation.lastError;
|
|
84
|
-
this.observations.set(id, observation);
|
|
85
|
-
}
|
|
86
|
-
recordFailure(id, latencyMs, error) {
|
|
87
|
-
const previous = this.observations.get(id);
|
|
88
|
-
this.observations.set(id, {
|
|
89
|
-
...previous,
|
|
90
|
-
lastFailureAt: new Date().toISOString(),
|
|
91
|
-
lastLatencyMs: latencyMs,
|
|
92
|
-
consecutiveFailures: (previous?.consecutiveFailures ?? 0) + 1,
|
|
93
|
-
lastError: msg(error),
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
get(id) {
|
|
97
|
-
const observation = this.observations.get(id);
|
|
98
|
-
return observation ? { ...observation } : undefined;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
71
|
function namespaced(storage, prefix) {
|
|
102
72
|
return {
|
|
103
73
|
get: (k) => storage.get(prefix + k),
|
|
@@ -105,9 +75,6 @@ function namespaced(storage, prefix) {
|
|
|
105
75
|
delete: (k) => storage.delete(prefix + k),
|
|
106
76
|
};
|
|
107
77
|
}
|
|
108
|
-
function msg(err) {
|
|
109
|
-
return err instanceof Error ? err.message : String(err);
|
|
110
|
-
}
|
|
111
78
|
/**
|
|
112
79
|
* Holds the connector set, resolves addresses, and caches per-connector tool
|
|
113
80
|
* lists in memory with a TTL. Connector failures are isolated: a broken
|
|
@@ -123,16 +90,16 @@ export class Registry {
|
|
|
123
90
|
catalogGenerations = new Map();
|
|
124
91
|
/** Serialize persisted catalog set/delete operations within this isolate. */
|
|
125
92
|
catalogMutations = new Map();
|
|
126
|
-
/** Same-request cold loads share one promise without retaining the request.
|
|
93
|
+
/** Same-request cold loads share one promise without retaining the request.
|
|
94
|
+
* See documentation/architecture.md#the-two-lifetimes. */
|
|
127
95
|
requestCatalogLoads = new WeakMap();
|
|
128
96
|
/** One live refresh per connector across agent and operator requests. */
|
|
129
97
|
catalogRefreshes = new Map();
|
|
130
98
|
/** Last payload-free agent catalog access in this runtime. */
|
|
131
99
|
catalogAccess = new Map();
|
|
132
|
-
/** Deployment-wide observations — every call, whatever view made it. */
|
|
133
|
-
health = new HealthLog();
|
|
134
100
|
/** Last drift counts reported to activity, per connector, in this runtime. */
|
|
135
101
|
reportedDrift = new Map();
|
|
102
|
+
observedOutputSchemas;
|
|
136
103
|
ttlMs;
|
|
137
104
|
staleMs;
|
|
138
105
|
persistToolCatalog;
|
|
@@ -140,6 +107,7 @@ export class Registry {
|
|
|
140
107
|
maxResultBytes;
|
|
141
108
|
constructor(connectors, opts) {
|
|
142
109
|
this.opts = opts;
|
|
110
|
+
this.observedOutputSchemas = new ObservedOutputSchemas();
|
|
143
111
|
this.ttlMs =
|
|
144
112
|
(opts.toolCacheTtlSeconds ?? DEFAULT_TTL_SECONDS) * 1000;
|
|
145
113
|
this.staleMs =
|
|
@@ -342,6 +310,12 @@ export class Registry {
|
|
|
342
310
|
resultsStorage() {
|
|
343
311
|
return namespaced(this.opts.storage, "results:");
|
|
344
312
|
}
|
|
313
|
+
observedOutputSchema(connectorId, definition) {
|
|
314
|
+
return this.observedOutputSchemas.get(connectorId, definition);
|
|
315
|
+
}
|
|
316
|
+
observeOutputShape(connectorId, definition, value) {
|
|
317
|
+
this.observedOutputSchemas.observe(connectorId, definition, value);
|
|
318
|
+
}
|
|
345
319
|
/** Resolve "<connectorId>.<toolName>" → connector + tool name. */
|
|
346
320
|
resolveAddress(address) {
|
|
347
321
|
const parts = splitAddress(address);
|
|
@@ -358,21 +332,6 @@ export class Registry {
|
|
|
358
332
|
catalogChunkKey(id, revision, index) {
|
|
359
333
|
return `${this.catalogKey(id)}:chunk:${revision}:${index}`;
|
|
360
334
|
}
|
|
361
|
-
validLegacyCatalog(value) {
|
|
362
|
-
if (!value || typeof value !== "object")
|
|
363
|
-
return null;
|
|
364
|
-
const catalog = value;
|
|
365
|
-
if (!Array.isArray(catalog.tools) ||
|
|
366
|
-
typeof catalog.fetchedAt !== "number" ||
|
|
367
|
-
typeof catalog.expiresAt !== "number" ||
|
|
368
|
-
typeof catalog.staleUntil !== "number" ||
|
|
369
|
-
(catalog.fingerprint !== undefined &&
|
|
370
|
-
typeof catalog.fingerprint !== "string") ||
|
|
371
|
-
!this.validCatalogTools(catalog.tools)) {
|
|
372
|
-
return null;
|
|
373
|
-
}
|
|
374
|
-
return catalog;
|
|
375
|
-
}
|
|
376
335
|
validCatalogTools(value) {
|
|
377
336
|
return value.every((tool) => tool !== null &&
|
|
378
337
|
typeof tool === "object" &&
|
|
@@ -443,28 +402,6 @@ export class Registry {
|
|
|
443
402
|
catch {
|
|
444
403
|
return null;
|
|
445
404
|
}
|
|
446
|
-
if (!parsed ||
|
|
447
|
-
typeof parsed !== "object" ||
|
|
448
|
-
parsed.version !== 2) {
|
|
449
|
-
const legacy = this.validLegacyCatalog(parsed);
|
|
450
|
-
if (!legacy || legacy.staleUntil <= now)
|
|
451
|
-
return null;
|
|
452
|
-
const snapshot = await snapshotCatalog(legacy.tools);
|
|
453
|
-
if (legacy.tools.length > MAX_CATALOG_TOOLS ||
|
|
454
|
-
snapshot.serializedBytes.byteLength > MAX_SERIALIZED_CATALOG_BYTES ||
|
|
455
|
-
(legacy.fingerprint !== undefined &&
|
|
456
|
-
legacy.fingerprint !== snapshot.fingerprint)) {
|
|
457
|
-
this.opts.logger.warn(`[connecta] connector "${id}" legacy catalog is oversized or has a fingerprint mismatch; ignoring persisted catalog.`);
|
|
458
|
-
return null;
|
|
459
|
-
}
|
|
460
|
-
return {
|
|
461
|
-
tools: legacy.tools,
|
|
462
|
-
fingerprint: snapshot.fingerprint,
|
|
463
|
-
fetchedAt: legacy.fetchedAt,
|
|
464
|
-
expiresAt: legacy.expiresAt,
|
|
465
|
-
staleUntil: legacy.staleUntil,
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
405
|
const manifest = this.validCatalogManifest(parsed);
|
|
469
406
|
if (!manifest) {
|
|
470
407
|
this.opts.logger.warn(`[connecta] connector "${id}" catalog manifest is invalid; ignoring persisted catalog.`);
|
|
@@ -739,7 +676,7 @@ export class Registry {
|
|
|
739
676
|
if (!flight) {
|
|
740
677
|
const requestScope = {};
|
|
741
678
|
let ctx;
|
|
742
|
-
flight = this.startCatalogRefresh(id, expectedGeneration, () =>
|
|
679
|
+
flight = this.startCatalogRefresh(id, expectedGeneration, () => withDeadline((signal) => {
|
|
743
680
|
const current = this.cache.get(id);
|
|
744
681
|
if (current && current.exp > Date.now()) {
|
|
745
682
|
return Promise.resolve(current.tools);
|
|
@@ -753,7 +690,10 @@ export class Registry {
|
|
|
753
690
|
timeoutMs: options.refreshTimeoutMs,
|
|
754
691
|
});
|
|
755
692
|
return this.refreshToolsWithContext(id, connector, ctx, true);
|
|
756
|
-
},
|
|
693
|
+
}, {
|
|
694
|
+
timeoutMs: options.refreshTimeoutMs,
|
|
695
|
+
timeoutError: new Error(`deferred catalog refresh of "${id}" timed out after ${options.refreshTimeoutMs}ms`),
|
|
696
|
+
}), async () => {
|
|
757
697
|
if (ctx)
|
|
758
698
|
await closeConnectorScope(connector, ctx, options.defer);
|
|
759
699
|
});
|
|
@@ -912,31 +852,6 @@ export class Registry {
|
|
|
912
852
|
this.requestCatalogLoads.delete(requestScope);
|
|
913
853
|
}
|
|
914
854
|
}
|
|
915
|
-
/** Return a cached catalog without performing storage or network I/O. */
|
|
916
|
-
peekTools(id) {
|
|
917
|
-
const connector = this.connectors.get(id);
|
|
918
|
-
if (connector?.staticTools)
|
|
919
|
-
return connector.staticTools;
|
|
920
|
-
const hit = this.cache.get(id);
|
|
921
|
-
return hit && hit.staleUntil > Date.now() ? hit.tools : undefined;
|
|
922
|
-
}
|
|
923
|
-
recordSuccess(id, latencyMs) {
|
|
924
|
-
this.health.recordSuccess(id, latencyMs);
|
|
925
|
-
}
|
|
926
|
-
recordFailure(id, latencyMs, error) {
|
|
927
|
-
this.health.recordFailure(id, latencyMs, error);
|
|
928
|
-
}
|
|
929
|
-
healthFor(id) {
|
|
930
|
-
return this.health.get(id);
|
|
931
|
-
}
|
|
932
|
-
/** Whether this deployment has seen a successful call to `id`. */
|
|
933
|
-
hasObservedSuccess(id) {
|
|
934
|
-
return this.observedSuccessAt(id) !== undefined;
|
|
935
|
-
}
|
|
936
|
-
/** The timestamp behind `hasObservedSuccess`. */
|
|
937
|
-
observedSuccessAt(id) {
|
|
938
|
-
return this.health.get(id)?.lastSuccessAt;
|
|
939
|
-
}
|
|
940
855
|
async credentialDriftFor(id) {
|
|
941
856
|
const credential = this.connectors.get(id)?.credential;
|
|
942
857
|
const vault = this.opts.credentialVault;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { JsonSchema, ToolDef } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Passive, process-local output-shape learning. Provider declarations always
|
|
4
|
+
* win; observations are an open, optional-field routing aid and never a
|
|
5
|
+
* replacement contract.
|
|
6
|
+
*/
|
|
7
|
+
export declare class ObservedOutputSchemas {
|
|
8
|
+
private readonly entries;
|
|
9
|
+
private cacheKey;
|
|
10
|
+
private set;
|
|
11
|
+
get(connectorId: string, definition: ToolDef): JsonSchema | undefined;
|
|
12
|
+
observe(connectorId: string, definition: ToolDef, value: unknown): void;
|
|
13
|
+
}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { isExplicitlyReadOnly } from "./tool-safety.js";
|
|
2
|
+
const OBSERVATION_TTL_MS = 24 * 60 * 60 * 1000;
|
|
3
|
+
const MAX_CACHED_SHAPES = 256;
|
|
4
|
+
const MAX_DEFINITION_BYTES = 64 * 1024;
|
|
5
|
+
const MAX_SCHEMA_BYTES = 16 * 1024;
|
|
6
|
+
const MAX_SCHEMA_DEPTH = 6;
|
|
7
|
+
const MAX_SCHEMA_NODES = 128;
|
|
8
|
+
const MAX_OBJECT_PROPERTIES = 48;
|
|
9
|
+
const MAX_ARRAY_ITEMS = 32;
|
|
10
|
+
const MAX_PROPERTY_NAME_BYTES = 128;
|
|
11
|
+
const UNSAFE_PROPERTY_NAMES = new Set([
|
|
12
|
+
"__proto__",
|
|
13
|
+
"constructor",
|
|
14
|
+
"prototype",
|
|
15
|
+
]);
|
|
16
|
+
const encoder = new TextEncoder();
|
|
17
|
+
function broadType(value) {
|
|
18
|
+
if (value === null)
|
|
19
|
+
return "null";
|
|
20
|
+
if (Array.isArray(value))
|
|
21
|
+
return "array";
|
|
22
|
+
switch (typeof value) {
|
|
23
|
+
case "string":
|
|
24
|
+
case "boolean":
|
|
25
|
+
return typeof value;
|
|
26
|
+
case "number":
|
|
27
|
+
return Number.isFinite(value) ? "number" : undefined;
|
|
28
|
+
case "object":
|
|
29
|
+
return "object";
|
|
30
|
+
default:
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function serializedSchema(schema) {
|
|
35
|
+
try {
|
|
36
|
+
const text = JSON.stringify(schema);
|
|
37
|
+
return encoder.encode(text).byteLength <= MAX_SCHEMA_BYTES
|
|
38
|
+
? text
|
|
39
|
+
: undefined;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function cloneSchema(schema) {
|
|
46
|
+
const serialized = serializedSchema(schema);
|
|
47
|
+
if (!serialized)
|
|
48
|
+
return undefined;
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(serialized);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function definitionIdentity(definition) {
|
|
57
|
+
try {
|
|
58
|
+
const serialized = JSON.stringify(definition);
|
|
59
|
+
return encoder.encode(serialized).byteLength <= MAX_DEFINITION_BYTES
|
|
60
|
+
? serialized
|
|
61
|
+
: undefined;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function schemaType(schema) {
|
|
68
|
+
return typeof schema.type === "string" ? schema.type : undefined;
|
|
69
|
+
}
|
|
70
|
+
function safePropertyName(name) {
|
|
71
|
+
return (!UNSAFE_PROPERTY_NAMES.has(name) &&
|
|
72
|
+
encoder.encode(name).byteLength <= MAX_PROPERTY_NAME_BYTES);
|
|
73
|
+
}
|
|
74
|
+
function boundedPropertyNames(value) {
|
|
75
|
+
const names = [];
|
|
76
|
+
for (const name in value) {
|
|
77
|
+
if (!Object.hasOwn(value, name) || !safePropertyName(name))
|
|
78
|
+
continue;
|
|
79
|
+
names.push(name);
|
|
80
|
+
if (names.length >= MAX_OBJECT_PROPERTIES)
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
return names.sort();
|
|
84
|
+
}
|
|
85
|
+
function unionBranches(schema) {
|
|
86
|
+
return Array.isArray(schema.anyOf)
|
|
87
|
+
? schema.anyOf
|
|
88
|
+
: [schema];
|
|
89
|
+
}
|
|
90
|
+
function branchOrder(schema) {
|
|
91
|
+
return ["null", "boolean", "number", "string", "array", "object"].indexOf(schemaType(schema) ?? "");
|
|
92
|
+
}
|
|
93
|
+
function mergeSchemas(left, right) {
|
|
94
|
+
const leftType = schemaType(left);
|
|
95
|
+
const rightType = schemaType(right);
|
|
96
|
+
if (leftType === "object" && rightType === "object") {
|
|
97
|
+
const leftProperties = left.properties && typeof left.properties === "object"
|
|
98
|
+
? left.properties
|
|
99
|
+
: {};
|
|
100
|
+
const rightProperties = right.properties && typeof right.properties === "object"
|
|
101
|
+
? right.properties
|
|
102
|
+
: {};
|
|
103
|
+
const properties = Object.create(null);
|
|
104
|
+
for (const key of [...new Set([
|
|
105
|
+
...Object.keys(leftProperties),
|
|
106
|
+
...Object.keys(rightProperties),
|
|
107
|
+
])].sort()) {
|
|
108
|
+
const leftProperty = leftProperties[key];
|
|
109
|
+
const rightProperty = rightProperties[key];
|
|
110
|
+
properties[key] =
|
|
111
|
+
leftProperty && rightProperty
|
|
112
|
+
? mergeSchemas(leftProperty, rightProperty)
|
|
113
|
+
: (leftProperty ?? rightProperty);
|
|
114
|
+
}
|
|
115
|
+
return { type: "object", properties };
|
|
116
|
+
}
|
|
117
|
+
if (leftType === "array" && rightType === "array") {
|
|
118
|
+
const leftItems = left.items && typeof left.items === "object"
|
|
119
|
+
? left.items
|
|
120
|
+
: undefined;
|
|
121
|
+
const rightItems = right.items && typeof right.items === "object"
|
|
122
|
+
? right.items
|
|
123
|
+
: undefined;
|
|
124
|
+
return {
|
|
125
|
+
type: "array",
|
|
126
|
+
...(leftItems || rightItems
|
|
127
|
+
? {
|
|
128
|
+
items: leftItems && rightItems
|
|
129
|
+
? mergeSchemas(leftItems, rightItems)
|
|
130
|
+
: (leftItems ?? rightItems),
|
|
131
|
+
}
|
|
132
|
+
: {}),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (leftType && leftType === rightType)
|
|
136
|
+
return left;
|
|
137
|
+
const byType = new Map();
|
|
138
|
+
for (const branch of [...unionBranches(left), ...unionBranches(right)]) {
|
|
139
|
+
const type = schemaType(branch);
|
|
140
|
+
if (!type)
|
|
141
|
+
continue;
|
|
142
|
+
const existing = byType.get(type);
|
|
143
|
+
byType.set(type, existing ? mergeSchemas(existing, branch) : branch);
|
|
144
|
+
}
|
|
145
|
+
const branches = [...byType.values()].sort((a, b) => branchOrder(a) - branchOrder(b));
|
|
146
|
+
return branches.length === 1 ? branches[0] : { anyOf: branches };
|
|
147
|
+
}
|
|
148
|
+
function inferSchema(value, budget, depth = 0) {
|
|
149
|
+
const type = broadType(value);
|
|
150
|
+
if (!type || depth > MAX_SCHEMA_DEPTH || budget.nodes >= MAX_SCHEMA_NODES) {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
budget.nodes++;
|
|
154
|
+
if (type !== "object" && type !== "array")
|
|
155
|
+
return { type };
|
|
156
|
+
const object = value;
|
|
157
|
+
if (budget.seen.has(object))
|
|
158
|
+
return undefined;
|
|
159
|
+
budget.seen.add(object);
|
|
160
|
+
try {
|
|
161
|
+
if (type === "array") {
|
|
162
|
+
let items;
|
|
163
|
+
for (const item of value.slice(0, MAX_ARRAY_ITEMS)) {
|
|
164
|
+
const inferred = inferSchema(item, budget, depth + 1);
|
|
165
|
+
if (inferred)
|
|
166
|
+
items = items ? mergeSchemas(items, inferred) : inferred;
|
|
167
|
+
if (budget.nodes >= MAX_SCHEMA_NODES)
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
return { type: "array", ...(items ? { items } : {}) };
|
|
171
|
+
}
|
|
172
|
+
const properties = Object.create(null);
|
|
173
|
+
const record = value;
|
|
174
|
+
for (const key of boundedPropertyNames(record)) {
|
|
175
|
+
const inferred = inferSchema(record[key], budget, depth + 1);
|
|
176
|
+
if (inferred)
|
|
177
|
+
properties[key] = inferred;
|
|
178
|
+
if (budget.nodes >= MAX_SCHEMA_NODES)
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
return { type: "object", properties };
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
budget.seen.delete(object);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function boundSchema(schema, budget, depth = 0) {
|
|
188
|
+
if (depth > MAX_SCHEMA_DEPTH || budget.nodes >= MAX_SCHEMA_NODES) {
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
budget.nodes++;
|
|
192
|
+
if (Array.isArray(schema.anyOf)) {
|
|
193
|
+
const branches = schema.anyOf
|
|
194
|
+
.slice(0, 6)
|
|
195
|
+
.map((branch) => boundSchema(branch, budget, depth + 1))
|
|
196
|
+
.filter((branch) => Boolean(branch));
|
|
197
|
+
return branches.length >= 2 ? { anyOf: branches } : branches[0];
|
|
198
|
+
}
|
|
199
|
+
const type = schemaType(schema);
|
|
200
|
+
if (!type)
|
|
201
|
+
return undefined;
|
|
202
|
+
if (type === "array") {
|
|
203
|
+
const items = schema.items && typeof schema.items === "object"
|
|
204
|
+
? boundSchema(schema.items, budget, depth + 1)
|
|
205
|
+
: undefined;
|
|
206
|
+
return { type: "array", ...(items ? { items } : {}) };
|
|
207
|
+
}
|
|
208
|
+
if (type === "object") {
|
|
209
|
+
const source = schema.properties && typeof schema.properties === "object"
|
|
210
|
+
? schema.properties
|
|
211
|
+
: {};
|
|
212
|
+
const properties = Object.create(null);
|
|
213
|
+
let included = 0;
|
|
214
|
+
for (const key of Object.keys(source).sort()) {
|
|
215
|
+
if (included >= MAX_OBJECT_PROPERTIES ||
|
|
216
|
+
!safePropertyName(key)) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const property = boundSchema(source[key], budget, depth + 1);
|
|
220
|
+
if (property) {
|
|
221
|
+
properties[key] = property;
|
|
222
|
+
included++;
|
|
223
|
+
}
|
|
224
|
+
if (budget.nodes >= MAX_SCHEMA_NODES)
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
return { type: "object", properties };
|
|
228
|
+
}
|
|
229
|
+
return { type };
|
|
230
|
+
}
|
|
231
|
+
function observedSchema(value) {
|
|
232
|
+
try {
|
|
233
|
+
const inferred = inferSchema(value, { nodes: 0, seen: new WeakSet() });
|
|
234
|
+
const schema = inferred
|
|
235
|
+
? boundSchema(inferred, { nodes: 0 })
|
|
236
|
+
: undefined;
|
|
237
|
+
return schema && serializedSchema(schema) ? schema : undefined;
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Passive, process-local output-shape learning. Provider declarations always
|
|
245
|
+
* win; observations are an open, optional-field routing aid and never a
|
|
246
|
+
* replacement contract.
|
|
247
|
+
*/
|
|
248
|
+
export class ObservedOutputSchemas {
|
|
249
|
+
entries = new Map();
|
|
250
|
+
cacheKey(connectorId, definition) {
|
|
251
|
+
return JSON.stringify([connectorId, definition.name]);
|
|
252
|
+
}
|
|
253
|
+
set(key, entry) {
|
|
254
|
+
this.entries.delete(key);
|
|
255
|
+
this.entries.set(key, entry);
|
|
256
|
+
while (this.entries.size > MAX_CACHED_SHAPES) {
|
|
257
|
+
const oldest = this.entries.keys().next().value;
|
|
258
|
+
if (oldest === undefined)
|
|
259
|
+
break;
|
|
260
|
+
this.entries.delete(oldest);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
get(connectorId, definition) {
|
|
264
|
+
try {
|
|
265
|
+
const key = this.cacheKey(connectorId, definition);
|
|
266
|
+
if (definition.outputSchema) {
|
|
267
|
+
this.entries.delete(key);
|
|
268
|
+
return undefined;
|
|
269
|
+
}
|
|
270
|
+
if (!isExplicitlyReadOnly(definition)) {
|
|
271
|
+
this.entries.delete(key);
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
const identity = definitionIdentity(definition);
|
|
275
|
+
if (!identity) {
|
|
276
|
+
this.entries.delete(key);
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
const entry = this.entries.get(key);
|
|
280
|
+
if (!entry)
|
|
281
|
+
return undefined;
|
|
282
|
+
if (entry.definition !== identity || entry.expiresAt <= Date.now()) {
|
|
283
|
+
this.entries.delete(key);
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
this.entries.delete(key);
|
|
287
|
+
this.entries.set(key, entry);
|
|
288
|
+
return cloneSchema(entry.schema);
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
observe(connectorId, definition, value) {
|
|
295
|
+
try {
|
|
296
|
+
const key = this.cacheKey(connectorId, definition);
|
|
297
|
+
if (definition.outputSchema) {
|
|
298
|
+
this.entries.delete(key);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (!isExplicitlyReadOnly(definition)) {
|
|
302
|
+
this.entries.delete(key);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const identity = definitionIdentity(definition);
|
|
306
|
+
const inferred = observedSchema(value);
|
|
307
|
+
if (!identity) {
|
|
308
|
+
this.entries.delete(key);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (!inferred)
|
|
312
|
+
return;
|
|
313
|
+
const current = this.entries.get(key);
|
|
314
|
+
const merged = boundSchema(current &&
|
|
315
|
+
current.definition === identity &&
|
|
316
|
+
current.expiresAt > Date.now()
|
|
317
|
+
? mergeSchemas(current.schema, inferred)
|
|
318
|
+
: inferred, { nodes: 0 });
|
|
319
|
+
if (!merged || !serializedSchema(merged))
|
|
320
|
+
return;
|
|
321
|
+
this.set(key, {
|
|
322
|
+
definition: identity,
|
|
323
|
+
expiresAt: Date.now() + OBSERVATION_TTL_MS,
|
|
324
|
+
schema: merged,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
// Observation is an optimization. A provider success stays successful.
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
package/dist/routes/mcp.js
CHANGED
|
@@ -186,7 +186,7 @@ async function serveMcp(request, opts, baseUrl, actor, registry, runtimeContext)
|
|
|
186
186
|
// it — without this line no host reads execute_code's _meta.ui, no host
|
|
187
187
|
// fetches the shell, and the whole design is inert. This is the one
|
|
188
188
|
// extension connecta advertises; the versioned extensions framework
|
|
189
|
-
// stays declined as a general surface (
|
|
189
|
+
// stays declined as a general surface (https://github.com/zackbart/connecta/blob/main/records/mcp-2026-07-28.md).
|
|
190
190
|
capabilities: {
|
|
191
191
|
extensions: {
|
|
192
192
|
[MCP_APPS_EXTENSION]: { mimeTypes: [PROGRAM_UI_MIME_TYPE] },
|
package/dist/routes/oauth.js
CHANGED
|
@@ -174,9 +174,9 @@ function html(body, status = 200, branding) {
|
|
|
174
174
|
* list the flat 400 was meant to withhold. So zero-I/O refusals read the same
|
|
175
175
|
* keys in the same `conn:<id>:` namespace, where an unconfigured id gets misses.
|
|
176
176
|
*
|
|
177
|
-
* This is deliberately *not* a constant-time claim
|
|
178
|
-
*
|
|
179
|
-
*
|
|
177
|
+
* This is deliberately *not* a constant-time claim: a hit and a miss are not
|
|
178
|
+
* identical in a KV store, and a connector shipping its own `verifyState` may
|
|
179
|
+
* do more or less work. What it
|
|
180
180
|
* removes is the order-of-magnitude "no I/O versus a round trip" difference,
|
|
181
181
|
* which is the only part of the signal that makes enumeration cheap.
|
|
182
182
|
*
|
package/dist/routes/shared.d.ts
CHANGED
|
@@ -6,38 +6,39 @@ import type { DeferredWork } from "../connector-scope.js";
|
|
|
6
6
|
import type { AdmissionController } from "../executor-admission.js";
|
|
7
7
|
import type { Registry } from "../registry.js";
|
|
8
8
|
import type { ConnectaBranding, Executor, InboundAuth, Logger } from "../types.js";
|
|
9
|
+
export { msg } from "../errors.js";
|
|
9
10
|
export interface ServerOptions {
|
|
10
11
|
registry: Registry;
|
|
11
12
|
auth: InboundAuth[];
|
|
12
|
-
publicUrl?: string;
|
|
13
|
+
publicUrl?: string | undefined;
|
|
13
14
|
serverInfo: Implementation;
|
|
14
15
|
logger: Logger;
|
|
15
|
-
activity?: ActivityStore;
|
|
16
|
-
activityReadGate?: ActivityReadGate;
|
|
17
|
-
activityDeploymentId?: string;
|
|
18
|
-
deploymentInfo?: Record<string, unknown
|
|
16
|
+
activity?: ActivityStore | undefined;
|
|
17
|
+
activityReadGate?: ActivityReadGate | undefined;
|
|
18
|
+
activityDeploymentId?: string | undefined;
|
|
19
|
+
deploymentInfo?: Record<string, unknown> | undefined;
|
|
19
20
|
/** Deadline for call_tool/call_destructive_tool calls that pass no timeoutMs. Off when unset. */
|
|
20
|
-
defaultToolTimeoutMs?: number;
|
|
21
|
+
defaultToolTimeoutMs?: number | undefined;
|
|
21
22
|
/** Per-connector deadline for the search/describe probe fan-out. Default 30_000. */
|
|
22
|
-
probeTimeoutMs?: number;
|
|
23
|
+
probeTimeoutMs?: number | undefined;
|
|
23
24
|
/** Maximum simultaneous connector discovery operations. Default 4. */
|
|
24
|
-
discoveryConcurrency?: number;
|
|
25
|
+
discoveryConcurrency?: number | undefined;
|
|
25
26
|
/** Aggregate serialized-byte budget for connecta.emit per run. Default 4_000_000. */
|
|
26
|
-
maxEmittedBytes?: number;
|
|
27
|
+
maxEmittedBytes?: number | undefined;
|
|
27
28
|
/** Block-count budget for connecta.emit per run. Default 32. */
|
|
28
|
-
maxEmittedBlocks?: number;
|
|
29
|
+
maxEmittedBlocks?: number | undefined;
|
|
29
30
|
/** Required sandbox backing the execute_code meta-tool. */
|
|
30
31
|
executor: Executor;
|
|
31
32
|
/** Sanitized identity of the configured sandbox, when it has one. */
|
|
32
|
-
executorName?: string;
|
|
33
|
+
executorName?: string | undefined;
|
|
33
34
|
/** Global FIFO boundary for all non-preflight `/mcp` requests. */
|
|
34
35
|
requestAdmission: AdmissionController;
|
|
35
36
|
/** Encrypted connector-credential storage backing the Credentials page. */
|
|
36
|
-
credentialVault?: CredentialVault;
|
|
37
|
+
credentialVault?: CredentialVault | undefined;
|
|
37
38
|
/** Hashed deployment access tokens backing MCP admission and the Tokens page. */
|
|
38
|
-
accessTokens?: AccessTokenManager;
|
|
39
|
+
accessTokens?: AccessTokenManager | undefined;
|
|
39
40
|
/** Optional browser UI and OAuth result-page labels. */
|
|
40
|
-
branding?: ConnectaBranding;
|
|
41
|
+
branding?: ConnectaBranding | undefined;
|
|
41
42
|
}
|
|
42
43
|
export interface RuntimeExecutionContext {
|
|
43
44
|
waitUntil(promise: Promise<unknown>): void;
|
|
@@ -51,7 +52,6 @@ export interface RouteContext {
|
|
|
51
52
|
defer: DeferredWork | undefined;
|
|
52
53
|
runtimeContext: RuntimeExecutionContext | undefined;
|
|
53
54
|
}
|
|
54
|
-
export declare function msg(err: unknown): string;
|
|
55
55
|
export declare function privateJson(body: unknown, init?: ResponseInit): Response;
|
|
56
56
|
/**
|
|
57
57
|
* Bounded, escaped form of a caller-influenced value (an identity id or OAuth
|
package/dist/routes/shared.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { operatorPageForPath } from "../ui.js";
|
|
2
|
-
export
|
|
3
|
-
return err instanceof Error ? err.message : String(err);
|
|
4
|
-
}
|
|
2
|
+
export { msg } from "../errors.js";
|
|
5
3
|
export function privateJson(body, init = {}) {
|
|
6
4
|
const headers = new Headers(init.headers);
|
|
7
5
|
headers.set("Content-Type", "application/json");
|
package/dist/skills.js
CHANGED
|
@@ -23,13 +23,13 @@ The minimum guest API is:
|
|
|
23
23
|
|
|
24
24
|
Search inside the run and finish the task there. A discovery-only program wastes a round trip. Use 2–4 distinctive action/object terms, not the full request. Use separate short searches for distinct operations.
|
|
25
25
|
|
|
26
|
-
For top-level \`search_tools\`, omit \`limit\` initially (the default is 10), then page with a limit up to 50 if needed. Empty or whitespace-only queries browse all tools. A non-empty query with no ASCII terms returns no matches; mixed input searches with its ASCII terms. \`includeSchemas: "compact"\` adds bounded input and
|
|
26
|
+
For top-level \`search_tools\`, omit \`limit\` initially (the default is 10), then page with a limit up to 50 if needed. Empty or whitespace-only queries browse all tools. A non-empty query with no ASCII terms returns no matches; mixed input searches with its ASCII terms. \`includeSchemas: "compact"\` adds bounded input and available output shapes. An observed shape carries \`outputSchemaSource: "observed"\`; treat it as routing evidence rather than a provider contract. Plain objects expose \`inputKeys\`, \`requiredInputKeys\`, and \`outputKeys\`; truncation flags mark incomplete shapes; matches also carry declared annotations.
|
|
27
27
|
|
|
28
28
|
- \`connecta.search({})\` loads all catalogs. Pass \`connector: "<id>"\` when the integration is obvious. Use \`safety: "readOnly"\` for program calls. These inputs filter discovery; they grant no authority.
|
|
29
|
-
- Request \`includeSchemas: "compact"\`. Check address, purpose, annotations, required inputs, truncation, safety, and
|
|
29
|
+
- Request \`includeSchemas: "compact"\`. Check address, purpose, annotations, required inputs, truncation, safety, and available outputs. Never select only because a result ranks first or has fewer required inputs.
|
|
30
30
|
- Supply every \`requiredInputKey\` from the task or a prior result. For dependencies, match the earlier \`outputKey\` to the later required key. An empty required-key list does not permit invented arguments. Missing \`outputKeys\` means inspect \`outputSchema\`.
|
|
31
31
|
- Use \`connecta.describe({ address })\` or \`{ addresses }\` when a compact schema is truncated or insufficient. Use \`format: "json"\` only for exact constraints. Write the property names the schema displays; never guess positions or aliases.
|
|
32
|
-
- Reduce through
|
|
32
|
+
- Reduce through available output keys. Treat an observed key as a hint, since later results may omit it or add others. Do not guess collection roots such as \`items\` or \`results\`. If a match or result key is missing, inspect, re-search, or describe inside the same run instead of returning discovery for another call.
|
|
33
33
|
|
|
34
34
|
Only tools explicitly annotated \`readOnlyHint: true\` are reachable. The catalog, credential, admission, and read-only gates run below the sandbox; code cannot widen its authority.
|
|
35
35
|
|