@zackbart/connecta 0.19.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/README.md +3 -1
  3. package/bin/connecta.mjs +23 -6
  4. package/dist/apps-shell.d.ts +10 -12
  5. package/dist/apps-shell.js +29 -220
  6. package/dist/auth/bearer.d.ts +2 -2
  7. package/dist/auth/bearer.js +2 -2
  8. package/dist/auth/clerk.js +1 -0
  9. package/dist/auth/cloudflare-access.d.ts +8 -0
  10. package/dist/auth/cloudflare-access.js +66 -0
  11. package/dist/execute.d.ts +0 -7
  12. package/dist/execute.js +20 -123
  13. package/dist/index.d.ts +2 -2
  14. package/dist/index.js +110 -69
  15. package/dist/invocation.d.ts +1 -1
  16. package/dist/meta-tools.d.ts +0 -1
  17. package/dist/meta-tools.js +10 -495
  18. package/dist/operator-ui/generated.d.ts +2 -2
  19. package/dist/operator-ui/generated.js +1 -1
  20. package/dist/operator-ui/model.d.ts +3 -3
  21. package/dist/operator-ui/view.d.ts +1 -1
  22. package/dist/operator-ui/view.js +6 -3
  23. package/dist/routes/access-tokens.d.ts +1 -1
  24. package/dist/routes/access-tokens.js +2 -2
  25. package/dist/routes/activity.js +2 -2
  26. package/dist/routes/credentials.js +1 -1
  27. package/dist/routes/mcp.js +1 -1
  28. package/dist/routes/oauth.js +1 -1
  29. package/dist/routes/shared.d.ts +4 -4
  30. package/dist/routes/shared.js +10 -10
  31. package/dist/routes/ui.js +12 -9
  32. package/dist/skills.d.ts +1 -1
  33. package/dist/skills.js +11 -8
  34. package/dist/types.d.ts +37 -22
  35. package/dist/ui.d.ts +1 -1
  36. package/dist/ui.js +3 -3
  37. package/dist/version.d.ts +1 -1
  38. package/dist/version.js +1 -1
  39. package/documentation/architecture.md +7 -4
  40. package/documentation/auth.md +71 -7
  41. package/documentation/code-mode.md +23 -23
  42. package/documentation/meta-tools.md +26 -50
  43. package/documentation/operations.md +36 -21
  44. package/documentation/operator-ui.md +21 -5
  45. package/documentation/provider-conventions.md +3 -4
  46. package/documentation/upgrading.md +106 -8
  47. package/ethos.md +3 -4
  48. package/examples/worker/README.md +52 -32
  49. package/examples/worker/src/index.ts +32 -38
  50. package/examples/worker/wrangler.jsonc +12 -4
  51. package/package.json +5 -1
  52. package/templates/node/package.json +1 -1
@@ -0,0 +1,66 @@
1
+ function identityString(identity, field) {
2
+ const value = identity[field];
3
+ return typeof value === "string" && value.length > 0 ? value : undefined;
4
+ }
5
+ function unauthorized() {
6
+ return {
7
+ ok: false,
8
+ response: Response.json({ error: "Cloudflare Access authentication required" }, { status: 401 }),
9
+ };
10
+ }
11
+ /**
12
+ * Trust the identity Cloudflare Access attached to this direct Worker
13
+ * invocation. Access has already validated the browser session, Managed OAuth
14
+ * token, or service-token headers before the Worker runs; this adapter does
15
+ * not accept or parse a caller-supplied JWT.
16
+ */
17
+ export function cloudflareAccessAuth() {
18
+ return {
19
+ kind: "cloudflare-access",
20
+ interactiveOperator: true,
21
+ activityActorNamespace: "cloudflare-access",
22
+ uiAuth: { kind: "cloudflare-access" },
23
+ async authorize(_request, _baseUrl, runtimeContext) {
24
+ const access = runtimeContext?.access;
25
+ if (!access)
26
+ return unauthorized();
27
+ let identity;
28
+ try {
29
+ identity = await access.getIdentity();
30
+ }
31
+ catch {
32
+ return unauthorized();
33
+ }
34
+ if (!identity) {
35
+ // Access service-token policies authenticate the request and attach
36
+ // ctx.access, but getIdentity() is a user-identity API and returns
37
+ // undefined. Cloudflare strips the service-token headers before the
38
+ // Worker, so the Access application audience is the only trusted,
39
+ // stable service attribution available without parsing a JWT.
40
+ return { ok: true, subjectId: access.aud };
41
+ }
42
+ const userId = identityString(identity, "user_uuid") ??
43
+ identityString(identity, "email");
44
+ const commonName = identityString(identity, "common_name");
45
+ const serviceTokenId = identityString(identity, "service_token_id");
46
+ if (identity.service_token_status === true ||
47
+ serviceTokenId ||
48
+ (!userId && commonName)) {
49
+ const subjectId = serviceTokenId ?? commonName;
50
+ return subjectId
51
+ ? { ok: true, subjectId }
52
+ : {
53
+ ok: false,
54
+ response: Response.json({ error: "Cloudflare Access service identity required" }, { status: 403 }),
55
+ };
56
+ }
57
+ if (!userId) {
58
+ return {
59
+ ok: false,
60
+ response: Response.json({ error: "Cloudflare Access user identity required" }, { status: 403 }),
61
+ };
62
+ }
63
+ return { ok: true, userId, subjectId: userId };
64
+ },
65
+ };
66
+ }
package/dist/execute.d.ts CHANGED
@@ -91,14 +91,8 @@ export type EmittedBlock = {
91
91
  data: string;
92
92
  mimeType: string;
93
93
  };
94
- interface UiReadBinding {
95
- address: string;
96
- fixedArgs: Record<string, unknown>;
97
- viewArgs: string[];
98
- }
99
94
  interface UiPayload {
100
95
  html: string;
101
- reads?: Record<string, UiReadBinding>;
102
96
  }
103
97
  /**
104
98
  * Request-local collection for `connecta.emit` and `connecta.ui`. Budgets fail
@@ -136,7 +130,6 @@ export declare class EmitCollector {
136
130
  * complaint about its type would send the author to fix the wrong thing.
137
131
  */
138
132
  acceptUi(...values: unknown[]): void;
139
- acceptValidatedUi(payload: UiPayload): void;
140
133
  private assertUiVacant;
141
134
  private acceptUiPayload;
142
135
  }
package/dist/execute.js CHANGED
@@ -6,8 +6,7 @@ import { guardExecuteResultValue, MAX_EXECUTE_LOG_CHARS, truncateExecuteText, }
6
6
  import { ExecutorAdmissionError, ExecutorExecutionError, isAdmittingExecutor, } from "./executor-admission.js";
7
7
  import { boundedEchoText, classifyCallError, msg } from "./errors.js";
8
8
  import { InvocationFailure, InvocationService, } from "./invocation.js";
9
- import { hasConnectorGuides } from "./skills.js";
10
- import { isExplicitlyReadOnly } from "./tool-safety.js";
9
+ import { connectorGuide, connectorGuideRequired, connectorSkillName, hasConnectorGuides, } from "./skills.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();
@@ -965,9 +856,15 @@ function connectorInventory(connectors) {
965
856
  return `${prefix}none.`;
966
857
  const entries = connectors.map((connector) => {
967
858
  const shortcut = sanitizeIdentifier(connector.id);
968
- return shortcut === connector.id
859
+ const address = shortcut === connector.id
969
860
  ? connector.id
970
861
  : `${connector.id} (shortcut ${shortcut})`;
862
+ if (!connectorGuide(connector))
863
+ return address;
864
+ const requirement = connectorGuideRequired(connector)
865
+ ? "required guide"
866
+ : "guide";
867
+ return `${address} (${requirement} ${connectorSkillName(connector.id)})`;
971
868
  });
972
869
  const shown = [];
973
870
  for (let index = 0; index < entries.length; index++) {
@@ -988,18 +885,18 @@ function connectorInventory(connectors) {
988
885
  return `${prefix}${shown.join(", ")}.`;
989
886
  return `${prefix}${shown.join(", ")}${shown.length > 0 ? "; " : ""}+${omitted} more.`;
990
887
  }
991
- const executeDescription = (emitBudgets, connectorGuides, connectors) => `Choose the route before discovery. Exactly one unknown-address read uses top-level search_tools then call_tool. execute_code is the primary surface for everything wider: make exactly one execute_code call that searches, selects, calls, and reduces. A discovery-only program wastes its round trip: finish here, don't return catalog matches for a later call. Only readOnlyHint: true tools are available. Limits: ${EXECUTE_MAX_HOST_CALLS} host calls per run, ${EXECUTE_MAX_BATCH_CALLS} per batch, ${EXECUTE_HOST_CALL_TIMEOUT_MS / 1_000}-second host deadline.
888
+ const executeDescription = (emitBudgets, connectorGuides, connectors) => `Choose the route before discovery. A known address uses call_tool. Unknown-address and wider read-only work use exactly one execute_code call that discovers, calls, and returns the answer. Finish in that program; don't return catalog matches for a later call. Only readOnlyHint: true tools are available. Limits: ${EXECUTE_MAX_HOST_CALLS} host calls per run, ${EXECUTE_MAX_BATCH_CALLS} per batch, ${EXECUTE_HOST_CALL_TIMEOUT_MS / 1_000}-second host deadline.
992
889
 
993
890
  ${connectorInventory(connectors)}
994
891
 
995
- Write one plain-JavaScript async arrow function. Use only:
892
+ Fetch required guides named above before executing. Write one plain-JavaScript async arrow function. Use only:
996
893
  - <connectorId>.<toolName>(args) for a sanitized shortcut, or connecta.call(address, args) for a canonical address.
997
- - connecta.search(args), connecta.describe(args), and connecta.batch(calls) for discovery and independent read-only calls.
894
+ - connecta.search(args) returns { tools }; connecta.describe(args) returns { tools }; use entry key lists. connecta.batch(calls) accepts canonical connector addresses only.
998
895
  - 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.
896
+ - connecta.ui(html) for one display-only, success-only view; return the same summary the HTML renders.
1000
897
  - console.log(...) — captured.
1001
898
 
1002
- 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.`;
899
+ No portable ambient capabilities. Return JSON; reduce large results before truncation. Build arguments from required input keys and schemas, never descriptions or output keys. Fetch skills({ name: "usage" }) only when this is insufficient or repair is needed; it has full rules, examples${connectorGuides ? ", guide handling" : ""}, and runtime details.`;
1003
900
  /** Register the execute_code meta-tool. Only called when an executor is configured. */
1004
901
  export function registerExecuteTool(server, registry, ctx) {
1005
902
  // Resolved once so the description and the collector cannot disagree about
package/dist/index.d.ts CHANGED
@@ -132,7 +132,7 @@ export interface ConnectaConfig {
132
132
  credentials?: ConnectaCredentialsConfig;
133
133
  /**
134
134
  * Named, revocable Bearer tokens for MCP clients. Creation and mutation
135
- * require an eligible Clerk operator; token secrets are returned once.
135
+ * require an eligible interactive operator; token secrets are returned once.
136
136
  */
137
137
  accessTokens?: ConnectaAccessTokensConfig;
138
138
  /** Tool-catalog caching, persistence, stale fallback, and probe deadlines. */
@@ -192,6 +192,6 @@ export { CONNECTA_VERSION } from "./version.js";
192
192
  export type { Registry } from "./registry.js";
193
193
  export type { RemoteMcpOptions, RemoteMcpAuth, RemoteMcpRedirectPolicy, } from "./connectors/remote-mcp.js";
194
194
  export type { ApiOptions, ApiTool } from "./connectors/api.js";
195
- export type { CatalogDriftCounts, CatalogDriftReport, Connector, ConnectorCallAdmissionInput, ConnectorCallAdmissionPolicy, ConnectorCallAdmissionRule, ConnectorRollingWindowBudget, ConnectaBranding, ConnectorCredentialAccess, ConnectorCredentialConfig, ConnectorCredentialFieldConfig, ConnectorCredentialValues, ConnectorContext, ConnectorUsageGuide, ConnectorStatus, CredentialTestResult, AdmittingExecutor, AdmissionSnapshot, ExecuteResult, Executor, ExecutorLease, ExecutorProvider, InboundAuth, UiAuthConfig, AuthResult, JsonSchema, KVStorage, Logger, ToolDef, ToolAnnotations, } from "./types.js";
195
+ export type { CatalogDriftCounts, CatalogDriftReport, Connector, ConnectorCallAdmissionInput, ConnectorCallAdmissionPolicy, ConnectorCallAdmissionRule, ConnectorRollingWindowBudget, ConnectaBranding, ConnectorCredentialAccess, ConnectorCredentialConfig, ConnectorCredentialFieldConfig, ConnectorCredentialValues, ConnectorContext, ConnectorUsageGuide, ConnectorStatus, CredentialTestResult, AdmittingExecutor, AdmissionSnapshot, ExecuteResult, Executor, ExecutorLease, ExecutorProvider, InboundAuth, InboundAuthRuntimeContext, UiAuthConfig, AuthResult, JsonSchema, KVStorage, Logger, ToolDef, ToolAnnotations, } from "./types.js";
196
196
  export type { ActivityActor, ActivityCallSource, ActivityOutcome, ActivityPage, ActivityReadActor, ActivityReadEvent, ActivityReader, ActivityReadGate, ActivityReadPage, ActivitySink, ActivityStore, AgentFriction, CatalogDriftActivityEvent, ToolCallActivityEvent, } from "./activity.js";
197
197
  export { InvalidActivityCursorError } from "./activity.js";
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 ' +
@@ -221,8 +262,8 @@ export function createConnecta(config) {
221
262
  ? new AccessTokenManager(storage, config.accessTokens)
222
263
  : undefined;
223
264
  if (accessTokens &&
224
- !configuredAuth.some((provider) => provider.uiAuth?.kind === "clerk")) {
225
- throw new Error("accessTokens requires a Clerk auth provider: only an eligible Clerk " +
265
+ !configuredAuth.some((provider) => provider.interactiveOperator)) {
266
+ throw new Error("accessTokens requires an interactive operator auth provider: only an eligible " +
226
267
  "operator may create, rename, or revoke deployment access tokens");
227
268
  }
228
269
  const serverInfo = {
@@ -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. */