@zackbart/connecta 0.16.1 → 0.17.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 +102 -0
- package/dist/catalog-service.d.ts +4 -0
- package/dist/catalog-service.js +8 -1
- package/dist/catalog.js +114 -12
- package/dist/errors.d.ts +4 -6
- package/dist/execute.d.ts +5 -0
- package/dist/execute.js +229 -161
- package/dist/invocation.js +3 -1
- package/dist/meta-tools.d.ts +4 -0
- package/dist/meta-tools.js +46 -14
- package/dist/operator-ui/generated.d.ts +1 -1
- package/dist/operator-ui/generated.js +1 -1
- package/dist/operator-ui/model.d.ts +3 -1
- package/dist/providers/mixpanel.d.ts +3 -5
- package/dist/providers/mixpanel.js +73 -5
- package/dist/providers/stripe.d.ts +2 -2
- package/dist/providers/stripe.js +13 -11
- package/dist/registry.d.ts +32 -9
- package/dist/registry.js +217 -33
- package/dist/routes/mcp.js +6 -0
- package/dist/skills.d.ts +4 -0
- package/dist/skills.js +157 -18
- package/dist/types.d.ts +14 -2
- package/dist/ui.js +4 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +7 -4
- package/documentation/code-mode.md +45 -53
- package/documentation/connector-guides.md +24 -19
- package/documentation/connectors.md +13 -1
- package/documentation/meta-tools.md +26 -17
- package/documentation/mixpanel.md +20 -0
- package/documentation/operations.md +21 -18
- package/documentation/operator-ui.md +12 -2
- package/documentation/provider-audit.md +3 -3
- package/documentation/provider-conventions.md +26 -13
- package/documentation/stripe.md +45 -14
- package/documentation/upgrading.md +28 -4
- package/ethos.md +3 -3
- package/examples/worker/README.md +4 -3
- package/package.json +1 -1
- package/templates/node/package.json +1 -1
package/dist/routes/mcp.js
CHANGED
|
@@ -234,6 +234,9 @@ async function serveMcp(request, opts, baseUrl, actor, registry, runtimeContext)
|
|
|
234
234
|
? { discoveryConcurrency: opts.discoveryConcurrency }
|
|
235
235
|
: {}),
|
|
236
236
|
requestSignal: request.signal,
|
|
237
|
+
...(runtimeContext?.waitUntil
|
|
238
|
+
? { defer: runtimeContext.waitUntil.bind(runtimeContext) }
|
|
239
|
+
: {}),
|
|
237
240
|
});
|
|
238
241
|
registerExecuteTool(server, registry, {
|
|
239
242
|
baseUrl,
|
|
@@ -241,6 +244,9 @@ async function serveMcp(request, opts, baseUrl, actor, registry, runtimeContext)
|
|
|
241
244
|
logger: opts.logger,
|
|
242
245
|
...(activity ? { activity } : {}),
|
|
243
246
|
requestSignal: request.signal,
|
|
247
|
+
...(runtimeContext?.waitUntil
|
|
248
|
+
? { defer: runtimeContext.waitUntil.bind(runtimeContext) }
|
|
249
|
+
: {}),
|
|
244
250
|
...(opts.discoveryConcurrency !== undefined
|
|
245
251
|
? { discoveryConcurrency: opts.discoveryConcurrency }
|
|
246
252
|
: {}),
|
package/dist/skills.d.ts
CHANGED
|
@@ -10,6 +10,10 @@ export declare function hasConnectorGuides(connectors: readonly Connector[]): bo
|
|
|
10
10
|
export declare function connectorSkillName(connectorId: string): string;
|
|
11
11
|
/** The connector's guide, or undefined when it declares none (or a blank one). */
|
|
12
12
|
export declare function connectorGuide(connector: Connector): string | undefined;
|
|
13
|
+
/** Discovery budget for one connector-guide summary, including an ellipsis. */
|
|
14
|
+
export declare const GUIDE_SUMMARY_LENGTH = 120;
|
|
15
|
+
/** Normalize authored and derived summaries under one construction contract. */
|
|
16
|
+
export declare function normalizeGuideSummary(summary: string): string | undefined;
|
|
13
17
|
/** Bounded, decision-useful discovery summary for a connector guide. */
|
|
14
18
|
export declare function connectorGuideSummary(connector: Connector): string | undefined;
|
|
15
19
|
/** Whether correct use always depends on conventions outside the tool schema. */
|
package/dist/skills.js
CHANGED
|
@@ -17,24 +17,24 @@ Use exact addresses from discovery; never invent one. Search 2–4 distinctive a
|
|
|
17
17
|
|
|
18
18
|
## Inside a program
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
Portable code uses only connector globals (\`<connectorId>.<toolName>(args)\`), \`connecta\`, and \`console.*\`. QuickJS blocks imports and lacks fetch/process/timers/crypto/WebSocket. Dynamic Workers require only \`{ loader }\`; bindings/modules/globalOutbound violate it. Then env maps are empty; node:fs/http/https absent; outbound fetch/WebSocket/node:net/tls denied; DNS unresolved. Runtime builtins remain through import() and process.getBuiltinModule(), including node:path and cloudflare:workers; the set can drift. Timers/process/crypto/WebSocket and data: fetch remain. Avoid them; QuickJS fails.
|
|
21
21
|
|
|
22
22
|
- \`connecta.search({})\` loads all catalogs; pass \`connector: "<id>"\` when obvious to load one. \`safety: "readOnly"\` keeps executable calls. Neither grants authority. Matches carry \`address\` and annotations.
|
|
23
23
|
- Exact schemas: \`connecta.describe({ address: "connector.tool" })\` for one, \`{ addresses: [...] }\` for many; \`format: "json"\` only for exact constraints.
|
|
24
|
-
-
|
|
25
|
-
- Search inside the run
|
|
24
|
+
- Caught Connecta errors have \`message\`, \`code\`, \`retryable\`, and \`details\`; branch on fields. For 2–10 independent calls, \`connecta.batch([...])\` returns success data or an \`errorDetails\` whose code and retryable flag match the throw.
|
|
25
|
+
- Search inside the run; return only the reduction the answer needs, never raw payloads.
|
|
26
26
|
- Only tools annotated \`readOnlyHint: true\` are reachable; the gate, credentials, and admission are enforced below the sandbox — nothing a program does widens its reach.
|
|
27
27
|
|
|
28
28
|
## Rendering a view
|
|
29
29
|
|
|
30
|
-
\`connecta.ui(html)\` renders
|
|
30
|
+
\`connecta.ui(html)\` renders one success-only display view, never for the model. Fetch and check the shape first. On empty or missing data, return a trimmed first record instead of rendering. Otherwise render returned variables; the model reads the return value, not the view.
|
|
31
31
|
|
|
32
32
|
`;
|
|
33
33
|
/** Deployment-scoped guide routing appended to the shared usage guide. */
|
|
34
34
|
const CONNECTOR_GUIDES_SECTION = `
|
|
35
35
|
## Per-connector guides
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
Connector guides appear in \`skills({})\` and discovery with an exact \`guide\` name and bounded \`guideSummary\`. Fetch only a listed or carried name with \`skills({ name: <guide> })\`; never infer one from a connector id. \`guideRequired: true\` is a hard stop. \`guideRequiredReasons\` says why: \`connector_required\` and \`approval_required\` stand after schema expansion; \`schema_truncated\` clears after exact describe. Otherwise fetch for a relevant sequence, unit, pagination rule, alias, or API convention. A read-only call with a complete compact schema may skip an irrelevant guide. Connector guides never apply to another deployment.
|
|
38
38
|
`;
|
|
39
39
|
/** Shared Connecta routing guidance, byte-identical across deployments. */
|
|
40
40
|
export const USAGE_SKILL = USAGE_SKILL_BASE + CONNECTOR_GUIDES_SECTION;
|
|
@@ -80,17 +80,63 @@ export function connectorGuide(connector) {
|
|
|
80
80
|
const content = typeof guide === "string" ? guide : guide?.content;
|
|
81
81
|
return content && content.trim() !== "" ? content : undefined;
|
|
82
82
|
}
|
|
83
|
-
|
|
83
|
+
/** Discovery budget for one connector-guide summary, including an ellipsis. */
|
|
84
|
+
export const GUIDE_SUMMARY_LENGTH = 120;
|
|
84
85
|
/** A `---`/`***`/`___` rule, which also opens and closes YAML frontmatter. */
|
|
85
86
|
const RULE_RE = /^\s*(?:-{3,}|\*{3,}|_{3,})\s*$/;
|
|
86
87
|
/** A fenced code block's delimiter. */
|
|
87
88
|
const FENCE_RE = /^\s*(?:```|~~~)/;
|
|
89
|
+
/** Markdown blocks that end a paragraph without a blank physical line. */
|
|
90
|
+
const HEADING_RE = /^\s*#{1,6}/;
|
|
91
|
+
const LIST_ITEM_RE = /^\s*(?:[-*+]\s+|\d+[.)]\s+)/;
|
|
92
|
+
const SETEXT_UNDERLINE_RE = /^\s*=+\s*$/;
|
|
93
|
+
const TABLE_DELIMITER_RE = /^\s*\|?\s*:?-{3,}:?\s*(?:\|\s*:?-{3,}:?\s*)+\|?\s*$/;
|
|
94
|
+
const OPENS_CLAUSE_RE = /^(?:The|A|An)\b/u;
|
|
95
|
+
const ARTICLE_RE = /^(?:the|a|an)\b/u;
|
|
88
96
|
/**
|
|
89
97
|
* Markup that carries no summary text of its own: horizontal rules, HTML
|
|
90
98
|
* comments, and table rows. Skipped so a guide that opens with one is
|
|
91
99
|
* summarized by its first real line instead of by punctuation.
|
|
92
100
|
*/
|
|
93
101
|
const NOT_SUMMARY_RE = /^\s*(?:<!--|\|)|^\s*(?:-{3,}|\*{3,}|_{3,})\s*$/;
|
|
102
|
+
/** A standard Markdown table starts with a pipe-bearing row and delimiter. */
|
|
103
|
+
function startsTable(lines, index) {
|
|
104
|
+
const header = lines[index] ?? "";
|
|
105
|
+
const delimiter = lines[index + 1] ?? "";
|
|
106
|
+
return header.includes("|") && TABLE_DELIMITER_RE.test(delimiter);
|
|
107
|
+
}
|
|
108
|
+
/** True when a sentence-looking period belongs to an abbreviation. */
|
|
109
|
+
function isAbbreviation(text, end) {
|
|
110
|
+
const token = text.slice(0, end).match(/\S+$/u)?.[0] ?? "";
|
|
111
|
+
// These introduce an example or restatement even before a capitalized word.
|
|
112
|
+
if (/^(?:e\.g|i\.e)\.$/iu.test(token))
|
|
113
|
+
return true;
|
|
114
|
+
// An initial or title belongs to the proper name that follows it.
|
|
115
|
+
if (/^[A-Z]\.$/u.test(token))
|
|
116
|
+
return true;
|
|
117
|
+
if (/^(?:Mr|Mrs|Ms|Dr|Prof|Sr|Jr|St)\.$/iu.test(token))
|
|
118
|
+
return true;
|
|
119
|
+
if (!/^(?:[A-Za-z]\.){2,}$/u.test(token) &&
|
|
120
|
+
!/^(?:vs|etc|approx|dept|fig|no)\.$/iu.test(token)) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
// Initialisms can end a sentence or extend a name ("U.S. East region").
|
|
124
|
+
// The mistakes are asymmetric: a false ending presents a fragment as a
|
|
125
|
+
// complete thought, while a missed ending gets an honest ellipsis. Count
|
|
126
|
+
// the period only with narrow evidence of a new clause: an article in one
|
|
127
|
+
// of its first two words. This is grammar evidence, not a starter-word list.
|
|
128
|
+
const following = text
|
|
129
|
+
.slice(end)
|
|
130
|
+
.match(/^[)\]}'"”’]*\s+(\S+)(?:\s+(\S+))?/u);
|
|
131
|
+
if (!following)
|
|
132
|
+
return false;
|
|
133
|
+
const [, nextWord, afterNext] = following;
|
|
134
|
+
const startsClause = nextWord !== undefined &&
|
|
135
|
+
/^\p{Lu}/u.test(nextWord) &&
|
|
136
|
+
(OPENS_CLAUSE_RE.test(nextWord) ||
|
|
137
|
+
(afterNext !== undefined && ARTICLE_RE.test(afterNext)));
|
|
138
|
+
return !startsClause;
|
|
139
|
+
}
|
|
94
140
|
/** Drop a leading YAML frontmatter block — metadata, not summary text. */
|
|
95
141
|
function withoutFrontmatter(lines) {
|
|
96
142
|
let start = 0;
|
|
@@ -102,33 +148,107 @@ function withoutFrontmatter(lines) {
|
|
|
102
148
|
const close = lines.findIndex((line, i) => i > start && RULE_RE.test(line));
|
|
103
149
|
return close === -1 ? lines : lines.slice(close + 1);
|
|
104
150
|
}
|
|
151
|
+
/** Normalize authored and derived summaries under one construction contract. */
|
|
152
|
+
export function normalizeGuideSummary(summary) {
|
|
153
|
+
const normalized = summary.replace(/\s+/g, " ").trim();
|
|
154
|
+
return normalized === "" ? undefined : normalized;
|
|
155
|
+
}
|
|
105
156
|
/**
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* markup.
|
|
157
|
+
* Shorten a normalized summary at the strongest readable boundary available.
|
|
158
|
+
* A complete sentence needs no ellipsis; clause and word cuts do, so discovery
|
|
159
|
+
* never presents an unfinished fragment as the guide's complete thought.
|
|
110
160
|
*/
|
|
111
161
|
function boundedSummary(summary) {
|
|
112
|
-
const
|
|
113
|
-
if (
|
|
162
|
+
const normalized = normalizeGuideSummary(summary);
|
|
163
|
+
if (!normalized)
|
|
114
164
|
return undefined;
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
165
|
+
if (normalized.length <= GUIDE_SUMMARY_LENGTH)
|
|
166
|
+
return normalized;
|
|
167
|
+
const contentBudget = GUIDE_SUMMARY_LENGTH - 1;
|
|
168
|
+
let sentenceEnd = 0;
|
|
169
|
+
const sentenceBoundary = /[.!?…。!?](?:[)\]}'"”’]+)?(?=\s|$)/gu;
|
|
170
|
+
for (const match of normalized.matchAll(sentenceBoundary)) {
|
|
171
|
+
const end = (match.index ?? 0) + match[0].length;
|
|
172
|
+
if (end > GUIDE_SUMMARY_LENGTH)
|
|
173
|
+
break;
|
|
174
|
+
const punctuationEnd = (match.index ?? 0) + 1;
|
|
175
|
+
if (match[0].startsWith(".") &&
|
|
176
|
+
isAbbreviation(normalized, punctuationEnd)) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
// Do not mistake another short fragment for a useful complete thought.
|
|
180
|
+
if (end >= 24)
|
|
181
|
+
sentenceEnd = end;
|
|
182
|
+
}
|
|
183
|
+
if (sentenceEnd > 0)
|
|
184
|
+
return normalized.slice(0, sentenceEnd);
|
|
185
|
+
const available = normalized.slice(0, contentBudget);
|
|
186
|
+
let clauseEnd = 0;
|
|
187
|
+
const clauseBoundary = /[,;:](?=\s)|\s[—–-](?=\s)/g;
|
|
188
|
+
for (const match of available.matchAll(clauseBoundary)) {
|
|
189
|
+
const end = match.index ?? 0;
|
|
190
|
+
// Prefer a clause only when it retains most of the discovery budget.
|
|
191
|
+
if (end >= 80)
|
|
192
|
+
clauseEnd = end;
|
|
193
|
+
}
|
|
194
|
+
if (clauseEnd > 0) {
|
|
195
|
+
return `${available.slice(0, clauseEnd).trimEnd()}…`;
|
|
196
|
+
}
|
|
197
|
+
const wordEnd = available.search(/\s+\S*$/);
|
|
198
|
+
if (wordEnd > 0) {
|
|
199
|
+
const prefix = available
|
|
200
|
+
.slice(0, wordEnd)
|
|
201
|
+
.trimEnd()
|
|
202
|
+
.replace(/[,;:([{—–-]+$/u, "")
|
|
203
|
+
.trimEnd();
|
|
204
|
+
if (prefix !== "")
|
|
205
|
+
return `${prefix}…`;
|
|
206
|
+
}
|
|
207
|
+
let hardEnd = contentBudget;
|
|
208
|
+
const code = normalized.charCodeAt(hardEnd - 1);
|
|
209
|
+
if (code >= 0xd800 && code <= 0xdbff)
|
|
210
|
+
hardEnd--;
|
|
211
|
+
return `${normalized.slice(0, hardEnd)}…`;
|
|
118
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* One thought describing a guide for the cheap list view: the first meaningful
|
|
215
|
+
* paragraph, joined across physical lines, with headings and the connector
|
|
216
|
+
* description as fallbacks when the guide opens with markup alone.
|
|
217
|
+
*/
|
|
119
218
|
function summarizeGuide(connector, guide) {
|
|
219
|
+
const lines = withoutFrontmatter(guide.split("\n"));
|
|
120
220
|
let inFence = false;
|
|
221
|
+
let inComment = false;
|
|
121
222
|
let headingFallback;
|
|
122
|
-
for (
|
|
223
|
+
for (let index = 0; index < lines.length; index++) {
|
|
224
|
+
const raw = lines[index] ?? "";
|
|
225
|
+
if (inComment) {
|
|
226
|
+
if (raw.includes("-->"))
|
|
227
|
+
inComment = false;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
123
230
|
if (FENCE_RE.test(raw)) {
|
|
124
231
|
inFence = !inFence;
|
|
125
232
|
continue;
|
|
126
233
|
}
|
|
127
234
|
if (inFence)
|
|
128
235
|
continue;
|
|
236
|
+
if (raw.trimStart().startsWith("<!--")) {
|
|
237
|
+
if (!raw.includes("-->"))
|
|
238
|
+
inComment = true;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (startsTable(lines, index)) {
|
|
242
|
+
index++;
|
|
243
|
+
while (index + 1 < lines.length &&
|
|
244
|
+
(lines[index + 1] ?? "").includes("|")) {
|
|
245
|
+
index++;
|
|
246
|
+
}
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
129
249
|
if (raw.trim() === "" || NOT_SUMMARY_RE.test(raw))
|
|
130
250
|
continue;
|
|
131
|
-
const heading =
|
|
251
|
+
const heading = HEADING_RE.test(raw);
|
|
132
252
|
const line = raw
|
|
133
253
|
// `\s*` (not `\s+`) so a bare `#` strips to nothing and is skipped, and
|
|
134
254
|
// an unspaced `#Heading` is still read as a heading.
|
|
@@ -142,7 +262,24 @@ function summarizeGuide(connector, guide) {
|
|
|
142
262
|
headingFallback ??= boundedSummary(line);
|
|
143
263
|
continue;
|
|
144
264
|
}
|
|
145
|
-
|
|
265
|
+
const paragraph = [line];
|
|
266
|
+
while (index + 1 < lines.length) {
|
|
267
|
+
const next = lines[index + 1] ?? "";
|
|
268
|
+
if (next.trim() === "" ||
|
|
269
|
+
startsTable(lines, index + 1) ||
|
|
270
|
+
FENCE_RE.test(next) ||
|
|
271
|
+
HEADING_RE.test(next) ||
|
|
272
|
+
LIST_ITEM_RE.test(next) ||
|
|
273
|
+
SETEXT_UNDERLINE_RE.test(next) ||
|
|
274
|
+
NOT_SUMMARY_RE.test(next)) {
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
paragraph.push(next.trim());
|
|
278
|
+
index++;
|
|
279
|
+
if (paragraph.join(" ").length > GUIDE_SUMMARY_LENGTH)
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
return boundedSummary(paragraph.join(" ")) ?? line;
|
|
146
283
|
}
|
|
147
284
|
if (headingFallback)
|
|
148
285
|
return headingFallback;
|
|
@@ -155,6 +292,8 @@ export function connectorGuideSummary(connector) {
|
|
|
155
292
|
if (!guide)
|
|
156
293
|
return undefined;
|
|
157
294
|
const configured = typeof connector.usageGuide === "object"
|
|
295
|
+
// Registry construction rejects over-budget configured summaries. Keep
|
|
296
|
+
// normalization here so direct Connector callers see the same text.
|
|
158
297
|
? boundedSummary(connector.usageGuide.summary ?? "")
|
|
159
298
|
: undefined;
|
|
160
299
|
return configured ?? summarizeGuide(connector, guide);
|
package/dist/types.d.ts
CHANGED
|
@@ -180,6 +180,12 @@ export interface CatalogDriftReport extends CatalogDriftCounts {
|
|
|
180
180
|
/** When the observation was taken; never when a probe was scheduled. */
|
|
181
181
|
observedAt: string;
|
|
182
182
|
}
|
|
183
|
+
/** The last agent-facing catalog read this runtime served for one connector. */
|
|
184
|
+
export interface CatalogAccessObservation {
|
|
185
|
+
/** Fresh means the read used a live or unexpired catalog; stale means SWR. */
|
|
186
|
+
state: "fresh" | "stale";
|
|
187
|
+
observedAt: string;
|
|
188
|
+
}
|
|
183
189
|
export interface ConnectorStatus {
|
|
184
190
|
state: ConnectorStatusState;
|
|
185
191
|
/** When state === "auth_required", the URL the operator should open. */
|
|
@@ -193,6 +199,11 @@ export interface ConnectorStatus {
|
|
|
193
199
|
* isolate or process has seen nothing, not that nothing drifted.
|
|
194
200
|
*/
|
|
195
201
|
catalogDrift?: CatalogDriftReport;
|
|
202
|
+
/**
|
|
203
|
+
* The last agent-facing catalog read in this runtime. This payload-free
|
|
204
|
+
* observation is not persisted and operator reads do not replace it.
|
|
205
|
+
*/
|
|
206
|
+
catalogAccess?: CatalogAccessObservation;
|
|
196
207
|
}
|
|
197
208
|
/** The whole plugin contract — the one open seam. */
|
|
198
209
|
export interface Connector {
|
|
@@ -313,8 +324,9 @@ export interface ConnectorUsageGuide {
|
|
|
313
324
|
/** Markdown returned verbatim by `skills({ name: "connector:<id>" })`. */
|
|
314
325
|
content: string;
|
|
315
326
|
/**
|
|
316
|
-
*
|
|
317
|
-
*
|
|
327
|
+
* Discovery hint describing the conventions the guide covers. Whitespace is
|
|
328
|
+
* normalized, and a value over 120 characters throws at construction. When
|
|
329
|
+
* omitted, Connecta derives a bounded summary from the first body paragraph.
|
|
318
330
|
*/
|
|
319
331
|
summary?: string;
|
|
320
332
|
/**
|
package/dist/ui.js
CHANGED
|
@@ -374,6 +374,9 @@ export async function buildUiData(registry, baseUrl, serverInfo, credentialVault
|
|
|
374
374
|
// `boundedCatalogDrift`, so what lands here cannot carry a name or a
|
|
375
375
|
// schema even if the plugin seam returned one.
|
|
376
376
|
...(status.catalogDrift ? { catalogDrift: status.catalogDrift } : {}),
|
|
377
|
+
...(status.catalogAccess
|
|
378
|
+
? { catalogAccess: status.catalogAccess }
|
|
379
|
+
: {}),
|
|
377
380
|
...(c.disconnectAuth && c.startAuth ? { oauth: true } : {}),
|
|
378
381
|
...(credential ? { credential } : {}),
|
|
379
382
|
};
|
|
@@ -461,7 +464,7 @@ export function renderUiHtml(uiAuth, mcpUrl = "/mcp", branding, nonce, page = "c
|
|
|
461
464
|
: `<span class="product">${escapeHtmlAttr(brand.productName)}</span>`
|
|
462
465
|
: "";
|
|
463
466
|
const clerkScript = clerk && clerkScriptOrigin
|
|
464
|
-
? `<script${nonceAttr}
|
|
467
|
+
? `<script${nonceAttr} crossorigin="anonymous" data-clerk-publishable-key="${escapeHtmlAttr(clerk.publishableKey)}" src="${escapeHtmlAttr(clerkScriptOrigin)}/npm/@clerk/clerk-js@6/dist/clerk.browser.js"></script>`
|
|
465
468
|
: "";
|
|
466
469
|
return `<!doctype html>
|
|
467
470
|
<html lang="en">
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
|
@@ -26,7 +26,10 @@ ethos invariant, not a style preference: a client retained across requests on
|
|
|
26
26
|
Workers is a cross-request capability leak, and a promise awaited after the
|
|
27
27
|
response is work the runtime may have already torn down. Deferred work has one
|
|
28
28
|
sanctioned channel — `ctx.waitUntil`, threaded through `fetch(request, env,
|
|
29
|
-
ctx)
|
|
29
|
+
ctx)`. Best-effort activity writes use it. An agent read that already demanded
|
|
30
|
+
an expired catalog refresh may also use it while serving a complete catalog
|
|
31
|
+
inside its stale window. That refresh owns a new scope and deadline; it never
|
|
32
|
+
carries the inbound scope or signal past the request.
|
|
30
33
|
|
|
31
34
|
The registry is deliberately on the long side of that line and the MCP server
|
|
32
35
|
deliberately on the short side. A fresh `McpServer` per request is what makes
|
|
@@ -87,8 +90,8 @@ owns or hands out, and a change usually belongs in exactly one of them:
|
|
|
87
90
|
|
|
88
91
|
| Module | Owns |
|
|
89
92
|
| --- | --- |
|
|
90
|
-
| `src/registry.ts` | The connector set,
|
|
91
|
-
| `src/catalog-service.ts` |
|
|
93
|
+
| `src/registry.ts` | The connector set, address resolution, catalog TTL/persistence/completeness, shared refresh single-flight, connector health, per-connector call limiters, and drift. Construction-time refusals live here. |
|
|
94
|
+
| `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. |
|
|
92
95
|
| `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. |
|
|
93
96
|
| `src/catalog.ts` | Ranking, description summarizing, and the compact schema renderer discovery shows. |
|
|
94
97
|
|
|
@@ -137,7 +140,7 @@ src/
|
|
|
137
140
|
apps-shell.ts the one build-time MCP Apps template
|
|
138
141
|
skills.ts MCP instructions, the usage skill, connector guides
|
|
139
142
|
registry.ts connector set, addresses, health, call limiters
|
|
140
|
-
catalog-service.ts catalog
|
|
143
|
+
catalog-service.ts request-local catalog access, search, and describe
|
|
141
144
|
catalog.ts ranking, summaries, compact schema rendering
|
|
142
145
|
invocation.ts one tool call, end to end
|
|
143
146
|
catalog-drift.ts vetted manifests and the counts a refresh produces
|
|
@@ -53,8 +53,8 @@ createConnecta({
|
|
|
53
53
|
});
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
Dynamic Workers require the Workers Paid plan. The
|
|
57
|
-
|
|
56
|
+
Dynamic Workers require the Workers Paid plan. The supported constructor passes only `loader`; `bindings`, `modules`, or
|
|
57
|
+
`globalOutbound` grant ambient guest authority and violate `P2`. The [Worker example](../examples/worker/README.md#code-mode) carries the full setup.
|
|
58
58
|
|
|
59
59
|
## What an executor must implement
|
|
60
60
|
|
|
@@ -104,8 +104,8 @@ Connecta passes exactly one provider, named `connecta`. An executor must:
|
|
|
104
104
|
uncaught tool failure keeps its type (`E1`).
|
|
105
105
|
6. **Capture `console.log`, `console.warn`, and `console.error`** into `logs` in
|
|
106
106
|
call order (`R5`), bounding what it retains.
|
|
107
|
-
7. **Bound the guest**: wall clock, memory, stack, and CPU (`L3`, `L5`)
|
|
108
|
-
|
|
107
|
+
7. **Bound the guest**: wall clock, memory, stack, and CPU (`L3`, `L5`). Keep
|
|
108
|
+
ambient capabilities within the documented and tested `P2`/`X5` boundary.
|
|
109
109
|
8. **Grant no ambient authority of its own.** Never back this with `eval` or
|
|
110
110
|
`node:vm`: the sandbox is a containment layer on top of connecta's boundary,
|
|
111
111
|
not a replacement for it, and every capability arrives through `fns`.
|
|
@@ -138,10 +138,10 @@ reinterpreted, so do not rely on it.
|
|
|
138
138
|
It is host plumbing, callable but not contract: it takes a connector id and an
|
|
139
139
|
unsanitized-or-sanitized tool name and can change shape without notice.
|
|
140
140
|
|
|
141
|
-
Anything else a runtime happens to expose is outside the contract and
|
|
142
|
-
be used
|
|
143
|
-
filesystem access
|
|
144
|
-
|
|
141
|
+
Anything else a runtime happens to expose is outside the portable contract and
|
|
142
|
+
must not be used. QuickJS grants none of it. A loader-only Dynamic Worker denies
|
|
143
|
+
external egress and filesystem access and keeps its environment maps empty, but
|
|
144
|
+
it exposes the globals and runtime builtins described in `X5`.
|
|
145
145
|
|
|
146
146
|
**P3.** Values cross the host bridge as JSON. Arguments must be
|
|
147
147
|
JSON-serializable and results arrive as plain JSON values. A value outside JSON —
|
|
@@ -154,8 +154,9 @@ scratch storage carried to the next program, and no request-bound object outlive
|
|
|
154
154
|
the request that created it. Within one execution, host calls share one
|
|
155
155
|
downstream request scope.
|
|
156
156
|
|
|
157
|
-
**P5.** Plain JavaScript only. TypeScript syntax is a syntax error
|
|
158
|
-
|
|
157
|
+
**P5.** Plain JavaScript only. TypeScript syntax is a syntax error. Portable code
|
|
158
|
+
does not import: QuickJS blocks imports, while Dynamic Workers expose the `X5`
|
|
159
|
+
runtime modules. Neither executor exposes `require`.
|
|
159
160
|
|
|
160
161
|
## Addressing
|
|
161
162
|
|
|
@@ -240,8 +241,8 @@ can set `includeSchemaKeys: false` to buy the bytes back.
|
|
|
240
241
|
**S3.** Discovery is bounded and the bounds throw rather than silently shrink: a
|
|
241
242
|
`limit` outside 1–100 is `invalid_args`, and a page whose serialized form
|
|
242
243
|
exceeds 256,000 bytes is `result_too_large`, each with a hint naming the ways to
|
|
243
|
-
ask for less.
|
|
244
|
-
(`E1`)
|
|
244
|
+
ask for less. The thrown error carries the stable `code`, `retryable`, and
|
|
245
|
+
`details` fields (`E1`).
|
|
245
246
|
|
|
246
247
|
### connecta.describe
|
|
247
248
|
|
|
@@ -295,9 +296,7 @@ order. A success is `{ address, ok: true, data }`. A failure is
|
|
|
295
296
|
field names the host's internal batch path uses. One failing call never rejects
|
|
296
297
|
the batch, and more than ten calls throws.
|
|
297
298
|
|
|
298
|
-
**S8.** `
|
|
299
|
-
error crosses the bridge as a bare message (`E1`), a batch of one is the supported
|
|
300
|
-
way for a program to *decide* something about a failure rather than report it.
|
|
299
|
+
**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.
|
|
301
300
|
|
|
302
301
|
### connecta.emit
|
|
303
302
|
|
|
@@ -310,24 +309,18 @@ clauses are [Emitted output](#emitted-output) (`M1`–`M10`).
|
|
|
310
309
|
|
|
311
310
|
## Errors
|
|
312
311
|
|
|
313
|
-
**E1.** There are four error channels
|
|
312
|
+
**E1.** There are four error channels. Connecta failures are typed whether caught or uncaught.
|
|
314
313
|
|
|
315
314
|
| Channel | Shape | Typed? |
|
|
316
315
|
| --- | --- | --- |
|
|
317
|
-
| A
|
|
316
|
+
| A caught Connecta host failure | `Error` with `message`, `code`, `retryable`, and `details` | yes |
|
|
318
317
|
| `connecta.batch` outcome | `{ ok: false, error, errorDetails }` | yes |
|
|
319
318
|
| An uncaught **tool or discovery** failure, as the model sees it | `{ error: { code, message, retryable, … } }` with `isError` | yes |
|
|
320
|
-
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
host call
|
|
324
|
-
|
|
325
|
-
classify, use `errorDetails`; to hand a failure to the model with its type
|
|
326
|
-
intact, let it escape uncaught — connecta re-attaches the typed details on the
|
|
327
|
-
way out. The model-facing version of this lives in `execute_code`'s description,
|
|
328
|
-
not in the always-loaded usage skill, which `test/meta-tools.test.ts` caps at
|
|
329
|
-
2,500 bytes — a budget the guide already spends nearly all of, so new text there
|
|
330
|
-
displaces old rather than adding to what every request pays for.
|
|
319
|
+
| Program or execution failure (`E5`, `E6`, a bridge bound in `L6`) | error text | no |
|
|
320
|
+
|
|
321
|
+
Both executor bridges reduce a rejected host call to `new Error(message)`. Connecta restores the typed failure in a trusted prelude with a per-execution authenticated frame (`X11`), without turning the rejection into a returned value.
|
|
322
|
+
`message` remains the human text. `code` and `retryable` are the stable branch fields; `details` is the complete host classification. This covers `call`, connector shortcuts, `search`, `describe`, `emit`, `ui`, rejected batch input, and the host-call budget.
|
|
323
|
+
Program-authored errors stay untyped, and code must never parse error prose.
|
|
331
324
|
|
|
332
325
|
**E2.** The taxonomy: `retryable` is what connecta reports, `Y3` what a program may do.
|
|
333
326
|
|
|
@@ -339,16 +332,18 @@ displaces old rather than adding to what every request pays for.
|
|
|
339
332
|
| `destructive_tool_requires_approval` | the tool is not explicitly read-only | false |
|
|
340
333
|
| `auth_required` | the credential is missing, expired, or rejected | false |
|
|
341
334
|
| `invalid_args` | arguments or discovery bounds were rejected | false |
|
|
342
|
-
| `not_found` | the downstream answered and the resource is not there — the one code that says skip this id rather than stop,
|
|
335
|
+
| `not_found` | the downstream answered and the resource is not there — the one code that says skip this id rather than stop, raised only where the provider tells absence from a permission gap ([H11](./provider-conventions.md#h11--errors-are-mapped-to-what-the-caller-does-next)) | false |
|
|
343
336
|
| `input_required_unsupported` | a downstream asked for mid-call input | false |
|
|
344
337
|
| `rate_limited` | the downstream reported a rate limit | true |
|
|
345
338
|
| `unavailable` | the downstream is down or unreachable | true |
|
|
346
339
|
| `timeout` | the per-call 15-second deadline expired | true |
|
|
347
340
|
| `cancelled` | the run ended while this call was in flight (`E5`) | false |
|
|
348
|
-
| `connector_call_failed` | anything else the connector threw
|
|
341
|
+
| `connector_call_failed` | anything else the connector threw | per message |
|
|
349
342
|
| `batch_call_failed` | a `connecta.batch` entry connecta could not even attempt | per message |
|
|
350
343
|
| `catalog_lookup_failed` | the connector's catalog could not be loaded | per cause |
|
|
351
344
|
| `result_processing_failed` | the result could not be prepared | per message |
|
|
345
|
+
| `result_too_large` | a discovery response exceeded its byte bound | false |
|
|
346
|
+
| `budget_exceeded` | the run exhausted a host-call or emitted-output budget | false |
|
|
352
347
|
|
|
353
348
|
**E3.** `auth_required` carries the same recovery envelope as `call_tool`:
|
|
354
349
|
`connector`, `operation`, `recovery` (`oauth`, `operator_config`, or
|
|
@@ -608,25 +603,22 @@ annotation-gated `maxRetries`; code mode fixes it at zero, so one
|
|
|
608
603
|
`connecta.call` is exactly one downstream attempt. The program is the retry
|
|
609
604
|
loop, and its budget is visible to it (`L4`).
|
|
610
605
|
|
|
611
|
-
**Y2.** A program may retry a failure whose `errorDetails.retryable` is true,
|
|
612
|
-
learned through `connecta.batch` (`S8`). Every attempt spends host-call budget,
|
|
613
|
-
so a retry loop that ignores the budget converts a transient failure into a
|
|
614
|
-
budget failure.
|
|
606
|
+
**Y2.** A program may retry a caught failure whose `retryable` is true, or a batch failure whose `errorDetails.retryable` is true (`S8`). Every attempt spends host-call budget, so an unchecked loop converts a transient failure into `budget_exceeded`.
|
|
615
607
|
|
|
616
608
|
**Y3.** What must never be retried automatically:
|
|
617
609
|
|
|
618
610
|
- anything with `retryable: false` — a policy refusal, a missing credential, a
|
|
619
611
|
bad address, or malformed arguments will fail identically forever;
|
|
620
|
-
- `rate_limited`, immediately.
|
|
621
|
-
wait
|
|
622
|
-
|
|
623
|
-
`retryAfterMs` in hand.
|
|
612
|
+
- `rate_limited`, immediately. A portable program has no timer, and a
|
|
613
|
+
Dynamic-Worker-only wait would spend the run's wall-clock budget on code that
|
|
614
|
+
fails on QuickJS. Return the failure and let the model, which can wait,
|
|
615
|
+
re-issue with `retryAfterMs` in hand.
|
|
624
616
|
- a cancelled or timed-out *execution*: it is already over (`L1`).
|
|
625
617
|
|
|
626
618
|
**Y4.** Connecta's own retry machinery beneath the meta-tools honours a
|
|
627
619
|
connector-reported `Retry-After` exactly or not at all, and declines windows
|
|
628
620
|
longer than 10 seconds rather than shortening them. A program sees the window
|
|
629
|
-
verbatim as `errorDetails.retryAfterMs`.
|
|
621
|
+
verbatim as `err.details.retryAfterMs` or `errorDetails.retryAfterMs`.
|
|
630
622
|
|
|
631
623
|
## Cancellation and limits
|
|
632
624
|
|
|
@@ -656,10 +648,7 @@ because connecta enforces them above the sandbox:
|
|
|
656
648
|
| Result | 24,000 serialized characters |
|
|
657
649
|
| Logs presented to the model | 4,000 characters |
|
|
658
650
|
|
|
659
|
-
Exhausting the host-call budget fails that call
|
|
660
|
-
`connector_call_failed` (`E2`) and a message naming the budget — no connector was
|
|
661
|
-
reached, so nothing more specific is true. Retrying it is pointless: the budget
|
|
662
|
-
does not refill inside one execution.
|
|
651
|
+
Exhausting the host-call budget fails that call with non-retryable `budget_exceeded` (`E2`) and a message naming the budget. No connector is reached, and the budget does not refill inside one execution.
|
|
663
652
|
|
|
664
653
|
**L5.** The guest is memory-, stack-, and CPU-bounded, and a program that
|
|
665
654
|
exhausts a bound ends the run with an error instead of degrading the host. The
|
|
@@ -672,7 +661,7 @@ code safe to run at all.
|
|
|
672
661
|
**L6.** A host call's serialized arguments and its serialized result are each
|
|
673
662
|
bounded — QuickJS caps both at 256 KiB (`X10`) — and exceeding either fails that
|
|
674
663
|
call, not the execution, so a program can catch it and ask for less. The failure
|
|
675
|
-
is untyped text (`E1`). An over-bound *result* names the address the program
|
|
664
|
+
is executor-owned untyped text, not a Connecta host failure (`E1`). An over-bound *result* names the address the program
|
|
676
665
|
called, not the internal dispatcher behind the shortcut namespaces; an over-bound
|
|
677
666
|
*argument* payload is refused before it is parsed, so it names no address at
|
|
678
667
|
all — parsing it to write a better message would spend exactly the work the bound
|
|
@@ -750,11 +739,9 @@ Worker renders arguments with `String()` (so an object logs as
|
|
|
750
739
|
latter two. Only the three captured everywhere are contract (`R5`); rendering is
|
|
751
740
|
not.
|
|
752
741
|
|
|
753
|
-
**X5. Leftover
|
|
754
|
-
`
|
|
755
|
-
`
|
|
756
|
-
`process.env` is empty, and timers work. `P2` is the contract — a program that
|
|
757
|
-
uses `setTimeout` is writing Workers-only code, and it will fail on Node.
|
|
742
|
+
**X5. Leftover authority.** QuickJS blocks imports and has no `fetch`, `process`, timers, `crypto`, or `WebSocket`. A Dynamic Worker has those globals plus a non-contract set of runtime builtins through `import()` and `process.getBuiltinModule()`, including `node:path`, `node:crypto`, `node:net`, `node:tls`, `node:dns`, `node:module`, and `cloudflare:workers`. The upstream set can drift; this list is not an allowlist.
|
|
743
|
+
The supported Worker construction is exactly `new DynamicWorkerExecutor({ loader })`. Do not pass `bindings`, `modules`, or `globalOutbound`: each can grant ambient configuration, code, or egress. Under it, `process.env`, lexical `this.env`, and `cloudflare:workers.env` are empty; `node:fs`, `node:http`, and `node:https` are unavailable through either access route; external `fetch`, `WebSocket`, `node:net`, and `node:tls` fail with workerd's outbound-denial error; DNS lookup ends unresolved; and `fetch("data:...")` resolves locally.
|
|
744
|
+
`P2` is the portable contract. Programs use none of this runtime-only authority, including timers and `crypto`, because the same code fails on QuickJS. The `execute_code` description and served `usage` skill say so before an agent writes code.
|
|
758
745
|
|
|
759
746
|
**X6. Stall detection.** QuickJS notices a program awaiting something that can
|
|
760
747
|
never settle and fails fast; the Dynamic Worker waits for its deadline. The fast
|
|
@@ -786,11 +773,16 @@ a `process.send` with a hard ceiling. A program that returns a quarter-megabyte
|
|
|
786
773
|
from one tool call therefore fails on Node and may succeed on Workers — reduce
|
|
787
774
|
inside the program either way (`R1`).
|
|
788
775
|
|
|
776
|
+
**X11. Typed host rejection.** Both executors rebuild Connecta's authenticated host-failure frame as a thrown guest `Error` (`E1`). The per-run secret stays in the trusted prelude closure, and the prelude locks `globalThis.Error`, so guest code and connector prose cannot forge the host transport frame.
|
|
777
|
+
The human message is unchanged; a mismatched frame is ordinary untyped prose.
|
|
778
|
+
|
|
789
779
|
## Changes from earlier code mode
|
|
790
780
|
|
|
791
|
-
|
|
781
|
+
Six behaviors changed with this contract, matching the changelog's Unreleased
|
|
792
782
|
entry. Programs that ran before still run.
|
|
793
783
|
|
|
784
|
+
- **Caught Connecta failures expose their classification** (`E1`, `X11`). Their human message and thrown semantics stay unchanged; `code`, `retryable`, and `details` are additive.
|
|
785
|
+
|
|
794
786
|
- **`connecta.batch` failures gained `errorDetails`** (`S7`). They carried only a
|
|
795
787
|
message, which left a program unable to tell a policy refusal from a transient
|
|
796
788
|
failure. Additive, and it reuses the host's internal batch field names, so a
|
|
@@ -833,7 +825,7 @@ the upstream `Executor` shape assignable.
|
|
|
833
825
|
| Clauses | Test |
|
|
834
826
|
| --- | --- |
|
|
835
827
|
| `P1`, `P5` | `test/guest-api-contract.test.ts` (TypeScript syntax), `test/quickjs-executor.test.ts` (`normalizeCode`) |
|
|
836
|
-
| `P2`, `X5` | `test/guest-api-contract.test.ts` (
|
|
828
|
+
| `P2`, `X5` | `test/guest-api-contract.test.ts` (Dynamic globals plus loader-only filesystem, HTTP, environment, egress, DNS, and local `data:` boundaries), `test/guest-api-contract-quickjs.test.ts` (exact absent globals and blocked imports), `test/deployment-shapes.test.ts` (loader-only Worker construction) |
|
|
837
829
|
| `P3`, `X9` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` |
|
|
838
830
|
| `P4` | `test/guest-api-contract.test.ts` (no cross-run leakage), `test/execute.test.ts` (one catalog load per connector per execution) |
|
|
839
831
|
| `A1`, `A2` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (sanitizing) |
|
|
@@ -846,8 +838,8 @@ the upstream `Executor` shape assignable.
|
|
|
846
838
|
| `S5` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (`unwrapMcpResult`) |
|
|
847
839
|
| `S6` | `test/execute.test.ts` (fail-closed annotations, activity parity) |
|
|
848
840
|
| `S7` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (batch cap) |
|
|
849
|
-
| `S8`, `E1` |
|
|
850
|
-
| `E2`, `E8` | `test/guest-api-contract.test.ts` (code → `retryable`, 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` |
|
|
841
|
+
| `S8`, `E1`, `X11` | both guest-contract executors (caught call, namespace, discovery, utility, batch-validation, budget, and forgery cases; typed batch equivalence) |
|
|
842
|
+
| `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
843
|
| `E3` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (`auth_required`) |
|
|
852
844
|
| `E4` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (destructive) |
|
|
853
845
|
| `E5` | `test/guest-api-contract.test.ts` (execution-failure channel, in-flight `cancelled`), `test/execute.test.ts` (admission), `test/executor-admission.test.ts`, `test/quickjs-executor.test.ts` (mid-run shutdown) |
|