@zackbart/connecta 0.17.0 → 0.18.1

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 CHANGED
@@ -2,6 +2,139 @@
2
2
 
3
3
  All notable changes to this package are documented here.
4
4
 
5
+ ## 0.18.1 — 2026-08-18
6
+
7
+ This patch is the response to one long investigation run against a live
8
+ deployment: a Mixpanel-and-Stripe user-journey reconstruction that rated the
9
+ connectors trustworthy and the ceremony too high. Nothing breaks and no
10
+ constructor changes. The two maintained guides now say the things that
11
+ investigation had to discover the hard way — which questions Insights cannot
12
+ answer, when Mixpanel's `false` is an absent property, which Stripe reads must
13
+ be projected inside the sandbox and why that is a privacy boundary as much as
14
+ a size one, and which fields Stripe search will not filter on. Beside them, a
15
+ new maintained RevenueCat connection lands, with the fact that decides its
16
+ deployment shape stated up front: a RevenueCat secret key reaches exactly one
17
+ project, so several projects are several connectors. A deployment that
18
+ constructs none of the three connections can ignore this release entirely.
19
+
20
+ ### Added
21
+
22
+ - **`revenuecat()` — a maintained RevenueCat hosted-MCP connection** at
23
+ `@zackbart/connecta/providers/revenuecat`. OAuth by default (account-scoped:
24
+ the guide requires `list-projects` first and carries `project_id` into every
25
+ project-scoped call, stopping on ambiguity); an API v2 secret key as the
26
+ headless alternative, which binds the connector to one project — the guide's
27
+ first line names it, and a second project is a second connector with its own
28
+ key. All 95 documented tools are classified from RevenueCat's tools reference
29
+ (50 read-only, 15 additive, 29 destructive); `render-paywall-screenshot`
30
+ carries no access column there and fails closed. Borderline verdicts are
31
+ argued beside the rows they decide: `create-webhook-integration`,
32
+ `grant-customer-entitlement`, and `assign-customer-offering` are destructive
33
+ on consequence, not on verb. RevenueCat publishes six per-domain rate limits
34
+ and one admission rule cannot honestly cover 95 tools, so `callAdmission`
35
+ stays with the operator and the guide states the numbers. The maintainer-run
36
+ drift check accepts `revenuecat` behind `CONNECTA_DRIFT_REVENUECAT_KEY`; no
37
+ schema digests are recorded until a maintainer runs `--record` against a live
38
+ project (#433).
39
+
40
+ ### Changed
41
+
42
+ - **The Mixpanel guide says what Insights cannot answer.** One analysis is one
43
+ `execute_code` program with one `Get-Query-Schema` fetch; per-user event
44
+ timelines and sequence questions are out of reach through `Run-Query`
45
+ (`Get-User-Replays-Data` covers one user only where session replay is on);
46
+ `false` on a boolean property may be an absent one and must be confirmed with
47
+ `List-Properties` before it becomes a signal; breakdown responses are
48
+ flattened one row per combination inside the program with `$overall`
49
+ dropped. Convention P7 gains the rendering-rule clause this rests on (#430).
50
+
51
+ - **The Stripe guide names the reads that must be projected and the fields
52
+ search cannot filter.** Any `stripe_api_read` list or `stripe_api_search`
53
+ that returns full objects belongs inside `execute_code`, projected before
54
+ `return` — the reduction keeps customer PII out of the transcript, not only
55
+ bytes. Charges search takes a documented field set with no `payment_intent`;
56
+ follow the PaymentIntent's `latest_charge` instead. The account → search →
57
+ details → read sequence is one program, and decline outcomes are `outcome`,
58
+ `failure_code`, `failure_message` on the charge (#431).
59
+
60
+ ## 0.18.0 — 2026-08-13
61
+
62
+ This minor release spends fewer tokens before the first call and answers
63
+ better when a call goes wrong. The always-loaded MCP instructions and tool
64
+ definitions are 44.6% lighter, the `execute_code` description now names the
65
+ deployment's configured connector IDs before the first catalog search, and a
66
+ failed `connecta.describe` entry carries the same typed recovery as a failed
67
+ call. The compatibility change is the floor: Node 22 is now the minimum
68
+ supported release, matching the shipped Docker template, and CI runs one job
69
+ per pull request against it; the Cloudflare Worker target is unchanged. Stripe
70
+ OAuth deployments must drop the connector-wide `mode` — each returned account
71
+ now carries its own. An evaluation of erasable TypeScript syntax in
72
+ `execute_code` programs ended in a recorded refusal, so portable programs
73
+ remain plain JavaScript. A deployment already on Node 22 that lets accounts
74
+ carry their Stripe mode needs no configuration change.
75
+
76
+ ### Added
77
+
78
+ - **`execute_code` now names the deployment's connectors up front.** Its
79
+ description carries a connector inventory derived only from the configured
80
+ registry: canonical IDs in registry order, the executable shortcut when
81
+ sanitization changes the ID, bounded to 256 serialized bytes with an exact
82
+ omitted count. A cold model can scope its first catalog search without
83
+ guessing, and rendering the inventory performs no catalog request and no
84
+ credential access (#416).
85
+
86
+ - **Failed `connecta.describe` entries are now as repairable as failed calls.**
87
+ Each failed entry carries `errorDetails` beside the preserved human text: the
88
+ invocation path's stable `code` and `retryable`, a route-aware `nextAction`,
89
+ and up to three deterministic nearby canonical addresses when the connector
90
+ is known. Caller-authored addresses are clamped everywhere they echo, so
91
+ hostile input can no longer displace partial results (#417).
92
+
93
+ ### Changed
94
+
95
+ - **The always-loaded surface got 44.6% lighter.** The MCP instructions and the
96
+ seven tool definitions now carry route selection, the fail-closed read-only
97
+ boundary, and the minimum guest syntax — 6,850 bytes instead of 12,358.
98
+ Selection discipline, runtime differences, worked examples, direct-call
99
+ options, `get_result` rules, the `connecta.ui` reads-binding shape, and
100
+ repair guidance moved into the on-demand `usage` skill, which grew to a
101
+ deliberate 9,000-byte cap and stays byte-identical across deployments.
102
+ Clients that never fetch the skill keep correct routing from the compact
103
+ definitions alone (#418).
104
+
105
+ - **Node 22 is now the minimum supported Node release.** CI and the published
106
+ engine range now match the Node 22 runtime used by the shipped deployment
107
+ template. The Worker test project continues to cover the other deployment
108
+ target (#422).
109
+
110
+ - **Stripe OAuth mode now belongs to each returned account.** OAuth-backed
111
+ connectors no longer accept a connector-wide `mode`. Their neutral metadata
112
+ and guide support mixed live and sandbox accounts, require
113
+ `list_available_accounts_or_orgs`, and carry its exact `stripe_context` and
114
+ `livemode` into each account-scoped call. They use the stricter sandbox
115
+ admission ceiling. Header credentials still require one fixed mode, retain
116
+ key-prefix contradiction checks, and keep Stripe Connect behavior (#414).
117
+
118
+ - **The reviewed Notion writes stay narrow.** Newly published workspace-private
119
+ creation, templates, placement, richer media, locking, and irreversible
120
+ content erasure do not join the maintained surface. They are distinct
121
+ ownership, asynchronous, ordering, file, coordination, or deletion workflows,
122
+ not extra fields on `create_page` or `update_page_properties`. The existing
123
+ request subsets remain valid, and `trash_page` stays isolated and reversible
124
+ (#408, #409).
125
+
126
+ ### Fixed
127
+
128
+ - **The Node template now declares its one reviewed install script.** npm 11 no
129
+ longer warns that esbuild's install is unreviewed, and npm 12 will not block
130
+ it. The approval is pinned to `esbuild@0.28.2`; the package smoke reads the
131
+ generated lockfile and fails if any resolved dependency has an unapproved
132
+ install script, including a future esbuild version. Local and container
133
+ installs keep the same `tsx` runtime path (#375).
134
+
135
+ - The documentation gate now ignores `.claude`, whose nested agent worktrees
136
+ are separate historical checkouts rather than source in the current tree.
137
+
5
138
  ## 0.17.0 — 2026-08-13
6
139
 
7
140
  This minor release makes catalog discovery faster and its answers more exact.
package/README.md CHANGED
@@ -2,129 +2,94 @@
2
2
 
3
3
  ![A monochrome clay Connecta hub joining many tools](https://raw.githubusercontent.com/zackbart/connecta/main/assets/connecta-clay-hero.png)
4
4
 
5
- One place for AI agents to connect to the tools you choose.
5
+ One MCP endpoint. The integrations you chose. Your agent reaches them by
6
+ writing code instead of loading a thousand tool definitions.
6
7
 
7
- Connecta gives an agent a single MCP endpoint instead of making it connect to
8
- every service separately. You decide which integrations are available and
9
- Connecta holds their credentials, and the agent mostly works by writing
10
- ordinary JavaScript that Connecta runs in a sandbox next to them — with a few
11
- explicit tools for the jobs a program is the wrong shape for, destructive calls
12
- among them.
8
+ ## The mental model
13
9
 
14
- ```mermaid
15
- flowchart LR
16
- Agent["AI agent"]
17
- Integrations["The integrations you chose"]
18
-
19
- subgraph Connecta["Connecta: one MCP endpoint; credentials stay here"]
20
- Sandbox["execute_code<br/>server-side sandbox"]
21
- Explicit["Explicit tools<br/>destructive calls, search, auth"]
22
- end
23
-
24
- Agent -->|"writes a program"| Sandbox
25
- Agent -->|"one deliberate call"| Explicit
26
- Sandbox --> Integrations
27
- Explicit --> Integrations
28
- ```
10
+ You ask your agent a question that touches a service — Linear, Stripe, an
11
+ internal API, anything you have connected. Here is what happens:
29
12
 
30
- ## Why Connecta
13
+ 1. The agent talks to one endpoint, yours, and sees seven tools. Always seven,
14
+ no matter how many services sit behind it.
15
+ 2. It writes a short JavaScript program. Connecta runs it in a sandbox next to
16
+ your integrations. The program can search for tools, call them, chain the
17
+ calls, and shape the result.
18
+ 3. Only the answer comes back into the agent's context — not raw pages of
19
+ API output.
20
+ 4. If the agent wants to change something — create, update, delete — it
21
+ cannot do that from a program. It makes one explicit call, and your MCP
22
+ client can put that call in front of you first.
31
23
 
32
- - **One connection.** Configure clients once, even as integrations change.
33
- - **Seven tools, not seven hundred.** A program can search the catalog, chain
34
- calls, and trim the results before the agent ever sees them — so nothing has
35
- to be loaded up front.
36
- - **Safer access.** Credentials stay server-side — the program never sees them
37
- — and consequential actions remain explicit and individual.
38
- - **Named client access.** A Clerk operator can issue and revoke one-time,
39
- hashed Bearer tokens for MCP clients that support header authentication.
40
- - **Your deployment.** Connecta runs on Node, Docker, or Cloudflare Workers,
41
- with configuration you can review and version.
24
+ Credentials never leave the server. The program never sees them, and neither
25
+ does the agent.
42
26
 
43
- ## Start here
27
+ ```mermaid
28
+ flowchart TB
29
+ Client["Your MCP client<br/>Claude, Cursor, …"]
44
30
 
45
- Create the prescribed Node deployment:
31
+ subgraph Connecta["Connecta one endpoint, seven tools, your credentials"]
32
+ Sandbox["execute_code<br/>the agent's program runs here<br/>read-only tools only"]
33
+ Explicit["call_destructive_tool<br/>one visible call per write<br/>your client can ask you first"]
34
+ end
46
35
 
47
- ```sh
48
- npx @zackbart/connecta init my-connecta
49
- cd my-connecta
50
- npm install
51
- CONNECTA_TOKEN=dev-token npm start
52
- ```
36
+ Integrations["The integrations you chose<br/>Linear · Stripe · Notion · your HTTP API · any MCP server"]
53
37
 
54
- Point an MCP client at `http://localhost:8787/mcp` with
55
- `Authorization: Bearer dev-token`. The generated project is deliberately small:
56
-
57
- ```text
58
- my-connecta/
59
- ├── src/index.ts # connectors, auth, storage, public URL
60
- ├── src/file-activity.ts # deployment-owned activity store, wired on request
61
- ├── package.json # exact Connecta and QuickJS versions
62
- ├── tsconfig.json
63
- ├── Dockerfile # the same source, containerized
64
- ├── docker-compose.yml # one service, state on a named volume
65
- ├── .dockerignore
66
- ├── .env.example
67
- ├── .gitignore
68
- ├── AGENTS.md
69
- ├── CLAUDE.md -> AGENTS.md
70
- └── README.md
38
+ Client -->|"one connection"| Sandbox
39
+ Client --> Explicit
40
+ Sandbox -->|"reads"| Integrations
41
+ Explicit -->|"writes"| Integrations
71
42
  ```
72
43
 
73
- The same project runs as a long-lived container without changing a line of it:
74
-
75
- ```sh
76
- cp .env.example .env # set CONNECTA_TOKEN
77
- docker compose up -d --build
44
+ This is the kind of thing the agent writes, not you:
45
+
46
+ ```js
47
+ async () => {
48
+ const { nodes } = await tracker.list_issues({ state: "started" });
49
+ const byOwner = {};
50
+ for (const issue of nodes) {
51
+ (byOwner[issue.assignee?.name ?? "unassigned"] ??= []).push(issue.identifier);
52
+ }
53
+ return byOwner;
54
+ }
78
55
  ```
79
56
 
80
- For an agent setting this up, the contract is:
81
-
82
- 1. Edit `src/index.ts`; do not copy Connecta internals into the deployment.
83
- 2. Keep the required `executor: quickJsExecutor()` configuration; without an
84
- executor the deployment refuses to boot.
85
- 3. Keep secrets in environment variables or a secret store, never source.
86
- 4. Add connectors explicitly: import a maintained prebuilt provider
87
- constructor when one exists, otherwise write a deliberate `remoteMcp()` or
88
- `api()` connector. There is no registry to browse and nothing registers
89
- itself.
90
- 5. Run `npm run typecheck`, start the server, and run
91
- `CONNECTA_TOKEN=... npm run doctor`. Doctor checks health, the executor, and
92
- the exact seven-tool model-facing surface, then executes a harmless sandbox
93
- program. The bearer stays in the environment rather than command history.
94
-
95
- The operator surface is the same in both shapes and off until you say so:
96
- Clerk sign-in, the credential vault, access-token issuance, and payload-free
97
- activity ship as commented configuration, each one a variable and an
98
- uncommented block away. The generated `README.md` walks through all four, and
99
- the [Worker example](./examples/worker/) does the same for KV and D1.
100
-
101
- Already have a deployment on an older version? `init` deliberately refuses to
102
- merge into it, so bringing one current is its own procedure:
103
- [Upgrading an existing deployment](./documentation/upgrading.md) is the
104
- runbook, written for the agent working inside that project.
105
-
106
- The template refuses to merge into an existing directory, so initialization
107
- cannot overwrite another project. Its generated programs have no filesystem,
108
- environment, arbitrary network, imports, or timers; only explicitly read-only
109
- connector tools are reachable. Unannotated or write-capable calls stay
110
- individual and cross `call_destructive_tool`, where the MCP host can ask the
111
- operator for approval.
112
-
113
- There are two deployment shapes and no others:
114
-
115
- - [Node, local or Docker](./templates/node/) — what `init` copies
116
- - [Cloudflare Worker deployment](./examples/worker/)
117
- - [Subsystem documentation](./documentation/)
118
-
119
- Every deployment configures a sandbox: QuickJS on Node or a Dynamic Worker on
120
- Cloudflare. Construction fails with an actionable error when the executor is
121
- missing, so the model-facing interface is always the same seven tools.
122
-
123
- ## Project status
124
-
125
- Connecta is built for its author's deployments first and is still evolving.
126
- Breaking changes are expected before 1.0.
127
-
128
- Read the [ethos](./ethos.md) for the product's principles, the
129
- [changelog](./CHANGELOG.md) for releases, and [security policy](./SECURITY.md)
130
- for vulnerability reporting.
57
+ Fifty issues in, one small object out. Your context window notices.
58
+
59
+ ## What you can do with it
60
+
61
+ - **Put every MCP server you use behind one connection.** Add or remove
62
+ services in a config file; your client never changes.
63
+ - **Wrap any HTTP API by hand.** A few lines per tool. No OpenAPI conversion —
64
+ generated tool sprawl is the problem, not the fix.
65
+ - **Use maintained connections** for Cloudflare, Linear, Mixpanel, Notion,
66
+ RevenueCat, and Stripe — known endpoints, auth defaults, and vetted read/write
67
+ classifications, imported one at a time.
68
+ - **Let the agent work in code.** Search, chain, filter, join, and reduce
69
+ inside the sandbox instead of round-tripping every call through the model.
70
+ - **Keep writes deliberate.** Only tools marked read-only run in a program.
71
+ Everything else is a separate, visible call your client can gate.
72
+ - **Run it where you like.** Node, a Docker container, or a Cloudflare Worker,
73
+ from the same small deployment file.
74
+
75
+ There is also an operator surface, off until you turn it on: sign-in, an
76
+ encrypted credential vault with rotation, revocable per-client tokens, and a
77
+ payload-free activity log.
78
+
79
+ Connecta is not a platform, a marketplace, a policy engine, or a multi-tenant
80
+ service. Those are decisions, and the [ethos](./ethos.md) records each one
81
+ and why.
82
+
83
+ ## Getting started
84
+
85
+ Setup is written for an agent. Point yours at [`AGENTS.md`](./AGENTS.md) and
86
+ ask it to set up a Connecta deployment; the
87
+ [documentation](./documentation/) covers every subsystem if you want to go
88
+ deeper, and [upgrading](./documentation/upgrading.md) an existing deployment
89
+ is its own runbook.
90
+
91
+ ## Status
92
+
93
+ Built for its author's deployments first and published openly. Breaking
94
+ changes are expected before 1.0. See the [changelog](./CHANGELOG.md) and
95
+ [security policy](./SECURITY.md).
@@ -72,6 +72,11 @@ interface CatalogFailureDetail {
72
72
  retryable: boolean;
73
73
  retryAfterMs?: number;
74
74
  }
75
+ interface CatalogDescriptionFailureDetail extends CatalogFailureDetail {
76
+ nextAction?: NonNullable<CallErrorDetails["nextAction"]>;
77
+ /** Nearby canonical addresses, ranked deterministically by tool name. */
78
+ suggestions?: string[];
79
+ }
75
80
  export interface CatalogSearchPage {
76
81
  entries: CatalogSearchEntry[];
77
82
  total: number;
@@ -109,6 +114,7 @@ export interface CatalogDescription {
109
114
  outputSchema?: unknown;
110
115
  annotations?: ToolDef["annotations"];
111
116
  error?: string;
117
+ errorDetails?: CatalogDescriptionFailureDetail;
112
118
  }
113
119
  export interface ResolvedCatalogTool {
114
120
  connector: Connector;
@@ -16,6 +16,7 @@ const MAX_QUERY_TERM_LENGTH = 64;
16
16
  * of the deployment.
17
17
  */
18
18
  const MAX_IDENTITY_CONNECTORS = 3;
19
+ const MAX_DESCRIBE_SUGGESTIONS = 3;
19
20
  const encoder = new TextEncoder();
20
21
  /** Clip one echoed query term without splitting a non-BMP code point. */
21
22
  function boundedQueryTerm(term) {
@@ -80,6 +81,39 @@ function recoveryQuery(address) {
80
81
  const candidate = separator >= 0 ? address.slice(separator + 1) : address;
81
82
  return boundedEchoText(candidate.replaceAll(/[._-]+/g, " ").trim() || address);
82
83
  }
84
+ function editDistance(left, right) {
85
+ let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
86
+ for (let leftIndex = 0; leftIndex < left.length; leftIndex += 1) {
87
+ const current = [leftIndex + 1];
88
+ for (let rightIndex = 0; rightIndex < right.length; rightIndex += 1) {
89
+ current.push(Math.min(previous[rightIndex + 1] + 1, current[rightIndex] + 1, previous[rightIndex] +
90
+ (left[leftIndex] === right[rightIndex] ? 0 : 1)));
91
+ }
92
+ previous = current;
93
+ }
94
+ return previous[right.length];
95
+ }
96
+ /** Nearby names only; descriptions never influence describe-miss recovery. */
97
+ function describeSuggestions(connectorId, attemptedName, tools) {
98
+ const attempted = attemptedName.toLowerCase();
99
+ return tools
100
+ .map((tool, order) => {
101
+ const name = tool.name.toLowerCase();
102
+ return { tool, order, distance: editDistance(attempted, name) };
103
+ })
104
+ .filter(({ tool, distance }) => {
105
+ const longest = Math.max(attempted.length, tool.name.length);
106
+ return (attempted.includes(tool.name.toLowerCase()) ||
107
+ tool.name.toLowerCase().includes(attempted) ||
108
+ distance <= Math.max(2, Math.floor(longest * 0.4)));
109
+ })
110
+ .sort((left, right) => left.distance - right.distance || left.order - right.order)
111
+ .map(({ tool }) => `${connectorId}.${tool.name}`)
112
+ // A clipped address would no longer be canonical. Omit an implausibly
113
+ // large catalog name instead of letting one suggestion erase the page.
114
+ .filter((address) => boundedEchoText(address) === address)
115
+ .slice(0, MAX_DESCRIBE_SUGGESTIONS);
116
+ }
83
117
  /** Serialize once and count the exact bytes the MCP adapter would emit. */
84
118
  export function boundedDiscoveryText(value, hint) {
85
119
  const text = JSON.stringify(value);
@@ -734,17 +768,48 @@ export class CatalogService {
734
768
  });
735
769
  return resolved.map(({ address, resolved: addressResolution }) => {
736
770
  if (!addressResolution) {
737
- return { address, error: `Unknown address "${address}"` };
771
+ const message = `Unknown address "${boundedEchoText(address)}"`;
772
+ return {
773
+ address: boundedEchoText(address),
774
+ error: message,
775
+ errorDetails: {
776
+ ...framingError("unknown_address", message),
777
+ nextAction: this.searchRecovery({ query: recoveryQuery(address) }, "Find the configured canonical address before retrying."),
778
+ },
779
+ };
738
780
  }
739
781
  const catalog = catalogs.get(addressResolution.connector.id);
740
782
  if (catalog instanceof Error) {
741
- return { address, error: catalog.message };
783
+ const classified = classifyCallError(catalog, "catalog_lookup_failed");
784
+ const message = boundedEchoText(classified.message);
785
+ return {
786
+ address: boundedEchoText(address),
787
+ error: message,
788
+ errorDetails: {
789
+ code: classified.code,
790
+ message,
791
+ retryable: classified.retryable,
792
+ ...(classified.retryAfterMs === undefined
793
+ ? {}
794
+ : { retryAfterMs: classified.retryAfterMs }),
795
+ },
796
+ };
742
797
  }
743
798
  const tool = catalog?.find((item) => item.name === addressResolution.toolName);
744
799
  if (!tool) {
800
+ const message = `Unknown tool "${boundedEchoText(addressResolution.toolName)}" on connector "${addressResolution.connector.id}"`;
801
+ const suggestions = describeSuggestions(addressResolution.connector.id, addressResolution.toolName, catalog ?? []);
745
802
  return {
746
- address,
747
- error: `Unknown tool "${addressResolution.toolName}" on connector "${addressResolution.connector.id}"`,
803
+ address: boundedEchoText(address),
804
+ error: message,
805
+ errorDetails: {
806
+ ...framingError("unknown_tool", message),
807
+ nextAction: this.searchRecovery({
808
+ query: recoveryQuery(addressResolution.toolName),
809
+ connector: addressResolution.connector.id,
810
+ }, "Find the connector's current canonical tool address."),
811
+ ...(suggestions.length > 0 ? { suggestions } : {}),
812
+ },
748
813
  };
749
814
  }
750
815
  const input = tool.inputSchema ?? { type: "object" };
package/dist/execute.d.ts CHANGED
@@ -6,6 +6,8 @@ import { InvocationFailure } from "./invocation.js";
6
6
  import type { RegistryView } from "./registry.js";
7
7
  import type { Executor, ExecutorProvider, Logger } from "./types.js";
8
8
  export declare const EXECUTE_MAX_BATCH_CALLS = 10;
9
+ /** Complete entries plus an exact omission count, all inside this byte cap. */
10
+ export declare const CONNECTOR_INVENTORY_MAX_BYTES = 256;
9
11
  /**
10
12
  * Default budgets for `connecta.emit`. The byte budget is a transport bound,
11
13
  * not a context bound — emitted image/audio blocks reach the model as media,
package/dist/execute.js CHANGED
@@ -4,7 +4,7 @@ import { boundedDiscoveryText, CatalogService, DiscoveryPolicyError, flatSearchR
4
4
  import { errorResult, jsonResult } from "./meta-tools.js";
5
5
  import { guardExecuteResultValue, MAX_EXECUTE_LOG_CHARS, truncateExecuteText, } from "./executor-result.js";
6
6
  import { ExecutorAdmissionError, ExecutorExecutionError, isAdmittingExecutor, } from "./executor-admission.js";
7
- import { classifyCallError } from "./errors.js";
7
+ import { boundedEchoText, classifyCallError } from "./errors.js";
8
8
  import { InvocationFailure, InvocationService, } from "./invocation.js";
9
9
  import { hasConnectorGuides } from "./skills.js";
10
10
  import { isExplicitlyReadOnly } from "./tool-safety.js";
@@ -12,6 +12,8 @@ import { isExplicitlyReadOnly } from "./tool-safety.js";
12
12
  const EXECUTE_MAX_HOST_CALLS = 20;
13
13
  export const EXECUTE_MAX_BATCH_CALLS = 10;
14
14
  const EXECUTE_HOST_CALL_TIMEOUT_MS = 15_000;
15
+ /** Complete entries plus an exact omission count, all inside this byte cap. */
16
+ export const CONNECTOR_INVENTORY_MAX_BYTES = 256;
15
17
  /**
16
18
  * Default budgets for `connecta.emit`. The byte budget is a transport bound,
17
19
  * not a context bound — emitted image/audio blocks reach the model as media,
@@ -996,24 +998,47 @@ function discardedEmitsText(emitted) {
996
998
  ];
997
999
  return lines.length > 0 ? `\n\n${lines.join("\n")}` : "";
998
1000
  }
999
- const executeDescription = (emitBudgets, connectorGuides) => `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.
1001
+ function connectorInventory(connectors) {
1002
+ const prefix = "Connectors: ";
1003
+ if (connectors.length === 0)
1004
+ return `${prefix}none.`;
1005
+ const entries = connectors.map((connector) => {
1006
+ const shortcut = sanitizeIdentifier(connector.id);
1007
+ return shortcut === connector.id
1008
+ ? connector.id
1009
+ : `${connector.id} (shortcut ${shortcut})`;
1010
+ });
1011
+ const shown = [];
1012
+ for (let index = 0; index < entries.length; index++) {
1013
+ const entry = entries[index];
1014
+ if (entry === undefined)
1015
+ break;
1016
+ const candidate = [...shown, entry].join(", ");
1017
+ const omitted = entries.length - index - 1;
1018
+ const suffix = omitted > 0 ? `; +${omitted} more.` : ".";
1019
+ const serialized = prefix + candidate + suffix;
1020
+ if (boundedEchoText(serialized, CONNECTOR_INVENTORY_MAX_BYTES) !== serialized) {
1021
+ break;
1022
+ }
1023
+ shown.push(entry);
1024
+ }
1025
+ const omitted = entries.length - shown.length;
1026
+ if (omitted === 0)
1027
+ return `${prefix}${shown.join(", ")}.`;
1028
+ return `${prefix}${shown.join(", ")}${shown.length > 0 ? "; " : ""}+${omitted} more.`;
1029
+ }
1030
+ 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.
1031
+
1032
+ ${connectorInventory(connectors)}
1000
1033
 
1001
- Write an async arrow function. Portable programs use no ambient capabilities. Use only:
1002
- - Connector globals call sanitized <connectorId>.<toolName>(args): non-identifier characters become "_"; prefix a leading digit; suffix a reserved word.
1003
- - connecta.call(address, args) and connecta.batch(calls) use canonical addresses. Every batch entry is { address, ok: true, data } or { address, ok: false, error, errorDetails: { code, retryable } }; destructure it.
1004
- - top-level search_tools returns { connectors: [{ id, tools }], total, offset, limit, hasMore }; connecta.search returns { tools, total, offset, limit, hasMore }; connecta.describe returns { tools }.
1005
- - connecta.search(args) loads catalogs and must be followed by selection and calls in this program; set connector to the obvious id to load one, otherwise it loads all. For distinct operations, make separate short searches here. Check address, description, requiredInputKeys, truncation, safety, and outputs; never take the first lexical or merely input-compatible match. Select by fit; do not require it to be the only match. Missing outputKeys means inspect outputSchema, not discard the candidate. Every required key needs task or prior-result data; do not prefer zero required keys. Put every requiredInputKey in call args. For dependencies, match an earlier outputKey to the later requiredInputKey. [] means no required keys, not permission to invent args. Describe only a truncated/insufficient compact shape. Reducers use declared outputKeys, never guessed items/results roots. Describe one address or an addresses array. Use safety: "readOnly" to avoid advertising calls this sandbox cannot execute.${connectorGuides ? " guideRequired: true = stop. Describe clears only schema_truncated; otherwise return its exact guide, fetch with top-level skills, then write the informed call." : ""}
1034
+ Write one plain-JavaScript async arrow function. Use only:
1035
+ - <connectorId>.<toolName>(args) for a sanitized shortcut, or connecta.call(address, args) for a canonical address.
1036
+ - connecta.search(args), connecta.describe(args), and connecta.batch(calls) for discovery and independent read-only calls.
1006
1037
  - 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.
1007
- - connecta.ui(html, options?) one success-only view. One arg is display-only; reads use { reads: { name: { address, fixedArgs?, viewArgs? } } } and markup calls connecta.read(name, args). Admission and one budget, not two, apply; a second, over-budget, or invalid call throws catchably; the model reads the return value, not the view; return the initial summary from its variables.
1038
+ - connecta.ui(html, options?) for one success-only view; return the same initial summary the HTML renders.
1008
1039
  - console.log(...) — captured.
1009
1040
 
1010
- QuickJS blocks imports and has no fetch, process, timers, crypto, or WebSocket. Dynamic Workers require only { loader }; bindings/modules/globalOutbound violate the contract. Then env maps are empty; node:fs/http/https are absent; outbound fetch/WebSocket/node:net/tls are denied; DNS is 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 runtime-only capabilities; QuickJS fails.
1011
-
1012
- Dependent example (only when the second call requires a value returned by the first): async () => { const { tools } = await connecta.search({ query: "pipeline run job logs", safety: "readOnly", includeSchemas: "compact" }); const pick = (suffix) => { const match = tools.find((t) => t.address.endsWith(suffix)); if (!match) throw new Error("no tool for " + suffix); return match.address; }; const run = await connecta.call(pick(".get_run"), { runId: 42 }); const logs = await connecta.call(pick(".get_job_logs"), { jobId: run.failedJobId }); return [run, logs]; }
1013
-
1014
- A caught Connecta failure keeps its message and exposes code, retryable, and details; batch uses the same fields. Branch on fields, never prose. Never retry retryable: false, or rate_limited immediately; portable code cannot wait. Return JSON; reduce large results before they truncate.
1015
-
1016
- Plain JS. Compact schemas are TypeScript-like: write the property names they display; never guess positions or aliases.`;
1041
+ 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.`;
1017
1042
  /** Register the execute_code meta-tool. Only called when an executor is configured. */
1018
1043
  export function registerExecuteTool(server, registry, ctx) {
1019
1044
  // Resolved once so the description and the collector cannot disagree about
@@ -1022,6 +1047,7 @@ export function registerExecuteTool(server, registry, ctx) {
1022
1047
  maxBytes: resolveEmitBudget(ctx.maxEmittedBytes, EXECUTE_MAX_EMITTED_BYTES),
1023
1048
  maxBlocks: resolveEmitBudget(ctx.maxEmittedBlocks, EXECUTE_MAX_EMITTED_BLOCKS),
1024
1049
  };
1050
+ const connectors = registry.listConnectors();
1025
1051
  const handler = createExecuteTool(registry, ctx.baseUrl, ctx.executor, ctx.logger, ctx.activity, {
1026
1052
  ...(ctx.discoveryConcurrency !== undefined
1027
1053
  ? { discoveryConcurrency: ctx.discoveryConcurrency }
@@ -1034,11 +1060,11 @@ export function registerExecuteTool(server, registry, ctx) {
1034
1060
  ...(ctx.defer !== undefined ? { defer: ctx.defer } : {}),
1035
1061
  });
1036
1062
  server.registerTool("execute_code", {
1037
- description: executeDescription(emitBudgets, hasConnectorGuides(registry.listConnectors())),
1063
+ description: executeDescription(emitBudgets, hasConnectorGuides(connectors), connectors),
1038
1064
  inputSchema: z.object({
1039
1065
  code: z
1040
1066
  .string()
1041
- .describe("One complete JavaScript async arrow function. Consume search/describe results and finish the task inside it; returning catalog data for a later call spends a round trip and buys nothing. So does aborting on a missing tool match or result key — re-search, describe, or read the result's actual keys here instead."),
1067
+ .describe("One complete JavaScript async arrow function that discovers, calls, and returns the reduced answer."),
1042
1068
  diagnostics: z
1043
1069
  .boolean()
1044
1070
  .optional()