@zackbart/connecta 0.19.0 → 0.20.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/dist/execute.js CHANGED
@@ -7,7 +7,6 @@ import { ExecutorAdmissionError, ExecutorExecutionError, isAdmittingExecutor, }
7
7
  import { boundedEchoText, classifyCallError, msg } from "./errors.js";
8
8
  import { InvocationFailure, InvocationService, } from "./invocation.js";
9
9
  import { hasConnectorGuides } from "./skills.js";
10
- import { isExplicitlyReadOnly } from "./tool-safety.js";
11
10
  /** Keep one model-written program from amplifying into an unbounded fan-out. */
12
11
  const EXECUTE_MAX_HOST_CALLS = 20;
13
12
  export const EXECUTE_MAX_BATCH_CALLS = 10;
@@ -149,11 +148,7 @@ function requireEmittedBlock(raw) {
149
148
  }
150
149
  return raw;
151
150
  }
152
- const UI_SHAPE_HINT = "connecta.ui accepts exactly one HTML argument and, optionally, one read-binding options object";
153
- const MAX_UI_READ_BINDINGS = 32;
154
- const MAX_UI_VIEW_ARGS = 32;
155
- const UI_READ_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
156
- const FORBIDDEN_UI_KEY = new Set(["__proto__", "constructor", "prototype"]);
151
+ const UI_SHAPE_HINT = "connecta.ui accepts exactly one argument: a non-empty string of HTML";
157
152
  /** What the argument was, named the way the emit validator names a bad field. */
158
153
  function describeUiArgument(raw) {
159
154
  if (raw === null)
@@ -168,9 +163,9 @@ function describeUiArgument(raw) {
168
163
  return `${/^[aeiou]/.test(kind) ? "an" : "a"} ${kind}`;
169
164
  }
170
165
  /**
171
- * Strict U1/V1 validation. The first argument remains HTML; the only second
172
- * argument is one read-binding manifest. There are no alternate object or MCP
173
- * block forms.
166
+ * Strict U1 validation. There is no options parameter and no sugar form, for
167
+ * M1's reason: sugar is how a one-shape contract grows hair. An options bag or
168
+ * an MCP block object is just a non-string, and fails as one.
174
169
  */
175
170
  function requireUiHtml(raw) {
176
171
  if (typeof raw !== "string" || raw.length === 0) {
@@ -178,80 +173,11 @@ function requireUiHtml(raw) {
178
173
  }
179
174
  return raw;
180
175
  }
181
- function requireRecord(raw, label) {
182
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
183
- throw guestFailure("invalid_args", `${label} must be an object`);
184
- }
185
- return raw;
186
- }
187
- function requireExactKeys(value, allowed, label) {
188
- const extras = Object.keys(value).filter((key) => !allowed.includes(key));
189
- if (extras.length > 0) {
190
- throw guestFailure("invalid_args", `${label} carries unsupported field(s) ${extras.map((key) => JSON.stringify(key)).join(", ")}`);
191
- }
192
- }
193
- function requireUiReadKey(raw, label) {
194
- if (typeof raw !== "string" ||
195
- raw.length === 0 ||
196
- raw.length > 128 ||
197
- FORBIDDEN_UI_KEY.has(raw)) {
198
- throw guestFailure("invalid_args", `${label} must be a non-empty string of at most 128 characters and cannot be __proto__, constructor, or prototype`);
199
- }
200
- return raw;
201
- }
202
- function requireUiReads(raw) {
203
- const record = requireRecord(raw, "connecta.ui options.reads");
204
- const names = Object.keys(record);
205
- if (names.length === 0 || names.length > MAX_UI_READ_BINDINGS) {
206
- throw guestFailure("invalid_args", `connecta.ui options.reads must contain from 1 through ${MAX_UI_READ_BINDINGS} named bindings`);
207
- }
208
- const reads = Object.create(null);
209
- for (const name of names) {
210
- if (!UI_READ_NAME.test(name) || FORBIDDEN_UI_KEY.has(name)) {
211
- throw guestFailure("invalid_args", `connecta.ui read binding name ${JSON.stringify(name)} must match ${UI_READ_NAME}`);
212
- }
213
- const value = requireRecord(record[name], `connecta.ui read binding ${JSON.stringify(name)}`);
214
- requireExactKeys(value, ["address", "fixedArgs", "viewArgs"], `connecta.ui read binding ${JSON.stringify(name)}`);
215
- if (typeof value.address !== "string" || value.address.length === 0) {
216
- throw guestFailure("invalid_args", `connecta.ui read binding ${JSON.stringify(name)} address must be a non-empty string`);
217
- }
218
- const fixedArgs = value.fixedArgs === undefined
219
- ? {}
220
- : requireRecord(value.fixedArgs, `connecta.ui read binding ${JSON.stringify(name)} fixedArgs`);
221
- const rawViewArgs = value.viewArgs ?? [];
222
- if (!Array.isArray(rawViewArgs) || rawViewArgs.length > MAX_UI_VIEW_ARGS) {
223
- throw guestFailure("invalid_args", `connecta.ui read binding ${JSON.stringify(name)} viewArgs must be an array of at most ${MAX_UI_VIEW_ARGS} strings`);
224
- }
225
- const viewArgs = rawViewArgs.map((key) => requireUiReadKey(key, `connecta.ui read binding ${JSON.stringify(name)} viewArgs entry`));
226
- if (new Set(viewArgs).size !== viewArgs.length) {
227
- throw guestFailure("invalid_args", `connecta.ui read binding ${JSON.stringify(name)} viewArgs must not repeat a key`);
228
- }
229
- for (const key of viewArgs) {
230
- if (Object.prototype.hasOwnProperty.call(fixedArgs, key)) {
231
- throw guestFailure("invalid_args", `connecta.ui read binding ${JSON.stringify(name)} view argument ${JSON.stringify(key)} cannot override a fixed argument`);
232
- }
233
- }
234
- reads[name] = {
235
- address: value.address,
236
- fixedArgs,
237
- viewArgs,
238
- };
239
- }
240
- return reads;
241
- }
242
176
  function requireUiPayload(values) {
243
- if (values.length !== 1 && values.length !== 2) {
177
+ if (values.length !== 1) {
244
178
  throw guestFailure("invalid_args", `${UI_SHAPE_HINT}; got ${values.length} arguments`);
245
179
  }
246
- const html = requireUiHtml(values[0]);
247
- if (values.length === 1)
248
- return { html };
249
- const options = requireRecord(values[1], "connecta.ui options");
250
- requireExactKeys(options, ["reads"], "connecta.ui options");
251
- if (!Object.prototype.hasOwnProperty.call(options, "reads")) {
252
- throw guestFailure("invalid_args", "connecta.ui options must contain reads");
253
- }
254
- return { html, reads: requireUiReads(options.reads) };
180
+ return { html: requireUiHtml(values[0]) };
255
181
  }
256
182
  /**
257
183
  * Request-local collection for `connecta.emit` and `connecta.ui`. Budgets fail
@@ -309,10 +235,6 @@ export class EmitCollector {
309
235
  this.assertUiVacant();
310
236
  this.acceptUiPayload(requireUiPayload(values));
311
237
  }
312
- acceptValidatedUi(payload) {
313
- this.assertUiVacant();
314
- this.acceptUiPayload(payload);
315
- }
316
238
  assertUiVacant() {
317
239
  if (this.ui) {
318
240
  throw guestFailure("invalid_args", "connecta.ui accepts at most one payload per run: a view was already accepted and stands");
@@ -595,36 +517,6 @@ export async function buildSandboxProviders(registry, baseUrl, logger, activity,
595
517
  const callNamespace = async (connectorId, toolAlias, args) => {
596
518
  return called("call", () => invocation.invokeToolAlias(String(connectorId), String(toolAlias), sanitizeIdentifier, args ?? {}, invocationContext()));
597
519
  };
598
- /**
599
- * A read binding is admitted while the program still owns the request. The
600
- * shell later calls the ordinary `call_tool`, which repeats this same
601
- * fail-closed check against the then-current catalog; validating here keeps
602
- * a typo or destructive address from producing a view whose controls can
603
- * never work, while validation at use keeps a stale view from retaining old
604
- * authority.
605
- */
606
- const validateUiReads = async (payload) => {
607
- if (!payload.reads)
608
- return payload;
609
- const reads = Object.create(null);
610
- for (const [name, binding] of Object.entries(payload.reads)) {
611
- const resolution = await catalog.resolveTool(binding.address, limits.signal !== undefined ? { signal: limits.signal } : {});
612
- if (!resolution.ok) {
613
- throw new InvocationFailure({
614
- ...resolution.error,
615
- message: `connecta.ui read binding ${JSON.stringify(name)} could not resolve ${JSON.stringify(binding.address)}: ${resolution.error.message}`,
616
- });
617
- }
618
- if (!isExplicitlyReadOnly(resolution.resolved.definition)) {
619
- throw guestFailure("destructive_tool_requires_approval", `connecta.ui read binding ${JSON.stringify(name)} refuses ${JSON.stringify(binding.address)}: the tool is not explicitly read-only`);
620
- }
621
- reads[name] = {
622
- ...binding,
623
- address: `${resolution.resolved.connector.id}.${resolution.resolved.toolName}`,
624
- };
625
- }
626
- return { html: payload.html, reads };
627
- };
628
520
  const fns = {
629
521
  __callNamespace: callNamespace,
630
522
  call: (address, args) => callAddress(address, args),
@@ -646,8 +538,7 @@ export async function buildSandboxProviders(registry, baseUrl, logger, activity,
646
538
  if (!limits.emitCollector) {
647
539
  throw guestFailure("unavailable", "connecta.ui is unavailable: no emission collector was configured for this execution", true);
648
540
  }
649
- const payload = await validateUiReads(requireUiPayload(values));
650
- limits.emitCollector.acceptValidatedUi(payload);
541
+ limits.emitCollector.acceptUi(...values);
651
542
  },
652
543
  batch: async (calls) => {
653
544
  const started = Date.now();
@@ -996,7 +887,7 @@ Write one plain-JavaScript async arrow function. Use only:
996
887
  - <connectorId>.<toolName>(args) for a sanitized shortcut, or connecta.call(address, args) for a canonical address.
997
888
  - connecta.search(args), connecta.describe(args), and connecta.batch(calls) for discovery and independent read-only calls.
998
889
  - connecta.emit(block) — { type: "text", text } or { type: "image" | "audio", data (base64), mimeType }. Success-only; ${emitBudgets.maxBlocks} blocks/${emitBudgets.maxBytes} bytes; invalid/over-budget throws.
999
- - connecta.ui(html, options?) for one success-only view; return the same initial summary the HTML renders.
890
+ - connecta.ui(html) for one display-only, success-only view; return the same summary the HTML renders.
1000
891
  - console.log(...) — captured.
1001
892
 
1002
893
  Programs have no portable ambient capabilities. Return JSON and reduce large results before they truncate. Fetch skills({ name: "usage" }) once for selection rules, exact result shapes, repair, examples, guide handling${connectorGuides ? ", connector-guide rules" : ""}, and runtime differences.`;
package/dist/index.js CHANGED
@@ -42,71 +42,116 @@ function normalizeAuth(auth) {
42
42
  return rank(a) - rank(b);
43
43
  });
44
44
  }
45
- const LEGACY_CONFIG_MIGRATIONS = [
46
- ["activityReadGate", "activity.readGate"],
47
- ["activityDeploymentId", "activity.deploymentId"],
48
- ["credentialEncryptionKey", "credentials.encryptionKey"],
49
- ["toolCacheTtlSeconds", "discovery.catalogTtlSeconds"],
50
- ["persistToolCatalog", "discovery.persistCatalog"],
51
- ["toolCatalogStaleSeconds", "discovery.staleCatalogSeconds"],
52
- ["probeTimeoutMs", "discovery.probeTimeoutMs"],
53
- ["defaultToolTimeoutMs", "calls.defaultTimeoutMs"],
54
- ["maxResultBytes", "calls.maxResultBytes"],
55
- ];
56
- const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
57
- /**
58
- * Fail closed at the public boundary: a JavaScript caller on the v0.6 shape
59
- * must receive one complete migration error, never silently lose an option to
60
- * a default. This runs before createConnecta reads any other config field.
61
- */
62
- function assertNoLegacyConfig(config) {
63
- const candidate = config;
64
- if (hasOwn(candidate, "toolkits")) {
65
- throw new Error("ConnectaConfig.toolkits was removed in issue #178. Deploy one " +
66
- "connecta instance per audience instead; see ethos.md.");
67
- }
68
- const credentials = candidate.credentials;
69
- const hasNestedHealth = typeof credentials === "object" &&
70
- credentials !== null &&
71
- hasOwn(credentials, "health");
72
- if (hasOwn(candidate, "credentialHealth") || hasNestedHealth) {
73
- throw new Error("`credentials.health` and legacy `credentialHealth` were removed in " +
74
- "issue #179. Credentials now fail at use; see ethos.md.");
75
- }
76
- const calls = candidate.calls;
77
- if (typeof calls === "object" &&
78
- calls !== null &&
79
- hasOwn(calls, "maxBatchResultBytes")) {
80
- throw new Error("`calls.maxBatchResultBytes` was removed in issue #273 along with " +
81
- "batch_call. Remove it; a program's batching is bounded by " +
82
- "execute_code's own limits — connecta.batch's per-run call ceiling " +
83
- "and the executor result cap — while each call inside it still honours " +
84
- "calls.maxResultBytes and any per-connector override.");
85
- }
86
- const found = [];
87
- if (hasOwn(candidate, "activity")) {
88
- const activity = candidate.activity;
89
- const isObject = typeof activity === "object" && activity !== null;
90
- // A valid v0.6 ActivityStore can itself own a backend field named `store`.
91
- // Its required `record` method (including a prototype method) therefore
92
- // takes precedence over the otherwise-new wrapper shape.
93
- const hasLegacyRecord = isObject &&
94
- typeof activity.record === "function";
95
- if (activity !== undefined &&
96
- (!isObject ||
97
- hasLegacyRecord ||
98
- !hasOwn(activity, "store"))) {
99
- found.push(["activity", "activity.store"]);
100
- }
45
+ const admissionPoolSchema = {
46
+ concurrency: null,
47
+ maxQueueSize: null,
48
+ queueTimeoutMs: null,
49
+ retryAfterMs: null,
50
+ };
51
+ const CONFIG_SCHEMA = {
52
+ connectors: null,
53
+ auth: null,
54
+ storage: null,
55
+ publicUrl: null,
56
+ activity: {
57
+ store: null,
58
+ readGate: null,
59
+ deploymentId: null,
60
+ },
61
+ credentials: {
62
+ encryptionKey: null,
63
+ },
64
+ accessTokens: {
65
+ maxActive: null,
66
+ },
67
+ discovery: {
68
+ concurrency: null,
69
+ catalogTtlSeconds: null,
70
+ persistCatalog: null,
71
+ staleCatalogSeconds: null,
72
+ probeTimeoutMs: null,
73
+ },
74
+ calls: {
75
+ defaultTimeoutMs: null,
76
+ maxResultBytes: null,
77
+ },
78
+ execute: {
79
+ maxEmittedBytes: null,
80
+ maxEmittedBlocks: null,
81
+ },
82
+ admission: {
83
+ requests: admissionPoolSchema,
84
+ code: admissionPoolSchema,
85
+ },
86
+ branding: {
87
+ productName: null,
88
+ productUrl: null,
89
+ ownerName: null,
90
+ ownerUrl: null,
91
+ description: null,
92
+ pageTitle: null,
93
+ favicon: {
94
+ svg: null,
95
+ ico: null,
96
+ href: null,
97
+ },
98
+ themeColor: null,
99
+ },
100
+ logger: null,
101
+ serverInfo: {
102
+ name: null,
103
+ version: null,
104
+ title: null,
105
+ websiteUrl: null,
106
+ icons: [
107
+ {
108
+ src: null,
109
+ mimeType: null,
110
+ sizes: null,
111
+ },
112
+ ],
113
+ },
114
+ deploymentInfo: null,
115
+ executor: null,
116
+ };
117
+ function unknownOptionPaths(value, path, schema) {
118
+ if (schema === null)
119
+ return [];
120
+ if (Array.isArray(schema)) {
121
+ if (!Array.isArray(value))
122
+ return [];
123
+ return value.flatMap((entry, index) => unknownOptionPaths(entry, `${path}[${index}]`, schema[0]));
101
124
  }
102
- for (const migration of LEGACY_CONFIG_MIGRATIONS) {
103
- if (hasOwn(candidate, migration[0]))
104
- found.push(migration);
125
+ if (typeof value !== "object" || value === null)
126
+ return [];
127
+ const known = new Set(Object.keys(schema));
128
+ const unknown = Reflect.ownKeys(value)
129
+ .filter((key) => typeof key !== "string" || !known.has(key))
130
+ .map((key) => `${path}.${String(key)}`)
131
+ .sort();
132
+ if (unknown.length > 0)
133
+ return unknown;
134
+ for (const [key, childSchema] of Object.entries(schema)) {
135
+ unknown.push(...unknownOptionPaths(value[key], `${path}.${key}`, childSchema));
105
136
  }
106
- if (found.length === 0)
137
+ return unknown;
138
+ }
139
+ function rejectUnknownOptions(paths) {
140
+ if (paths.length === 0)
107
141
  return;
108
- throw new Error("Unsupported v0.6.x ConnectaConfig options. Migrate each path for v0.7.0:\n" +
109
- found.map(([oldPath, newPath]) => `- ${oldPath} -> ${newPath}`).join("\n"));
142
+ throw new Error(`Unknown Connecta configuration option${paths.length === 1 ? "" : "s"}:\n` +
143
+ paths.map((path) => `- ${path}`).join("\n"));
144
+ }
145
+ /** Reject JavaScript typos and removed options before construction does work. */
146
+ function assertKnownConfig(config) {
147
+ rejectUnknownOptions(unknownOptionPaths(config, "ConnectaConfig", CONFIG_SCHEMA));
148
+ const activity = config.activity;
149
+ if (activity !== undefined &&
150
+ (typeof activity !== "object" ||
151
+ activity === null ||
152
+ typeof activity.store?.record !== "function")) {
153
+ throw new Error("ConnectaConfig.activity.store must implement record(event)");
154
+ }
110
155
  }
111
156
  /**
112
157
  * One-time construction warnings for deployment shapes that run fine but are
@@ -193,11 +238,7 @@ function warnInsecureConfig(config, inboundAuth, logger) {
193
238
  }
194
239
  }
195
240
  export function createConnecta(config) {
196
- assertNoLegacyConfig(config);
197
- if (hasOwn(config, "surface")) {
198
- throw new Error("ConnectaConfig.surface was removed in issue #273. Remove it; connecta " +
199
- "now serves one seven-tool surface.");
200
- }
241
+ assertKnownConfig(config);
201
242
  if (!config.executor) {
202
243
  throw new Error("ConnectaConfig.executor is required. Configure quickJsExecutor() from " +
203
244
  '"@zackbart/connecta/quickjs" on Node, or ' +
@@ -57,7 +57,7 @@ export interface InvocationContext<T> {
57
57
  requestSignal?: AbortSignal;
58
58
  unwrapResult?: boolean;
59
59
  /**
60
- * Caller-owned result policy. MCP passes fields + paging here; code mode
60
+ * Caller-owned result policy. MCP applies result paging here; code mode
61
61
  * normally accepts the already-unwrapped value unchanged.
62
62
  */
63
63
  processResult?: (value: unknown, resolved: ResolvedCatalogTool) => T | Promise<T>;
@@ -46,7 +46,6 @@ type ResultMode = "mcp" | "value";
46
46
  export interface CallArgs {
47
47
  address: string;
48
48
  args?: Record<string, unknown>;
49
- fields?: string[];
50
49
  resultMode?: ResultMode;
51
50
  timeoutMs?: number;
52
51
  /** Retries after the first attempt; honored only for safely annotated tools. */