@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/timeout.d.ts
CHANGED
|
@@ -5,13 +5,14 @@
|
|
|
5
5
|
export declare const DEFAULT_PROBE_TIMEOUT_MS = 30000;
|
|
6
6
|
/** A finite, positive integer number of milliseconds, or undefined. */
|
|
7
7
|
export declare function normalizeTimeoutMs(value: number | undefined): number | undefined;
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
/** Resolve after `ms`, or false when the caller aborts first. */
|
|
9
|
+
export declare function sleep(ms: number, signal?: AbortSignal): Promise<boolean>;
|
|
10
|
+
export interface DeadlineOptions {
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
timeoutError: Error;
|
|
14
|
+
}
|
|
15
|
+
export declare function withDeadline<T>(operation: (signal: AbortSignal) => Promise<T>, options: DeadlineOptions): Promise<T>;
|
|
15
16
|
/**
|
|
16
17
|
* Give one operation a caller-facing deadline and the matching cancellation
|
|
17
18
|
* signal. The timeout rejects with the stable, labelled error while aborting
|
package/dist/timeout.js
CHANGED
|
@@ -12,24 +12,53 @@ export function normalizeTimeoutMs(value) {
|
|
|
12
12
|
}
|
|
13
13
|
return Math.max(1, Math.trunc(value));
|
|
14
14
|
}
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const timer = setTimeout(() => {
|
|
24
|
-
reject(new Error(`${label} timed out after ${ms}ms`));
|
|
25
|
-
}, ms);
|
|
26
|
-
promise.then((value) => {
|
|
15
|
+
/** Resolve after `ms`, or false when the caller aborts first. */
|
|
16
|
+
export function sleep(ms, signal) {
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
let settled = false;
|
|
19
|
+
const finish = (value) => {
|
|
20
|
+
if (settled)
|
|
21
|
+
return;
|
|
22
|
+
settled = true;
|
|
27
23
|
clearTimeout(timer);
|
|
24
|
+
signal?.removeEventListener("abort", cancel);
|
|
28
25
|
resolve(value);
|
|
29
|
-
}
|
|
26
|
+
};
|
|
27
|
+
const timer = setTimeout(() => finish(true), ms);
|
|
28
|
+
const cancel = () => finish(false);
|
|
29
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
30
|
+
if (signal?.aborted)
|
|
31
|
+
cancel();
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export function withDeadline(operation, options) {
|
|
35
|
+
const controller = new AbortController();
|
|
36
|
+
const forwardAbort = () => controller.abort(options.signal?.reason);
|
|
37
|
+
options.signal?.addEventListener("abort", forwardAbort, { once: true });
|
|
38
|
+
if (options.signal?.aborted)
|
|
39
|
+
forwardAbort();
|
|
40
|
+
let rejectAbort;
|
|
41
|
+
const aborted = new Promise((_, reject) => { rejectAbort = reject; });
|
|
42
|
+
const onAbort = () => rejectAbort(controller.signal.reason ?? options.timeoutError);
|
|
43
|
+
controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
44
|
+
if (controller.signal.aborted)
|
|
45
|
+
onAbort();
|
|
46
|
+
const timer = options.timeoutMs === undefined
|
|
47
|
+
? undefined
|
|
48
|
+
: setTimeout(() => controller.abort(options.timeoutError), options.timeoutMs);
|
|
49
|
+
let work;
|
|
50
|
+
try {
|
|
51
|
+
work = operation(controller.signal);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
work = Promise.reject(error);
|
|
55
|
+
}
|
|
56
|
+
return Promise.race([work, aborted]).finally(() => {
|
|
57
|
+
controller.abort();
|
|
58
|
+
if (timer)
|
|
30
59
|
clearTimeout(timer);
|
|
31
|
-
|
|
32
|
-
|
|
60
|
+
controller.signal.removeEventListener("abort", onAbort);
|
|
61
|
+
options.signal?.removeEventListener("abort", forwardAbort);
|
|
33
62
|
});
|
|
34
63
|
}
|
|
35
64
|
/**
|
|
@@ -38,28 +67,8 @@ export function withTimeout(promise, ms, label) {
|
|
|
38
67
|
* any in-flight work that honors the signal.
|
|
39
68
|
*/
|
|
40
69
|
export function withAbortableTimeout(operation, ms, label) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const timer = setTimeout(() => {
|
|
45
|
-
controller.abort(timeoutError);
|
|
46
|
-
reject(timeoutError);
|
|
47
|
-
}, ms);
|
|
48
|
-
let promise;
|
|
49
|
-
try {
|
|
50
|
-
promise = operation(controller.signal);
|
|
51
|
-
}
|
|
52
|
-
catch (err) {
|
|
53
|
-
clearTimeout(timer);
|
|
54
|
-
reject(err);
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
promise.then((value) => {
|
|
58
|
-
clearTimeout(timer);
|
|
59
|
-
resolve(value);
|
|
60
|
-
}, (err) => {
|
|
61
|
-
clearTimeout(timer);
|
|
62
|
-
reject(err);
|
|
63
|
-
});
|
|
70
|
+
return withDeadline(operation, {
|
|
71
|
+
timeoutMs: ms,
|
|
72
|
+
timeoutError: new Error(`${label} timed out after ${ms}ms`),
|
|
64
73
|
});
|
|
65
74
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -311,8 +311,8 @@ export interface Connector {
|
|
|
311
311
|
* download link minted by one of the connector's tools. Called only after
|
|
312
312
|
* every built-in route misses, so a connector can never shadow `/mcp`,
|
|
313
313
|
* `/`, `/credentials`, `/activity`, `/health`, or the credential API. The
|
|
314
|
-
* first connector to return a
|
|
315
|
-
*
|
|
314
|
+
* first connector to return a Response wins, in registration order; return
|
|
315
|
+
* null to decline. See documentation/architecture.md route 9.
|
|
316
316
|
*
|
|
317
317
|
* These routes are PUBLIC: connecta applies no auth gate to them. A
|
|
318
318
|
* connector that serves data here MUST authenticate the request itself — for
|
|
@@ -351,7 +351,7 @@ export interface ExecutorProvider {
|
|
|
351
351
|
* Optional trusted sandbox-side setup run after provider globals exist.
|
|
352
352
|
* Connecta uses this to install lazy connector namespace proxies without
|
|
353
353
|
* materializing one host closure per tool. This is host-authored code, never
|
|
354
|
-
* model input.
|
|
354
|
+
* model input. See documentation/code-mode.md#what-an-executor-must-implement.
|
|
355
355
|
*/
|
|
356
356
|
prelude?: string;
|
|
357
357
|
}
|
package/dist/ui.d.ts
CHANGED
|
@@ -26,11 +26,6 @@ export declare function resolveBranding(branding?: ConnectaBranding): ResolvedBr
|
|
|
26
26
|
* `resolveBranding` does — a warning helper must never throw.
|
|
27
27
|
*/
|
|
28
28
|
export declare function droppedBrandingUrls(branding?: ConnectaBranding): string[];
|
|
29
|
-
/**
|
|
30
|
-
* True only for absolute `http:`/`https:` URLs. Downstream connectors control
|
|
31
|
-
* their `authorizationUrl`, so a hostile/misconfigured one could hand back a
|
|
32
|
-
* `javascript:` (or other) scheme; gate it before it can become an href.
|
|
33
|
-
*/
|
|
34
29
|
export declare function isSafeHttpUrl(url: unknown): boolean;
|
|
35
30
|
/**
|
|
36
31
|
* True for values allowed in the page's `<link rel="icon" href>`: an absolute
|
|
@@ -52,26 +47,7 @@ export declare function isSafeHttpUrl(url: unknown): boolean;
|
|
|
52
47
|
* on its own it would accept an authority that happened to equal the probe host.
|
|
53
48
|
*/
|
|
54
49
|
export declare function isSafeIconHref(href: unknown): boolean;
|
|
55
|
-
/**
|
|
56
|
-
* True only for an absolute `https:` URL — the gate every `uiAuth` URL passes:
|
|
57
|
-
* `frontendApiUrl`, which becomes the operator shell's sign-in loader source,
|
|
58
|
-
* and `signInUrl`/`signUpUrl`, which ClerkJS uses as *navigation targets* when
|
|
59
|
-
* the operator signs in. With those three gated, no operator-config value
|
|
60
|
-
* reaches the browser in a URL position — attribute or navigation — without
|
|
61
|
-
* validation, and there is no exception left to remember.
|
|
62
|
-
*
|
|
63
|
-
* Stricter than `isSafeHttpUrl` on purpose: no `http:` carve-out, no loopback
|
|
64
|
-
* carve-out, and no relative form. Nobody types `frontendApiUrl` — the shipped
|
|
65
|
-
* Clerk adapter derives it from the publishable key, and Clerk's Frontend API is
|
|
66
|
-
* always https — and a cleartext script source on an operator page would be a
|
|
67
|
-
* downgrade even where a browser's mixed-content rules had not already blocked
|
|
68
|
-
* it. `signInUrl`/`signUpUrl` *are* typed by the operator, but what belongs
|
|
69
|
-
* there is a hosted Account Portal address (`https://accounts.<domain>` or
|
|
70
|
-
* `https://<slug>.accounts.dev`), which is https as well; `http:` would carry a
|
|
71
|
-
* sign-in over cleartext, and a path relative to this origin is meaningless
|
|
72
|
-
* because this server hosts no sign-in page of its own. So the looser gate would
|
|
73
|
-
* buy nothing real, and the same strictness holds for all three.
|
|
74
|
-
*/
|
|
50
|
+
/** Absolute HTTPS gate for the `UiAuthConfig` URL fields documented in types.ts. */
|
|
75
51
|
export declare function isSafeHttpsUrl(url: unknown): boolean;
|
|
76
52
|
/**
|
|
77
53
|
* Names of the `uiAuth` URLs an inbound-auth provider supplied that failed their
|
package/dist/ui.js
CHANGED
|
@@ -95,17 +95,19 @@ export function droppedBrandingUrls(branding) {
|
|
|
95
95
|
* their `authorizationUrl`, so a hostile/misconfigured one could hand back a
|
|
96
96
|
* `javascript:` (or other) scheme; gate it before it can become an href.
|
|
97
97
|
*/
|
|
98
|
-
|
|
98
|
+
function safeUrl(url, schemes) {
|
|
99
99
|
if (typeof url !== "string")
|
|
100
100
|
return false;
|
|
101
101
|
try {
|
|
102
|
-
|
|
103
|
-
return scheme === "http:" || scheme === "https:";
|
|
102
|
+
return schemes.includes(new URL(url).protocol);
|
|
104
103
|
}
|
|
105
104
|
catch {
|
|
106
105
|
return false;
|
|
107
106
|
}
|
|
108
107
|
}
|
|
108
|
+
export function isSafeHttpUrl(url) {
|
|
109
|
+
return safeUrl(url, ["http:", "https:"]);
|
|
110
|
+
}
|
|
109
111
|
/**
|
|
110
112
|
* Only the second check's base; any origin works because the check is whether
|
|
111
113
|
* the href stays on whatever origin it is resolved against. It is deliberately
|
|
@@ -149,35 +151,9 @@ export function isSafeIconHref(href) {
|
|
|
149
151
|
return false;
|
|
150
152
|
}
|
|
151
153
|
}
|
|
152
|
-
/**
|
|
153
|
-
* True only for an absolute `https:` URL — the gate every `uiAuth` URL passes:
|
|
154
|
-
* `frontendApiUrl`, which becomes the operator shell's sign-in loader source,
|
|
155
|
-
* and `signInUrl`/`signUpUrl`, which ClerkJS uses as *navigation targets* when
|
|
156
|
-
* the operator signs in. With those three gated, no operator-config value
|
|
157
|
-
* reaches the browser in a URL position — attribute or navigation — without
|
|
158
|
-
* validation, and there is no exception left to remember.
|
|
159
|
-
*
|
|
160
|
-
* Stricter than `isSafeHttpUrl` on purpose: no `http:` carve-out, no loopback
|
|
161
|
-
* carve-out, and no relative form. Nobody types `frontendApiUrl` — the shipped
|
|
162
|
-
* Clerk adapter derives it from the publishable key, and Clerk's Frontend API is
|
|
163
|
-
* always https — and a cleartext script source on an operator page would be a
|
|
164
|
-
* downgrade even where a browser's mixed-content rules had not already blocked
|
|
165
|
-
* it. `signInUrl`/`signUpUrl` *are* typed by the operator, but what belongs
|
|
166
|
-
* there is a hosted Account Portal address (`https://accounts.<domain>` or
|
|
167
|
-
* `https://<slug>.accounts.dev`), which is https as well; `http:` would carry a
|
|
168
|
-
* sign-in over cleartext, and a path relative to this origin is meaningless
|
|
169
|
-
* because this server hosts no sign-in page of its own. So the looser gate would
|
|
170
|
-
* buy nothing real, and the same strictness holds for all three.
|
|
171
|
-
*/
|
|
154
|
+
/** Absolute HTTPS gate for the `UiAuthConfig` URL fields documented in types.ts. */
|
|
172
155
|
export function isSafeHttpsUrl(url) {
|
|
173
|
-
|
|
174
|
-
return false;
|
|
175
|
-
try {
|
|
176
|
-
return new URL(url).protocol === "https:";
|
|
177
|
-
}
|
|
178
|
-
catch {
|
|
179
|
-
return false;
|
|
180
|
-
}
|
|
156
|
+
return safeUrl(url, ["https:"]);
|
|
181
157
|
}
|
|
182
158
|
/**
|
|
183
159
|
* Names of the `uiAuth` URLs an inbound-auth provider supplied that failed their
|
|
@@ -303,18 +279,21 @@ export async function buildUiData(registry, baseUrl, serverInfo, credentialVault
|
|
|
303
279
|
: {}),
|
|
304
280
|
};
|
|
305
281
|
});
|
|
282
|
+
const credentialCard = {
|
|
283
|
+
label: c.credential.label,
|
|
284
|
+
...(c.credential.description
|
|
285
|
+
? { description: c.credential.description }
|
|
286
|
+
: {}),
|
|
287
|
+
...(c.credential.placeholder
|
|
288
|
+
? { placeholder: c.credential.placeholder }
|
|
289
|
+
: {}),
|
|
290
|
+
};
|
|
306
291
|
try {
|
|
307
292
|
const metadata = await credentialVault.metadata(c.id);
|
|
308
293
|
const fields = credentialFields(metadata);
|
|
309
294
|
const shape = storedCredentialShape(c.credential, metadata?.fields ?? null);
|
|
310
295
|
credential = {
|
|
311
|
-
|
|
312
|
-
...(c.credential.description
|
|
313
|
-
? { description: c.credential.description }
|
|
314
|
-
: {}),
|
|
315
|
-
...(c.credential.placeholder
|
|
316
|
-
? { placeholder: c.credential.placeholder }
|
|
317
|
-
: {}),
|
|
296
|
+
...credentialCard,
|
|
318
297
|
...(fields?.length ? { fields } : {}),
|
|
319
298
|
configured: shape.state === "valid",
|
|
320
299
|
removable: Boolean(metadata),
|
|
@@ -341,13 +320,7 @@ export async function buildUiData(registry, baseUrl, serverInfo, credentialVault
|
|
|
341
320
|
catch {
|
|
342
321
|
const fields = credentialFields();
|
|
343
322
|
credential = {
|
|
344
|
-
|
|
345
|
-
...(c.credential.description
|
|
346
|
-
? { description: c.credential.description }
|
|
347
|
-
: {}),
|
|
348
|
-
...(c.credential.placeholder
|
|
349
|
-
? { placeholder: c.credential.placeholder }
|
|
350
|
-
: {}),
|
|
323
|
+
...credentialCard,
|
|
351
324
|
...(fields?.length ? { fields } : {}),
|
|
352
325
|
configured: false,
|
|
353
326
|
removable: true,
|
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
|
|
@@ -140,4 +140,4 @@ typed error code.
|
|
|
140
140
|
| --- | --- |
|
|
141
141
|
| Independent partitions, exact rolling-window reset and retry, queued cancellation charging no budget, synchronous cancel during partition derivation, validated values snapshotted rather than read from mutable config, bounded partition state and contained `partitionKey` failures, empty and multi-rule policies refused | `test/call-admission.test.ts` (controller) |
|
|
142
142
|
| One base-registry limiter shared by direct and program calls, batch bounds with input order preserved, cancellation threading, no dispatch or retry or health poisoning after cancellation, short proactive windows retried without poisoning health, payload-free `/health` aggregates | `test/call-admission.test.ts` (integration, Node + Workers) |
|
|
143
|
-
| Where provider budgets are allowed to come from at all | [provider conventions P12](./provider-conventions.md#p12--declare-an-admission-budget-only-when-the-provider-documents-a-number), [provider audit](
|
|
143
|
+
| Where provider budgets are allowed to come from at all | [provider conventions P12](./provider-conventions.md#p12--declare-an-admission-budget-only-when-the-provider-documents-a-number), [provider audit](https://github.com/zackbart/connecta/blob/main/records/provider-audit.md) |
|
|
@@ -410,4 +410,4 @@ the window notices.
|
|
|
410
410
|
This connection is audited against
|
|
411
411
|
[the provider conventions](./provider-conventions.md). Its verdict per
|
|
412
412
|
convention, including every recorded exception, is the Cloudflare section of
|
|
413
|
-
[the provider audit](
|
|
413
|
+
[the provider audit](https://github.com/zackbart/connecta/blob/main/records/provider-audit.md).
|
|
@@ -5,14 +5,14 @@ is promised: what it can reach, what it gets back, how failures look, what it
|
|
|
5
5
|
may retry, what bounds it runs under, and what its execution leaves behind in
|
|
6
6
|
the activity surface. It is the interface a model actually programs against, so
|
|
7
7
|
it is specified in prose first and implemented second — the same discipline the
|
|
8
|
-
[MCP spec bump](
|
|
8
|
+
[MCP spec bump](https://github.com/zackbart/connecta/blob/main/records/mcp-2026-07-28.md) followed.
|
|
9
9
|
|
|
10
10
|
Two executors implement this document: QuickJS in a child process on Node, and
|
|
11
11
|
`DynamicWorkerExecutor` from `@cloudflare/codemode` on Workers. Divergence between
|
|
12
12
|
them is a bug unless it appears in [Executor exceptions](#executor-exceptions),
|
|
13
13
|
which names the reason. Anyone can implement a third from this document alone.
|
|
14
14
|
|
|
15
|
-
The [code-first exploration](
|
|
15
|
+
The [code-first exploration](https://github.com/zackbart/connecta/blob/main/records/code-first-exploration.md) is the evidence behind
|
|
16
16
|
the direction; [`ethos.md`](../ethos.md) carries the verdicts. Where its prototype
|
|
17
17
|
and this document disagree, this document wins. Clause identifiers (`A1`, `E3`, …)
|
|
18
18
|
are stable and cited by the tests in [Verification](#verification).
|
|
@@ -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`
|
|
@@ -176,7 +173,7 @@ word gets `_` appended (`my-service.get.thing` → `my_service.get_thing`). The
|
|
|
176
173
|
globals are lazy: no catalog is fetched until a program touches one. The
|
|
177
174
|
bounded deployment inventory in the `execute_code` description shows each
|
|
178
175
|
canonical connector id and labels the shortcut only when it differs; the
|
|
179
|
-
[discovery guide](./meta-tools.md#discovery-context) defines that bound.
|
|
176
|
+
[discovery guide](./meta-tools.md#discovery-context) defines that bound. The sugar is frozen: every expansion invents a collision class `A1` already solves ([#223](https://github.com/zackbart/connecta/issues/223)).
|
|
180
177
|
|
|
181
178
|
**A3.** A shortcut that resolves to more than one tool fails closed with
|
|
182
179
|
`ambiguous_tool_alias`, naming the colliding tool names and pointing at
|
|
@@ -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
|
|
@@ -427,7 +426,7 @@ on data nobody asked for.
|
|
|
427
426
|
|
|
428
427
|
**R5.** `console.log`, `console.warn`, and `console.error` are captured in call order and returned as a single `logs` string, capped at 4,000 characters with a truncation marker. Logs survive failure — they ride along with the error result, which is what makes them worth writing. How a non-string argument renders is not contract (`X4`).
|
|
429
428
|
|
|
430
|
-
**R6.** Nothing else is added to a normal program result. Passing `diagnostics: true` adds one request-local, payload-free `diagnostics` block; a program that emitted adds `emitted: N` and its blocks (`M2`). Omitted, `false`, and emit-free are byte-for-byte the ordinary response path.
|
|
429
|
+
**R6.** Nothing else is added to a normal program result. Passing `diagnostics: true` adds one request-local, payload-free `diagnostics` block; a program that emitted adds `emitted: N` and its blocks (`M2`). Omitted, `false`, and emit-free are byte-for-byte the ordinary response path. Diagnostics exist so catalog, connector, and executor costs are distinguishable without persisting payloads or charging normal responses ([#247](https://github.com/zackbart/connecta/issues/247)).
|
|
431
430
|
|
|
432
431
|
**R7.** Timing separates admission, provider setup, total executor wall time, catalog work, and connector work. Catalog and connector values are cumulative, so parallel work can exceed executor wall time. Each used operation kind (`search`, `describe`, `call`, `batch`) gets one aggregate with count, failures, duration, returned serialized bytes, and catalog/connector time; batch adds only its total child count.
|
|
433
432
|
|
|
@@ -438,7 +437,7 @@ on data nobody asked for.
|
|
|
438
437
|
MCP-native output a return value cannot carry: base64 is not projectable, so a
|
|
439
438
|
block that survives intake uncapped (`S5`) must not die at the `R2` exit
|
|
440
439
|
guard. The argument and the refused alternatives live in the
|
|
441
|
-
[design record](
|
|
440
|
+
[design record](https://github.com/zackbart/connecta/blob/main/records/rich-output-design.md) and `ethos.md`
|
|
442
441
|
([#267](https://github.com/zackbart/connecta/issues/267),
|
|
443
442
|
[#270](https://github.com/zackbart/connecta/issues/270)).
|
|
444
443
|
|
|
@@ -499,7 +498,7 @@ one MCP Apps view per successful run, assembled where composition already
|
|
|
499
498
|
happens. Programs supply HTML content and nothing else — the only `ui://` URI in
|
|
500
499
|
the system is connecta's build-time shell, so nothing a client could dereference
|
|
501
500
|
is derived from anything a program said. The argument, the refused shapes, and
|
|
502
|
-
the security posture live in the [design record](
|
|
501
|
+
the security posture live in the [design record](https://github.com/zackbart/connecta/blob/main/records/mcp-ui-design.md)
|
|
503
502
|
([#266](https://github.com/zackbart/connecta/issues/266),
|
|
504
503
|
[#277](https://github.com/zackbart/connecta/issues/277)); this section is the
|
|
505
504
|
contract, and it wins where the two disagree.
|
|
@@ -600,7 +599,7 @@ seven-tool surface, guest API, catalog, Apps delivery, and runtime do not change
|
|
|
600
599
|
([#286](https://github.com/zackbart/connecta/issues/286),
|
|
601
600
|
[#418](https://github.com/zackbart/connecta/issues/418)).
|
|
602
601
|
|
|
603
|
-
Bounded view reads follow normative [`V1`–`V8`](
|
|
602
|
+
Bounded view reads follow normative [`V1`–`V8`](https://github.com/zackbart/connecta/blob/main/records/program-ui-read-calls.md) ([#287](https://github.com/zackbart/connecta/issues/287), [#289](https://github.com/zackbart/connecta/issues/289)).
|
|
604
603
|
|
|
605
604
|
## Retry semantics
|
|
606
605
|
|
|
@@ -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) |
|
|
@@ -160,6 +160,29 @@ is that machinery extracted once ([#341](https://github.com/zackbart/connecta/is
|
|
|
160
160
|
one `guardedFetch({ provider, baseUrl, headers, maxResponseBytes, authenticate })`
|
|
161
161
|
factory returning the transport a connector sends every request through.
|
|
162
162
|
|
|
163
|
+
```ts
|
|
164
|
+
const send = guardedFetch({
|
|
165
|
+
provider: "Billing",
|
|
166
|
+
baseUrl: "https://billing.internal.example/v1",
|
|
167
|
+
maxResponseBytes: 4 * 1024 * 1024,
|
|
168
|
+
headers: { Accept: "application/json" },
|
|
169
|
+
authenticate: async (ctx) => {
|
|
170
|
+
const token = await ctx.credential?.get();
|
|
171
|
+
if (!token) throw new ConnectorCallError("auth_required", "...");
|
|
172
|
+
return { Authorization: `Bearer ${token}` };
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const invoice = await send(
|
|
177
|
+
{ method: "GET", path: `/invoices/${id}` },
|
|
178
|
+
ctx,
|
|
179
|
+
(response) => {
|
|
180
|
+
if (!response.ok) throw billingFailure(response.status);
|
|
181
|
+
return response.json();
|
|
182
|
+
},
|
|
183
|
+
);
|
|
184
|
+
```
|
|
185
|
+
|
|
163
186
|
What it owns is mechanical and provider-independent:
|
|
164
187
|
|
|
165
188
|
- **Confinement.** A request path is provider-relative, carries no query or
|
|
@@ -212,7 +235,7 @@ supplies the third caller that would settle it.
|
|
|
212
235
|
## MCP version skew
|
|
213
236
|
|
|
214
237
|
Connecta deliberately sits between protocol generations
|
|
215
|
-
([full revision inventory](
|
|
238
|
+
([full revision inventory](https://github.com/zackbart/connecta/blob/main/records/mcp-2026-07-28.md)):
|
|
216
239
|
|
|
217
240
|
- **Inbound:** `/mcp` serves both the 2026-07-28 revision and legacy 2025
|
|
218
241
|
clients. Modern clients negotiate with `server/discover` and do not send
|
|
@@ -268,6 +291,16 @@ Tool calls must use the shared invocation path. That keeps direct calls, batch
|
|
|
268
291
|
children, and code-mode host calls aligned on safety, retries, admission,
|
|
269
292
|
timeouts, validation, result guards, and typed failures.
|
|
270
293
|
|
|
294
|
+
That path also learns an observed output schema after a successful explicitly
|
|
295
|
+
read-only call when the catalog declared none. The observation retains field
|
|
296
|
+
names and broad JSON types rather than a result. Object fields stay optional,
|
|
297
|
+
additional fields stay allowed, discovery labels the source `observed`, and any
|
|
298
|
+
later provider declaration wins. Property names can be user-authored data, so
|
|
299
|
+
the bounded cache stays in this process or Worker isolate and expires entries
|
|
300
|
+
after 24 hours. An exact tool-definition comparison rejects stale shapes. This
|
|
301
|
+
does not weaken the catalog completeness rule or the refusal of result sampling:
|
|
302
|
+
no catalog read executes a tool.
|
|
303
|
+
|
|
271
304
|
Connector usage guides are configuration too. `usageGuide` accepts the
|
|
272
305
|
historical markdown string or `{ content, summary?, required? }`; the latter
|
|
273
306
|
lets discovery explain what the guide covers without loading it. The summary
|
package/documentation/linear.md
CHANGED
|
@@ -179,4 +179,4 @@ settings at construction.
|
|
|
179
179
|
This connection is audited against
|
|
180
180
|
[the provider conventions](./provider-conventions.md). Its verdict per
|
|
181
181
|
convention, including every recorded exception, is the Linear section of
|
|
182
|
-
[the provider audit](
|
|
182
|
+
[the provider audit](https://github.com/zackbart/connecta/blob/main/records/provider-audit.md).
|
|
@@ -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
|
|
@@ -145,4 +145,4 @@ either way and still needs restrained use.
|
|
|
145
145
|
This connection is audited against
|
|
146
146
|
[the provider conventions](./provider-conventions.md). Its verdict per
|
|
147
147
|
convention, including every recorded exception, is the Mixpanel section of
|
|
148
|
-
[the provider audit](
|
|
148
|
+
[the provider audit](https://github.com/zackbart/connecta/blob/main/records/provider-audit.md).
|
package/documentation/notion.md
CHANGED
|
@@ -268,4 +268,4 @@ from this connection, not hidden behind a generic call.
|
|
|
268
268
|
This connection is audited against
|
|
269
269
|
[the provider conventions](./provider-conventions.md). Its verdict per
|
|
270
270
|
convention, including every recorded exception, is the Notion section of
|
|
271
|
-
[the provider audit](
|
|
271
|
+
[the provider audit](https://github.com/zackbart/connecta/blob/main/records/provider-audit.md).
|