@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.
Files changed (75) hide show
  1. package/CHANGELOG.md +100 -4
  2. package/README.md +4 -0
  3. package/dist/catalog-service.d.ts +20 -13
  4. package/dist/catalog-service.js +123 -116
  5. package/dist/catalog.js +29 -46
  6. package/dist/connector-scope.js +2 -7
  7. package/dist/connectors/api.d.ts +4 -16
  8. package/dist/connectors/api.js +19 -46
  9. package/dist/connectors/guarded-fetch.d.ts +9 -23
  10. package/dist/connectors/guarded-fetch.js +38 -76
  11. package/dist/connectors/remote-mcp.js +36 -79
  12. package/dist/errors.d.ts +6 -27
  13. package/dist/errors.js +8 -5
  14. package/dist/execute.d.ts +24 -22
  15. package/dist/execute.js +98 -145
  16. package/dist/executor-result.d.ts +1 -0
  17. package/dist/executor-result.js +4 -11
  18. package/dist/executors/quickjs-child.js +1 -3
  19. package/dist/executors/quickjs-runtime.js +1 -3
  20. package/dist/executors/quickjs.js +1 -3
  21. package/dist/index.js +27 -57
  22. package/dist/invocation.js +114 -178
  23. package/dist/meta-tools.d.ts +15 -28
  24. package/dist/meta-tools.js +33 -89
  25. package/dist/providers/cloudflare.d.ts +2 -18
  26. package/dist/providers/cloudflare.js +1460 -2451
  27. package/dist/providers/linear.d.ts +4 -41
  28. package/dist/providers/linear.js +8 -39
  29. package/dist/providers/mixpanel.d.ts +3 -25
  30. package/dist/providers/mixpanel.js +7 -22
  31. package/dist/providers/notion.d.ts +1 -15
  32. package/dist/providers/notion.js +44 -173
  33. package/dist/providers/revenuecat.d.ts +4 -57
  34. package/dist/providers/revenuecat.js +10 -93
  35. package/dist/providers/stripe.d.ts +1 -12
  36. package/dist/providers/stripe.js +7 -45
  37. package/dist/registry.d.ts +16 -34
  38. package/dist/registry.js +18 -103
  39. package/dist/result-shapes.d.ts +13 -0
  40. package/dist/result-shapes.js +331 -0
  41. package/dist/routes/mcp.js +1 -1
  42. package/dist/routes/oauth.js +3 -3
  43. package/dist/routes/shared.d.ts +15 -15
  44. package/dist/routes/shared.js +1 -3
  45. package/dist/skills.js +3 -3
  46. package/dist/timeout.d.ts +8 -7
  47. package/dist/timeout.js +47 -38
  48. package/dist/types.d.ts +3 -3
  49. package/dist/ui.d.ts +1 -25
  50. package/dist/ui.js +18 -45
  51. package/dist/version.d.ts +1 -1
  52. package/dist/version.js +1 -1
  53. package/documentation/architecture.md +5 -2
  54. package/documentation/call-admission.md +1 -1
  55. package/documentation/cloudflare.md +1 -1
  56. package/documentation/code-mode.md +13 -13
  57. package/documentation/connectors.md +34 -1
  58. package/documentation/linear.md +1 -1
  59. package/documentation/meta-tools.md +17 -3
  60. package/documentation/mixpanel.md +1 -1
  61. package/documentation/notion.md +1 -1
  62. package/documentation/operations.md +20 -15
  63. package/documentation/provider-conventions.md +1 -1
  64. package/documentation/revenuecat.md +1 -1
  65. package/documentation/stripe.md +1 -1
  66. package/documentation/upgrading.md +24 -4
  67. package/ethos.md +74 -120
  68. package/package.json +3 -4
  69. package/templates/node/package.json +1 -1
  70. package/documentation/code-first-exploration.md +0 -292
  71. package/documentation/mcp-2026-07-28.md +0 -46
  72. package/documentation/mcp-ui-design.md +0 -382
  73. package/documentation/program-ui-read-calls.md +0 -213
  74. package/documentation/provider-audit.md +0 -198
  75. 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 { withAbortableTimeout } from "./timeout.js";
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, () => withAbortableTimeout((signal) => {
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
- }, options.refreshTimeoutMs, `deferred catalog refresh of "${id}"`), async () => {
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
+ }
@@ -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 (documentation/mcp-2026-07-28.md).
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] },
@@ -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, and documentation/connectors.md says
178
- * so in prose: a hit and a miss are not identical in a KV store, and a connector
179
- * shipping its own `verifyState` may do more or less work. What it
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
  *
@@ -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
@@ -1,7 +1,5 @@
1
1
  import { operatorPageForPath } from "../ui.js";
2
- export function msg(err) {
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 declared output shapes. Plain objects expose \`inputKeys\`, \`requiredInputKeys\`, and \`outputKeys\`; truncation flags mark incomplete shapes; matches also carry declared annotations.
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 declared outputs. Never select only because a result ranks first or has fewer required inputs.
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 declared output keys. 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.
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