@zackbart/connecta 0.18.2 → 0.18.3
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 +27 -0
- package/README.md +4 -0
- package/dist/catalog-service.d.ts +4 -0
- package/dist/catalog-service.js +22 -6
- package/dist/invocation.js +9 -3
- package/dist/registry.d.ts +7 -0
- package/dist/registry.js +9 -0
- package/dist/result-shapes.d.ts +13 -0
- package/dist/result-shapes.js +331 -0
- package/dist/skills.js +3 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +5 -2
- package/documentation/code-mode.md +6 -6
- package/documentation/connectors.md +10 -0
- package/documentation/meta-tools.md +17 -3
- package/documentation/operations.md +2 -1
- package/documentation/upgrading.md +15 -4
- package/ethos.md +3 -3
- package/package.json +1 -1
- package/templates/node/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this package are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.18.3 — 2026-08-25
|
|
6
|
+
|
|
7
|
+
This change gives code mode memory where downstream MCP catalogs are usually
|
|
8
|
+
silent: after a successful call with no declared output schema, Connecta keeps
|
|
9
|
+
only the result's field names and broad JSON types and shows that open shape on
|
|
10
|
+
later discovery. Nothing breaks, no deployment option changes, and providers
|
|
11
|
+
that already declare outputs are untouched. The bounded cache lives only in the
|
|
12
|
+
current process or Worker isolate and forgets each shape after 24 hours.
|
|
13
|
+
Discovery still never executes a tool, and
|
|
14
|
+
the new `outputSchemaSource: "observed"` marker is the warning label that keeps
|
|
15
|
+
one or several real results from masquerading as a provider contract.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **Passive observed output schemas.** Successful explicitly read-only calls
|
|
20
|
+
whose provider declared no `outputSchema` infer and merge an open
|
|
21
|
+
optional-field schema of names and broad JSON types. The inference retains no
|
|
22
|
+
arguments, scalar values, raw results, code, credentials, or errors. Property
|
|
23
|
+
names may be user-authored. It bounds depth, breadth, names, nodes, definition
|
|
24
|
+
size, and schema bytes; keeps at most 256 runtime entries; ties each entry to
|
|
25
|
+
the exact tool definition; and expires it after 24 hours.
|
|
26
|
+
Search and describe return it with `outputSchemaSource: "observed"`; a
|
|
27
|
+
declared schema always wins. Observation is synchronous, storage-free, and
|
|
28
|
+
unable to fail the call. The design follows new warm-cache evidence and two
|
|
29
|
+
live deployment audits: BePresent had 246/378 tools without output schemas,
|
|
30
|
+
while OneMany's maintained connectors declared all 90 (#442).
|
|
31
|
+
|
|
5
32
|
## 0.18.2 — 2026-08-18
|
|
6
33
|
|
|
7
34
|
This patch closes the gap the RevenueCat rollout exposed on the same day 0.18.1
|
package/README.md
CHANGED
|
@@ -67,6 +67,10 @@ Fifty issues in, one small object out. Your context window notices.
|
|
|
67
67
|
classifications, imported one at a time.
|
|
68
68
|
- **Let the agent work in code.** Search, chain, filter, join, and reduce
|
|
69
69
|
inside the sandbox instead of round-tripping every call through the model.
|
|
70
|
+
- **Teach undeclared result shapes by using them.** Successful read-only calls
|
|
71
|
+
retain field names and broad types in bounded runtime memory, never scalar
|
|
72
|
+
values, so later programs can project a remote MCP result its provider never
|
|
73
|
+
documented.
|
|
70
74
|
- **Keep writes deliberate.** Only tools marked read-only run in a program.
|
|
71
75
|
Everything else is a separate, visible call your client can gate.
|
|
72
76
|
- **Run it where you like.** Node, a Docker container, or a Cloudflare Worker,
|
|
@@ -49,6 +49,7 @@ interface CatalogSearchEntry {
|
|
|
49
49
|
description?: string;
|
|
50
50
|
inputSchema?: unknown;
|
|
51
51
|
outputSchema?: unknown;
|
|
52
|
+
outputSchemaSource?: "observed";
|
|
52
53
|
inputSchemaTruncated?: true;
|
|
53
54
|
outputSchemaTruncated?: true;
|
|
54
55
|
inputKeys?: string[];
|
|
@@ -112,6 +113,7 @@ export interface CatalogDescription {
|
|
|
112
113
|
guideRequiredReasons?: GuideRequiredReason[];
|
|
113
114
|
inputSchema?: unknown;
|
|
114
115
|
outputSchema?: unknown;
|
|
116
|
+
outputSchemaSource?: "observed";
|
|
115
117
|
annotations?: ToolDef["annotations"];
|
|
116
118
|
error?: string;
|
|
117
119
|
errorDetails?: CatalogDescriptionFailureDetail;
|
|
@@ -174,6 +176,7 @@ export declare class CatalogService {
|
|
|
174
176
|
}, purpose: string): NonNullable<CallErrorDetails["nextAction"]>;
|
|
175
177
|
loadConnector(id: string, callOptions?: ConnectorOperationOptions): Promise<ToolDef[]>;
|
|
176
178
|
private loadForDiscovery;
|
|
179
|
+
private outputSchema;
|
|
177
180
|
resolveTool(address: string, callOptions?: ConnectorOperationOptions): Promise<CatalogResolution>;
|
|
178
181
|
/**
|
|
179
182
|
* Resolve the JavaScript-safe property used by a lazy code-mode namespace
|
|
@@ -242,6 +245,7 @@ export declare function flatSearchResult(page: CatalogSearchPage): {
|
|
|
242
245
|
description?: string;
|
|
243
246
|
inputSchema?: unknown;
|
|
244
247
|
outputSchema?: unknown;
|
|
248
|
+
outputSchemaSource?: "observed";
|
|
245
249
|
inputSchemaTruncated?: true;
|
|
246
250
|
outputSchemaTruncated?: true;
|
|
247
251
|
inputKeys?: string[];
|
package/dist/catalog-service.js
CHANGED
|
@@ -266,6 +266,14 @@ export class CatalogService {
|
|
|
266
266
|
timeoutMs: this.probeTimeoutMs,
|
|
267
267
|
}), this.probeTimeoutMs, label);
|
|
268
268
|
}
|
|
269
|
+
outputSchema(connectorId, tool) {
|
|
270
|
+
if (tool.outputSchema)
|
|
271
|
+
return { schema: tool.outputSchema };
|
|
272
|
+
const observed = this.registry.observedOutputSchema(connectorId, tool);
|
|
273
|
+
return observed
|
|
274
|
+
? { schema: observed, source: "observed" }
|
|
275
|
+
: {};
|
|
276
|
+
}
|
|
269
277
|
async resolveTool(address, callOptions = {}) {
|
|
270
278
|
const resolved = this.registry.resolveAddress(address);
|
|
271
279
|
if (!resolved) {
|
|
@@ -487,15 +495,18 @@ export class CatalogService {
|
|
|
487
495
|
});
|
|
488
496
|
const pageMatches = matches.slice(offset, offset + limit);
|
|
489
497
|
const entries = pageMatches.map((match) => {
|
|
498
|
+
const output = args.includeSchemas
|
|
499
|
+
? this.outputSchema(match.connector.id, match.tool)
|
|
500
|
+
: {};
|
|
490
501
|
const input = match.tool.inputSchema ?? { type: "object" };
|
|
491
502
|
const renderedInput = args.includeSchemas
|
|
492
503
|
? renderSearchSchema(input, args.includeSchemas)
|
|
493
504
|
: undefined;
|
|
494
|
-
const renderedOutput = args.includeSchemas &&
|
|
495
|
-
? renderSearchSchema(
|
|
505
|
+
const renderedOutput = args.includeSchemas && output.schema
|
|
506
|
+
? renderSearchSchema(output.schema, args.includeSchemas)
|
|
496
507
|
: undefined;
|
|
497
508
|
const schemaKeys = args.includeSchemas && args.includeSchemaKeys
|
|
498
|
-
? schemaKeyMetadata(input,
|
|
509
|
+
? schemaKeyMetadata(input, output.schema)
|
|
499
510
|
: undefined;
|
|
500
511
|
const description = summarizeDiscoveryDescription(match.tool.description, args.fullDescriptions === true);
|
|
501
512
|
const requiredReasons = guideRequiredReasons(match.connector, match.tool, renderedInput?.truncated === true || renderedOutput?.truncated === true);
|
|
@@ -520,11 +531,14 @@ export class CatalogService {
|
|
|
520
531
|
...(renderedInput?.truncated
|
|
521
532
|
? { inputSchemaTruncated: true }
|
|
522
533
|
: {}),
|
|
523
|
-
...(args.includeSchemas &&
|
|
534
|
+
...(args.includeSchemas && output.schema
|
|
524
535
|
? {
|
|
525
536
|
outputSchema: renderedOutput?.schema,
|
|
526
537
|
}
|
|
527
538
|
: {}),
|
|
539
|
+
...(args.includeSchemas && output.source
|
|
540
|
+
? { outputSchemaSource: output.source }
|
|
541
|
+
: {}),
|
|
528
542
|
...(renderedOutput?.truncated
|
|
529
543
|
? { outputSchemaTruncated: true }
|
|
530
544
|
: {}),
|
|
@@ -813,6 +827,7 @@ export class CatalogService {
|
|
|
813
827
|
};
|
|
814
828
|
}
|
|
815
829
|
const input = tool.inputSchema ?? { type: "object" };
|
|
830
|
+
const output = this.outputSchema(addressResolution.connector.id, tool);
|
|
816
831
|
const description = summarizeDescription(tool.description, args.fullDescriptions === true);
|
|
817
832
|
const requiredReasons = guideRequiredReasons(addressResolution.connector, tool, false);
|
|
818
833
|
const guideSummary = connectorGuideSummary(addressResolution.connector);
|
|
@@ -833,11 +848,12 @@ export class CatalogService {
|
|
|
833
848
|
}
|
|
834
849
|
: {}),
|
|
835
850
|
inputSchema: renderSchema(input, format),
|
|
836
|
-
...(
|
|
851
|
+
...(output.schema
|
|
837
852
|
? {
|
|
838
|
-
outputSchema: renderSchema(
|
|
853
|
+
outputSchema: renderSchema(output.schema, format),
|
|
839
854
|
}
|
|
840
855
|
: {}),
|
|
856
|
+
...(output.source ? { outputSchemaSource: output.source } : {}),
|
|
841
857
|
...(tool.annotations ? { annotations: tool.annotations } : {}),
|
|
842
858
|
};
|
|
843
859
|
});
|
package/dist/invocation.js
CHANGED
|
@@ -288,6 +288,7 @@ export class InvocationService {
|
|
|
288
288
|
}
|
|
289
289
|
const maxRetries = Math.min(2, Math.max(0, Math.trunc(context.maxRetries ?? 0)));
|
|
290
290
|
let result;
|
|
291
|
+
let observedResult;
|
|
291
292
|
while (true) {
|
|
292
293
|
attempts++;
|
|
293
294
|
let permit;
|
|
@@ -362,9 +363,8 @@ export class InvocationService {
|
|
|
362
363
|
// reports the same downstream-failure wording, and the throw lands
|
|
363
364
|
// inside the attempt where it stays retry-eligible and feeds health.
|
|
364
365
|
assertRawMcpSuccess(resolved.connector.kind, raw);
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
: raw;
|
|
366
|
+
observedResult = unwrapMcpResult(resolved.connector.kind, raw);
|
|
367
|
+
result = context.unwrapResult ? observedResult : raw;
|
|
368
368
|
}
|
|
369
369
|
finally {
|
|
370
370
|
connectorMs += Date.now() - connectorStarted;
|
|
@@ -440,6 +440,12 @@ export class InvocationService {
|
|
|
440
440
|
const value = context.processResult
|
|
441
441
|
? await context.processResult(result, resolved)
|
|
442
442
|
: result;
|
|
443
|
+
try {
|
|
444
|
+
this.registry.observeOutputShape(resolved.connector.id, resolved.definition, observedResult);
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
// Shape learning is advisory. It cannot change a completed call.
|
|
448
|
+
}
|
|
443
449
|
resultProcessingMs += Date.now() - processingStarted;
|
|
444
450
|
const diagnostics = timing();
|
|
445
451
|
const friction = context.activityFriction?.(value);
|
package/dist/registry.d.ts
CHANGED
|
@@ -113,6 +113,10 @@ export interface RegistryView {
|
|
|
113
113
|
observedSuccessAt(id: string): string | undefined;
|
|
114
114
|
/** Local declared-vs-stored credential mismatch, with no downstream I/O. */
|
|
115
115
|
credentialDriftFor(id: string): Promise<string | undefined>;
|
|
116
|
+
/** Value-free shape learned from successful calls, never a provider declaration. */
|
|
117
|
+
observedOutputSchema(connectorId: string, definition: ToolDef): ToolDef["outputSchema"] | undefined;
|
|
118
|
+
/** Passively learn one successful unwrapped result; failures stay isolated. */
|
|
119
|
+
observeOutputShape(connectorId: string, definition: ToolDef, value: unknown): void;
|
|
116
120
|
statusFor(id: string, baseUrl: string, requestScope?: object, callOptions?: ConnectorOperationOptions): Promise<ConnectorStatus>;
|
|
117
121
|
invalidateStored(id: string): Promise<void>;
|
|
118
122
|
}
|
|
@@ -141,6 +145,7 @@ export declare class Registry implements RegistryView {
|
|
|
141
145
|
private readonly health;
|
|
142
146
|
/** Last drift counts reported to activity, per connector, in this runtime. */
|
|
143
147
|
private readonly reportedDrift;
|
|
148
|
+
private readonly observedOutputSchemas;
|
|
144
149
|
private readonly ttlMs;
|
|
145
150
|
private readonly staleMs;
|
|
146
151
|
private readonly persistToolCatalog;
|
|
@@ -203,6 +208,8 @@ export declare class Registry implements RegistryView {
|
|
|
203
208
|
* separate from any connector's `conn:<id>:` namespace. Backs get_result.
|
|
204
209
|
*/
|
|
205
210
|
resultsStorage(): KVStorage;
|
|
211
|
+
observedOutputSchema(connectorId: string, definition: ToolDef): ToolDef["outputSchema"] | undefined;
|
|
212
|
+
observeOutputShape(connectorId: string, definition: ToolDef, value: unknown): void;
|
|
206
213
|
/** Resolve "<connectorId>.<toolName>" → connector + tool name. */
|
|
207
214
|
resolveAddress(address: string): {
|
|
208
215
|
connector: Connector;
|
package/dist/registry.js
CHANGED
|
@@ -7,6 +7,7 @@ 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
12
|
import { withAbortableTimeout } from "./timeout.js";
|
|
12
13
|
const ID_RE = /^[a-z0-9_-]+$/;
|
|
@@ -133,6 +134,7 @@ export class Registry {
|
|
|
133
134
|
health = new HealthLog();
|
|
134
135
|
/** Last drift counts reported to activity, per connector, in this runtime. */
|
|
135
136
|
reportedDrift = new Map();
|
|
137
|
+
observedOutputSchemas;
|
|
136
138
|
ttlMs;
|
|
137
139
|
staleMs;
|
|
138
140
|
persistToolCatalog;
|
|
@@ -140,6 +142,7 @@ export class Registry {
|
|
|
140
142
|
maxResultBytes;
|
|
141
143
|
constructor(connectors, opts) {
|
|
142
144
|
this.opts = opts;
|
|
145
|
+
this.observedOutputSchemas = new ObservedOutputSchemas();
|
|
143
146
|
this.ttlMs =
|
|
144
147
|
(opts.toolCacheTtlSeconds ?? DEFAULT_TTL_SECONDS) * 1000;
|
|
145
148
|
this.staleMs =
|
|
@@ -342,6 +345,12 @@ export class Registry {
|
|
|
342
345
|
resultsStorage() {
|
|
343
346
|
return namespaced(this.opts.storage, "results:");
|
|
344
347
|
}
|
|
348
|
+
observedOutputSchema(connectorId, definition) {
|
|
349
|
+
return this.observedOutputSchemas.get(connectorId, definition);
|
|
350
|
+
}
|
|
351
|
+
observeOutputShape(connectorId, definition, value) {
|
|
352
|
+
this.observedOutputSchemas.observe(connectorId, definition, value);
|
|
353
|
+
}
|
|
345
354
|
/** Resolve "<connectorId>.<toolName>" → connector + tool name. */
|
|
346
355
|
resolveAddress(address) {
|
|
347
356
|
const parts = splitAddress(address);
|
|
@@ -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/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
|
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
|
@@ -14,8 +14,9 @@ stating before anything else.
|
|
|
14
14
|
|
|
15
15
|
**Per isolate, built once.** `createConnecta(config)` returns
|
|
16
16
|
`{ fetch, registry, close }`. The `Registry` owns the connector set, address
|
|
17
|
-
resolution, catalog caches, connector health, and the
|
|
18
|
-
limiters. It is constructed once and lives as long as the
|
|
17
|
+
resolution, catalog caches, observed output schemas, connector health, and the
|
|
18
|
+
per-connector call limiters. It is constructed once and lives as long as the
|
|
19
|
+
isolate or process —
|
|
19
20
|
on Workers that means a lazy module-scope singleton, which is why both
|
|
20
21
|
deployment shapes build it outside the request handler.
|
|
21
22
|
|
|
@@ -94,6 +95,7 @@ owns or hands out, and a change usually belongs in exactly one of them:
|
|
|
94
95
|
| `src/catalog-service.ts` | Request-local tool listing, search, and describe. It coalesces reads inside one request and opts agent reads into the runtime's deferred catalog channel when one exists. |
|
|
95
96
|
| `src/invocation.ts` | One tool call: argument validation, call admission, per-attempt timeout, retry with the connector's own `Retry-After` honoured exactly or declined, result unwrapping, size capping, and the activity record. |
|
|
96
97
|
| `src/catalog.ts` | Ranking, description summarizing, and the compact schema renderer discovery shows. |
|
|
98
|
+
| `src/result-shapes.ts` | Bounded runtime-only inference and merging for output shapes learned from successful read-only calls whose providers declared none. |
|
|
97
99
|
|
|
98
100
|
`src/meta-tools.ts` and `src/execute.ts` are two front doors onto the same
|
|
99
101
|
three services. That is the point: a program's `connecta.call` and a top-level
|
|
@@ -142,6 +144,7 @@ src/
|
|
|
142
144
|
registry.ts connector set, addresses, health, call limiters
|
|
143
145
|
catalog-service.ts request-local catalog access, search, and describe
|
|
144
146
|
catalog.ts ranking, summaries, compact schema rendering
|
|
147
|
+
result-shapes.ts passive runtime-only observed output schemas
|
|
145
148
|
invocation.ts one tool call, end to end
|
|
146
149
|
catalog-drift.ts vetted manifests and the counts a refresh produces
|
|
147
150
|
credentials.ts the AES-GCM connector vault over KVStorage
|
|
@@ -149,10 +149,7 @@ a cycle, a `BigInt`, a function, a class instance — never round-trips: it eith
|
|
|
149
149
|
ends the run with an error or is converted lossily, executor's choice (`X9`).
|
|
150
150
|
Return JSON-shaped data and the question does not arise.
|
|
151
151
|
|
|
152
|
-
**P4.** Nothing survives an execution. There is no module scope, cache, or
|
|
153
|
-
scratch storage carried to the next program, and no request-bound object outlives
|
|
154
|
-
the request that created it. Within one execution, host calls share one
|
|
155
|
-
downstream request scope.
|
|
152
|
+
**P4.** Nothing survives an execution. There is no module scope, cache, or scratch storage carried to the next program, and no request-bound object outlives the request that created it. Within one execution, host calls share one downstream request scope. `S9`'s host-owned output observation is catalog metadata, not guest memory: a later program receives no prior value or object, only a labeled field/type schema through discovery.
|
|
156
153
|
|
|
157
154
|
**P5.** Plain JavaScript only. TypeScript syntax is a syntax error. Portable code
|
|
158
155
|
does not import: QuickJS blocks imports, while Dynamic Workers expose the `X5`
|
|
@@ -223,7 +220,7 @@ const page = await connecta.search({
|
|
|
223
220
|
});
|
|
224
221
|
```
|
|
225
222
|
|
|
226
|
-
**S1.** Returns one flat page: `{ tools, total, offset, limit, hasMore }`, plus `nextOffset` when more remains and `matchMode: "partial"` when no tool matched every term. Top-level `search_tools` is different: it returns `{ connectors: [{ id, tools }], total, offset, limit, hasMore }`. Complete matches normally precede partial matches, but a partial candidate whose complete normalized tool name occurs in the normalized raw query competes by score; conversational cleanup applies only to scoring terms. Other candidates covering at least two terms fill the page after every complete match; when no complete match exists, the existing any-term fallback remains. Each entry in `tools` carries `address`, `name`, and — when requested — `description`, `inputSchema`, `outputSchema`, `annotations`, and the connector's `guide`. Tool rows expose neither lexical scores nor per-result coverage. An empty or whitespace-only query browses. Non-empty input with no ASCII lexical terms returns no tools and bounded no-match analysis; mixed input searches with its ASCII terms. Compact shapes omit property prose, put required fields first, and cap each shape at 1,024 UTF-8 bytes. Each enum node gets 256 of those bytes. About three near-cap enum nodes can therefore coexist while leaving the final quarter for surrounding syntax; the unchanged global fallback still applies above 1,024 bytes. A capped enum preserves whole values before `unknown` and an exact omitted-value count, while an empty enum renders as `never`. Either cap carries `inputSchemaTruncated` or `outputSchemaTruncated`; a shape-wide cap remains structurally valid with `unknown` types plus `/* truncated */`. Small enums remain complete. Use `connecta.describe` (or JSON search) for omitted exact constraints.
|
|
223
|
+
**S1.** Returns one flat page: `{ tools, total, offset, limit, hasMore }`, plus `nextOffset` when more remains and `matchMode: "partial"` when no tool matched every term. Top-level `search_tools` is different: it returns `{ connectors: [{ id, tools }], total, offset, limit, hasMore }`. Complete matches normally precede partial matches, but a partial candidate whose complete normalized tool name occurs in the normalized raw query competes by score; conversational cleanup applies only to scoring terms. Other candidates covering at least two terms fill the page after every complete match; when no complete match exists, the existing any-term fallback remains. Each entry in `tools` carries `address`, `name`, and — when requested — `description`, `inputSchema`, `outputSchema`, `annotations`, and the connector's `guide`. An output shape learned under `S9` also carries `outputSchemaSource: "observed"`; provider declarations carry no source marker. Tool rows expose neither lexical scores nor per-result coverage. An empty or whitespace-only query browses. Non-empty input with no ASCII lexical terms returns no tools and bounded no-match analysis; mixed input searches with its ASCII terms. Compact shapes omit property prose, put required fields first, and cap each shape at 1,024 UTF-8 bytes. Each enum node gets 256 of those bytes. About three near-cap enum nodes can therefore coexist while leaving the final quarter for surrounding syntax; the unchanged global fallback still applies above 1,024 bytes. A capped enum preserves whole values before `unknown` and an exact omitted-value count, while an empty enum renders as `never`. Either cap carries `inputSchemaTruncated` or `outputSchemaTruncated`; a shape-wide cap remains structurally valid with `unknown` types plus `/* truncated */`. Small enums remain complete. Use `connecta.describe` (or JSON search) for omitted exact constraints.
|
|
227
224
|
|
|
228
225
|
**S1a.** `connector` loads only the named catalog; omit it only when the integration is ambiguous, because an unscoped search fans out across every configured connector. `safety: "readOnly"` returns exactly the tools available through `connecta.call`, connector shortcuts, and `connecta.batch`; `"approvalRequired"` returns the complementary fail-closed class, including false, missing, and contradictory annotations. Omitted or `"all"` preserves the complete catalog. These filters grant no authority and change no admission decision.
|
|
229
226
|
|
|
@@ -265,7 +262,7 @@ carry a route-aware `nextAction`; a close miss may add three canonical `suggesti
|
|
|
265
262
|
Catalog failures add only `retryAfterMs` when known. One bad address never fails the whole call. Each failed entry clamps its
|
|
266
263
|
caller-authored `address` to 512 UTF-8 bytes with an `…` marker. Entry order
|
|
267
264
|
correlates a clipped address with its request; successes keep canonical addresses. More than 100
|
|
268
|
-
addresses is `invalid_args`; the same 256,000-byte ceiling applies.
|
|
265
|
+
addresses is `invalid_args`; the same 256,000-byte ceiling applies. A success whose output shape came from `S9` carries `outputSchemaSource: "observed"` beside the rendered schema.
|
|
269
266
|
|
|
270
267
|
### connecta.call
|
|
271
268
|
|
|
@@ -304,6 +301,8 @@ the batch, and more than ten calls throws.
|
|
|
304
301
|
|
|
305
302
|
**S8.** Batch and thrown failures share one vocabulary (`E1`): an entry's `errorDetails.code` and `retryable` equal the fields on the error the same call would throw. Use batch for independent concurrency, not to recover lost type.
|
|
306
303
|
|
|
304
|
+
**S9.** A successful explicitly read-only call whose provider declared no `outputSchema` passively learns one from the unwrapped result. The observation retains field names and broad JSON types only: no arguments, scalar values, raw results, code, credentials, or errors. Property names may be user-authored. Objects stay open, every field stays optional, and search or describe labels the shape `outputSchemaSource: "observed"` so a model cannot mistake runtime evidence for a provider contract. Later observations merge fields and types in a process-local 256-entry LRU; a provider declaration always wins. Inference stops at depth 6, 128 schema nodes, 48 properties per object, 32 inspected array items, and 128 UTF-8 bytes per property name; `__proto__`, `constructor`, and `prototype` names are discarded. A tool definition over 64 KiB or an observed schema over 16 KiB is ignored. An entry expires after 24 hours and carries the exact serialized tool definition, so a changed catalog entry, process restart, or Worker isolate eviction starts cold. A failed call or failed result-processing step learns nothing, and any observation failure is discarded without changing a successful call. No discovery read, timer, refresh, background job, or storage adapter executes or persists work for this cache: the result-sampling refusal in [#282](https://github.com/zackbart/connecta/issues/282) stands.
|
|
305
|
+
|
|
307
306
|
### connecta.emit
|
|
308
307
|
|
|
309
308
|
```js
|
|
@@ -847,6 +846,7 @@ the upstream `Executor` shape assignable.
|
|
|
847
846
|
| `S6` | `test/execute.test.ts` (fail-closed annotations, activity parity) |
|
|
848
847
|
| `S7` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (batch cap) |
|
|
849
848
|
| `S8`, `E1`, `X11` | both guest-contract executors (caught call, namespace, discovery, utility, batch-validation, budget, and forgery cases; typed batch equivalence) |
|
|
849
|
+
| `S9` | `test/result-shapes.test.ts` (value exclusion, bounds, merging, LRU and time expiry, runtime isolation, read-only admission, declared precedence, definition invalidation, unwrapped MCP results, discovery provenance, copy isolation, and failure isolation) |
|
|
850
850
|
| `E2`, `E8` | `test/guest-api-contract.test.ts` (code → `retryable`, caught, batch, and uncaught validation recovery), `test/meta-tools.test.ts` (direct, destructive, batch, provider fallback), `test/validate.test.ts` (bounded payload-free findings), `test/errors.test.ts` |
|
|
851
851
|
| `E3` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (`auth_required`) |
|
|
852
852
|
| `E4` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (destructive) |
|
|
@@ -268,6 +268,16 @@ Tool calls must use the shared invocation path. That keeps direct calls, batch
|
|
|
268
268
|
children, and code-mode host calls aligned on safety, retries, admission,
|
|
269
269
|
timeouts, validation, result guards, and typed failures.
|
|
270
270
|
|
|
271
|
+
That path also learns an observed output schema after a successful explicitly
|
|
272
|
+
read-only call when the catalog declared none. The observation retains field
|
|
273
|
+
names and broad JSON types rather than a result. Object fields stay optional,
|
|
274
|
+
additional fields stay allowed, discovery labels the source `observed`, and any
|
|
275
|
+
later provider declaration wins. Property names can be user-authored data, so
|
|
276
|
+
the bounded cache stays in this process or Worker isolate and expires entries
|
|
277
|
+
after 24 hours. An exact tool-definition comparison rejects stale shapes. This
|
|
278
|
+
does not weaken the catalog completeness rule or the refusal of result sampling:
|
|
279
|
+
no catalog read executes a tool.
|
|
280
|
+
|
|
271
281
|
Connector usage guides are configuration too. `usageGuide` accepts the
|
|
272
282
|
historical markdown string or `{ content, summary?, required? }`; the latter
|
|
273
283
|
lets discovery explain what the guide covers without loading it. The summary
|
|
@@ -78,21 +78,35 @@ headed to `call_tool` or generated code; `safety: "approvalRequired"` finds the
|
|
|
78
78
|
complementary set that must cross `call_destructive_tool`. Omitting `safety`,
|
|
79
79
|
or setting it to `"all"`, preserves the complete configured catalog. This is
|
|
80
80
|
only a discovery filter: it neither grants authority nor changes invocation admission.
|
|
81
|
-
`includeSchemas: "compact"` adds each match's input and any declared
|
|
82
|
-
shape.
|
|
81
|
+
`includeSchemas: "compact"` adds each match's input and any provider-declared
|
|
82
|
+
output shape. When the provider declared none but an earlier successful call
|
|
83
|
+
learned one, the same field carries the open observed schema beside
|
|
84
|
+
`outputSchemaSource: "observed"`. That marker matters: observed fields and broad
|
|
85
|
+
JSON types are routing evidence, not a provider contract, and every object field
|
|
86
|
+
remains optional and open to unseen names. A provider declaration always wins.
|
|
87
|
+
Bounded plain-object schemas also expose `inputKeys`,
|
|
83
88
|
`requiredInputKeys`, and `outputKeys`; a zero-input object keeps
|
|
84
89
|
`requiredInputKeys: []`, while an output object with no declared properties
|
|
85
90
|
omits `outputKeys`. A truncated shape omits its corresponding list rather than
|
|
86
91
|
repeating a large partial inventory. Matches carry declared
|
|
87
92
|
behavior annotations. Lexical rank is only one signal: select a candidate whose
|
|
88
93
|
required inputs are available, whose schema is complete enough for the call,
|
|
89
|
-
and whose safety and
|
|
94
|
+
and whose safety and available outputs fit the work. A reducer uses `outputKeys`
|
|
90
95
|
before inspecting the value; it does not assume a collection is named `items`
|
|
91
96
|
or `results`. When that shape is sufficient, call the returned address directly. Reserve schema
|
|
92
97
|
expansion through `connecta.describe` for a search without schemas, an
|
|
93
98
|
ambiguous compact shape, or exact
|
|
94
99
|
constraints that require `format: "json"`.
|
|
95
100
|
|
|
101
|
+
Observed schemas originate no provider traffic. A successful explicitly
|
|
102
|
+
read-only call the user already made contributes names and broad types after
|
|
103
|
+
Connecta unwraps the result. Arguments, scalar values, raw results, code,
|
|
104
|
+
credentials, and errors are not retained, though property names may themselves
|
|
105
|
+
be user-authored. Shapes merge in a 256-entry runtime cache for 24 hours under
|
|
106
|
+
the exact tool definition that produced them. A changed definition, process
|
|
107
|
+
restart, or Worker isolate eviction starts cold. Observation cannot fail the
|
|
108
|
+
call, and the declared catalog remains the fallback.
|
|
109
|
+
|
|
96
110
|
Compact search is deliberately a routing view, not a second copy of connector
|
|
97
111
|
documentation. Tool purposes are capped at 160 characters, connector
|
|
98
112
|
descriptions and property prose are omitted, required input fields render
|
|
@@ -75,7 +75,7 @@ optional.
|
|
|
75
75
|
| `connectors` | — (required) | the connector set ([connectors](./connectors.md)) |
|
|
76
76
|
| `executor` | — (required) | the sandbox `execute_code` runs in ([code mode](./code-mode.md#what-an-executor-must-implement)) |
|
|
77
77
|
| `auth?` | none ⇒ open (dev only) | one `InboundAuth` or an array; bearer providers are checked before Clerk ([inbound auth](./auth.md)) |
|
|
78
|
-
| `storage?` | `memoryStorage()` | the one state seam ([storage](./storage-and-credentials.md)) |
|
|
78
|
+
| `storage?` | `memoryStorage()` | the one state seam for catalogs, result paging, credentials, and access tokens ([storage](./storage-and-credentials.md)) |
|
|
79
79
|
| `publicUrl?` | per-request origin | public base URL; an HTTPS value also redirects inbound HTTP |
|
|
80
80
|
| `logger?` | `console`, prefixed `[connecta]` | `{ debug, info, warn, error }` |
|
|
81
81
|
| `branding?` | neutral Connecta defaults | operator-page and OAuth result-page labels and marks |
|
|
@@ -250,6 +250,7 @@ in.
|
|
|
250
250
|
| `remote-mcp-credential.test.ts` | `remoteMcp()` drawing a static key from `/credentials`: the declared slot and its refusal of named fields and bad header names, header framing (bearer, bare, and the two `Basic` forms) observed on the wire, an empty slot failing as `auth_required` rather than reaching the downstream, a value carrying a control character refused before framing and absent from every surface — `call_tool`, `status`, the Test result, `lastError`, and the thrown error — rotation replacing the cached client and a connect already in flight while a wiped value fails the next call, the Test action's catalog probe and scope close, the cleartext-destination warning, and the vault and `authorize_connector` handoff end to end |
|
|
251
251
|
| `remote-mcp-pagination.test.ts` | the `tools/list` cursor chain in both directions — exact cursor handoff, first-wins dedup, a failed later page rejecting rather than returning its prefix, the runaway backstops, the tool-metadata re-prime across pages, and paginated catalogs reaching the discovery path |
|
|
252
252
|
| `request-admission.test.ts` | `/mcp` bounded before auth, the stable 503 and `Retry-After`, health and operator responsiveness under saturation, payload-free counters, queued cancellation, shutdown rejection while active work drains, and the separate fallback code pool |
|
|
253
|
+
| `result-shapes.test.ts` | passive output-shape learning: value-free bounded inference, merging, 256-entry LRU eviction, 24-hour expiry, runtime isolation, read-only admission, declared-schema precedence, definition-change invalidation, discovery provenance, and failure isolation |
|
|
253
254
|
| `revenuecat-provider.test.ts` / `revenuecat-registry.test.ts` | the RevenueCat proxy's per-project key scoping and account-wide OAuth guides, its purpose-bearing summary, the argued borderline verdicts in its digest-free manifest, and the deliberately unclassified `render-paywall-screenshot`; then two project-scoped keys as two connectors in a real deployment |
|
|
254
255
|
| `server.test.ts` | end-to-end `/mcp` (401 → compact initialize instructions → seven compact definitions with bounded connector inventory → complete usage skill → `call_tool`), conditional guide pointers, open routes, Clerk `.well-known` metadata without network, code mode, and deferred catalog reads through both discovery surfaces |
|
|
255
256
|
| `server-route-contracts.test.ts` | the route contracts `server.ts` must keep byte-identical: every built-in answered ahead of connector routes inside the security wrapper, open data-free shells with framing denied, per-route auth and same-origin requirements with exact 401/403/405 bodies, and OAuth `verifyState`-before-`finishAuth` ordering |
|
|
@@ -57,7 +57,7 @@ exist so far:
|
|
|
57
57
|
| --- | --- | --- |
|
|
58
58
|
| **pre-template** | before 0.10.2 | no `connecta init` existed; hand-written, or copied from the retired `examples/node` |
|
|
59
59
|
| **A** | 0.10.2 – 0.15.1 | `.env.example`, `.gitignore`, `AGENTS.md`, `CLAUDE.md`, `README.md`, `package.json`, `src/index.ts`, `tsconfig.json` |
|
|
60
|
-
| **B** | 0.16.0 – 0.18.
|
|
60
|
+
| **B** | 0.16.0 – 0.18.3 | adds `.dockerignore`, `Dockerfile`, `docker-compose.yml`, and `src/file-activity.ts`; `src/index.ts` grows the four commented operator blocks; `.env.example` ships `CONNECTA_TOKEN=` empty |
|
|
61
61
|
|
|
62
62
|
Generation A is a decade in template years and identifying it precisely does
|
|
63
63
|
not matter, because you are about to reconstruct it exactly rather than guess
|
|
@@ -106,7 +106,7 @@ know what to preserve, once to know what to re-verify at the end.
|
|
|
106
106
|
### Bump the pin and install
|
|
107
107
|
|
|
108
108
|
```sh
|
|
109
|
-
npm pkg set dependencies.@zackbart/connecta=0.18.
|
|
109
|
+
npm pkg set dependencies.@zackbart/connecta=0.18.3
|
|
110
110
|
npm install
|
|
111
111
|
```
|
|
112
112
|
|
|
@@ -130,7 +130,7 @@ Generate the *current* template beside the base you already made, into the same
|
|
|
130
130
|
`$SCRATCH`:
|
|
131
131
|
|
|
132
132
|
```sh
|
|
133
|
-
(cd "$SCRATCH" && npx @zackbart/connecta@0.18.
|
|
133
|
+
(cd "$SCRATCH" && npx @zackbart/connecta@0.18.3 init current)
|
|
134
134
|
```
|
|
135
135
|
|
|
136
136
|
You now have a three-way merge with a real base: `$SCRATCH/base` is what this
|
|
@@ -186,7 +186,7 @@ A deployment older than 0.10.2 has no base to diff against. Do not try to
|
|
|
186
186
|
manufacture one. Instead:
|
|
187
187
|
|
|
188
188
|
1. `SCRATCH=$(mktemp -d)`, then
|
|
189
|
-
`(cd "$SCRATCH" && npx @zackbart/connecta@0.18.
|
|
189
|
+
`(cd "$SCRATCH" && npx @zackbart/connecta@0.18.3 init current)` — there is no
|
|
190
190
|
`base` leg here, only the current template to read from.
|
|
191
191
|
2. Copy `$SCRATCH/current` into the deployment file by file, **skipping
|
|
192
192
|
`src/index.ts`**.
|
|
@@ -207,6 +207,17 @@ first, so cross them bottom-up: start at the oldest one still above this
|
|
|
207
207
|
deployment's pin and work back up the page, because each boundary assumes the
|
|
208
208
|
older ones are already done.
|
|
209
209
|
|
|
210
|
+
### 0.18.2 → 0.18.3
|
|
211
|
+
|
|
212
|
+
Nothing throws for an existing deployment, and the version bump alone crosses
|
|
213
|
+
this boundary. Successful explicitly read-only calls whose provider declares
|
|
214
|
+
no output schema now teach later discovery the result's field names and broad
|
|
215
|
+
JSON types. The open optional-field shape is labeled
|
|
216
|
+
`outputSchemaSource: "observed"`, lives only in a bounded runtime cache, and
|
|
217
|
+
starts cold after 24 hours, a process restart, or Worker isolate eviction. No
|
|
218
|
+
configuration or storage migration is involved, and discovery still never
|
|
219
|
+
executes a tool.
|
|
220
|
+
|
|
210
221
|
### 0.18.1 → 0.18.2
|
|
211
222
|
|
|
212
223
|
Nothing throws for an existing deployment, and the version bump alone crosses
|
package/ethos.md
CHANGED
|
@@ -111,11 +111,11 @@ proposing one without a new argument is not.
|
|
|
111
111
|
| View-initiated read calls from program UI | accepted | named bindings materially improve refresh, cursor pagination, and drill-down without persistence or a new tool; the trusted shell delegates only to the existing fail-closed `call_tool`, and the one-string UI remains display-only ([evidence](./documentation/program-ui-read-calls.md), [#287](https://github.com/zackbart/connecta/issues/287), [#289](https://github.com/zackbart/connecta/issues/289)) |
|
|
112
112
|
| View-initiated mutation calls from program UI | gated | live-read utility says nothing about write consent: a click is not approval, stale/replayed effects need a host-tested story, and the ordinary destructive path keeps the action in the transcript ([#287](https://github.com/zackbart/connecta/issues/287)) |
|
|
113
113
|
| Result sampling on the catalog surface (`sample` / `dryRun`) | refused | sampling is execution and cannot ride a catalog read; most tools carry required arguments no sampler can invent, and undeclared `outputSchema` (measured 0/30 and 3/30 on real deployments) is a real gap that is not a sampleable one — a program that checks the shape before rendering already hands back the first record inside the run it was going to make anyway, at zero new surface ([#282](https://github.com/zackbart/connecta/issues/282)) |
|
|
114
|
+
| Passive observed output schemas | accepted | the sampling refusal stands: discovery originates no call and invents no arguments; instead, a successful explicitly read-only call whose provider declared no `outputSchema` records field names and broad JSON types only, under strict depth, breadth, property-name, node, and byte bounds, then merges that open optional-field shape in a 256-entry process-local cache for 24 hours; property names may be user-authored, search and describe label the shape `outputSchemaSource: "observed"`, a materially changed tool definition cannot inherit it, a provider declaration always wins, and no argument, scalar value, raw result, code, credential, or error is retained; this is new evidence rather than a rewrite of #282's facts: Blacksmith measured a value-free warm shape cache cutting one Linear code-mode task from 116.6 s / $1.91 to 56.6 s / $1.06, while live Connecta catalogs measured 246/378 missing on BePresent and 0/90 missing on OneMany ([study](https://www.blacksmith.sh/blog/code-smith-code-mode), [#442](https://github.com/zackbart/connecta/issues/442)) |
|
|
114
115
|
| Legacy embedded `UIResource` delivery | refused | superseded upstream and rendered by none of the clients connecta faces; per-request minted URIs also fight the caching the Apps spec assumes ([#266](https://github.com/zackbart/connecta/issues/266)) |
|
|
115
116
|
## Invariants
|
|
116
117
|
|
|
117
|
-
One line each; the enforcing tests live beside the subsystem documentation.
|
|
118
|
-
Breaking one is not a bug fix — it is a design change wearing a disguise.
|
|
118
|
+
One line each; the enforcing tests live beside the subsystem documentation. Breaking one is not a bug fix — it is a design change wearing a disguise.
|
|
119
119
|
|
|
120
120
|
- **Fail-closed read-only.** A missing, false, or contradictory annotation
|
|
121
121
|
never gets the benefit of the doubt.
|
|
@@ -131,6 +131,7 @@ Breaking one is not a bug fix — it is a design change wearing a disguise.
|
|
|
131
131
|
is never cached, persisted, or served as if it were small.
|
|
132
132
|
- **Activity is payload-free by construction.** The event type has nowhere to
|
|
133
133
|
put arguments, results, code, or raw error text.
|
|
134
|
+
- **An observed shape is never a declaration.** It contains field names and broad JSON types only, remains open and optional, is labeled on discovery, and disappears behind any provider-declared output schema.
|
|
134
135
|
- **Credentials never leave the host.** Encrypted at rest, readable only by
|
|
135
136
|
the owning connector, never rendered by any surface.
|
|
136
137
|
- **Import-graph purity.** Nothing reachable from the root entry imports a
|
|
@@ -145,5 +146,4 @@ Breaking one is not a bug fix — it is a design change wearing a disguise.
|
|
|
145
146
|
- **Structural mistakes throw at construction.** A deployment that boots into
|
|
146
147
|
the wrong shape is worse than one that refuses to boot.
|
|
147
148
|
|
|
148
|
-
---
|
|
149
149
|
Connecta began as a radical simplification of [executor](https://github.com/UsefulSoftwareCo/executor); the table above is the record of that simplification holding.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zackbart/connecta",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "One MCP to rule them all — a single MCP endpoint aggregating many downstream connectors behind a code-first surface of seven meta-tools.",
|