@zackbart/connecta 0.16.0 → 0.16.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/AGENTS.md CHANGED
@@ -83,11 +83,18 @@ Two boundaries CI enforces that are not obvious from reading a file:
83
83
  and fails otherwise. Need a Node API? It goes behind an explicit Node-only
84
84
  subpath (`/node` or `/quickjs`), never the root.
85
85
  - **The published surface.** Platform-specific storage adapters live in
86
- `examples/`, not the package. `@clerk/backend` and `quickjs-emscripten` are
87
- optional peers behind the `./auth/clerk` and `./quickjs` subpaths and must
88
- never become dependencies or install with core. Enforced by
89
- `test/package-surface.test.ts` and `scripts/check-package.mjs`. Anything
90
- heavyweight or platform-bound gets a subpath and an optional peer.
86
+ `examples/worker/`, never in `src/` and never in the `exports` map. That
87
+ example does ship in the tarball, Cloudflare KV and D1 adapters included
88
+ it is the Workers starting template a consumer copies, and copying it is the
89
+ point — but every `exports` target resolves into `dist/`, so those adapters
90
+ are readable reference source and not an importable subpath. What is
91
+ forbidden is a platform-bound adapter becoming importable from the package,
92
+ not a file appearing in the artifact. `@clerk/backend` and
93
+ `quickjs-emscripten` are optional peers behind the `./auth/clerk` and
94
+ `./quickjs` subpaths and must never become dependencies or install with
95
+ core. Enforced by `test/package-surface.test.ts` and
96
+ `scripts/check-package.mjs`. Anything heavyweight or platform-bound gets a
97
+ subpath and an optional peer.
91
98
 
92
99
  ## Where new tests go
93
100
 
package/CHANGELOG.md CHANGED
@@ -2,6 +2,193 @@
2
2
 
3
3
  All notable changes to this package are documented here.
4
4
 
5
+ ## 0.16.1 — 2026-08-13
6
+
7
+ This is the cleanup that follows 0.16.0 out the door: the packaging housekeeping
8
+ the pre-release smoke gauntlet turned up, one provider tool Cloudflare
9
+ deprecated out from under us, a discovery answer that told a plain lie, and the
10
+ upgrade runbook an existing deployment never had. Two things break, both on
11
+ Cloudflare and both named here rather than left to the section below:
12
+ `list_zone_settings` is gone from the `cloudflare()` named surface, and
13
+ Cloudflare's 404 arrives as `not_found` instead of `connector_call_failed`.
14
+ Nothing else does — no wire shape changes, no construction contract moves, no
15
+ other code reclassified, and the per-setting operations `list_zone_settings`
16
+ sat beside are the supported ones and are untouched. A deployment that writes
17
+ no `api()` connectors, branches on no error code, and never asked an agent for
18
+ a whole zone's settings in one call upgrades without reading further.
19
+
20
+ Three things are additions rather than repairs, and they are the reason this
21
+ release is worth reading rather than just installing: a new
22
+ `ConnectorCallErrorCode` member, `not_found`, with a rule for when a connector
23
+ may mint it; a `"./package.json"` entry in the `exports` map, so the installed
24
+ manifest resolves; and `@cloudflare/codemode` declared as an optional peer at
25
+ `^0.4.4 || ^0.5.0`. Strict semver would read those three as a minor, and would
26
+ read the two Cloudflare changes above as more than that. This ships as a patch
27
+ deliberately: every addition is opt-in at the point a deployment chooses to
28
+ read it, and the tool removal and the 404 reclassification ride along in the
29
+ same patch on purpose — both are scoped to one provider, both have a stated
30
+ replacement, and both carry a version boundary in
31
+ [`documentation/upgrading.md`](./documentation/upgrading.md). Holding them for
32
+ a minor would mean shipping a release that keeps calling an endpoint its
33
+ provider deprecated. The one install-time consequence is spelled out next.
34
+
35
+ One thing to check before upgrading a Worker: `@cloudflare/codemode` is now a
36
+ declared peer, so if your `package.json` holds it at a version outside
37
+ `^0.4.4 || ^0.5.0` — a `0.3.x`, or a `0.4` below `0.4.4` — npm stops the
38
+ upgrade with an `ERESOLVE` conflict rather than installing. Move it into the
39
+ range this release is tested against, or pass `--legacy-peer-deps` if you have
40
+ a reason to run outside it. A version already inside the range, and a range
41
+ loose enough for npm to pick one that is, both resolve exactly as before.
42
+
43
+ Alongside it, the upgrade path an existing deployment takes gets written down.
44
+ `connecta init` was the golden path for a new deployment and the whole story
45
+ for an old one, which is a gap with a shape: `init` refuses to merge into an
46
+ existing path — the guard that keeps an initializer from eating a connector
47
+ set — so an agent pointed at a deployment two releases behind had to
48
+ reconstruct the procedure from release prose written for the maintainer. Both
49
+ interesting failures there were silent too. It overwrites the configuration the
50
+ deployment exists for, or it "fixes" a construction throw by weakening a
51
+ fail-closed default and ships something quieter and wrong.
52
+
53
+ ### Added
54
+
55
+ - **`not_found`, for a downstream that answered and had nothing to give.** A
56
+ hand-written connector meeting a 404 had exactly one honest code,
57
+ `connector_call_failed`, which also means "the call blew up" — so a program
58
+ inside `execute_code` could not tell a clean absence from a broken connector,
59
+ and a loop over ids had to abort where it should have skipped one. The new
60
+ code earns its place the way every code has to: it changes what the caller
61
+ does next. You do not wait, you do not go to `authorize_connector`, you do
62
+ not repair the arguments — you re-address. It is non-retryable, carries no
63
+ recovery envelope, derives no activity friction class, and is exported from
64
+ the root entry as part of `ConnectorCallErrorCode`.
65
+
66
+ The qualifier is the interesting half, and it is now written down in
67
+ [H11](./documentation/provider-conventions.md#h11--errors-are-mapped-to-what-the-caller-does-next):
68
+ map a status to `not_found` only where the provider tells absence apart from
69
+ a permission gap. Cloudflare does — a token that may not touch a resource is
70
+ refused with 401 or 403 — so its 404 is now `not_found` instead of
71
+ `connector_call_failed`. Notion does not: `object_not_found` means both "it
72
+ is gone" and "it was never shared with this integration", so it deliberately
73
+ stays generic with a message that says so. The hosted-MCP proxy path mints
74
+ the code never, because `P1` forbids re-shaping downstream framing and
75
+ provider prose is never parsed to invent a classification (#373).
76
+
77
+ - **An upgrade runbook for existing deployments.**
78
+ [`documentation/upgrading.md`](./documentation/upgrading.md) is written for
79
+ the agent sitting inside a generated deployment it did not create: read the
80
+ exact pin and the template generation it implies, regenerate that generation
81
+ with `npx @zackbart/connecta@<pin> init` to get a real merge base, three-way
82
+ reconcile the scaffolding against the current template while `src/index.ts`
83
+ stays the deployment's own, cross the version boundaries that break
84
+ construction, and finish where `init` finishes — typecheck, start,
85
+ `connecta doctor`, then a program that exercises the deployment's *own*
86
+ connectors, which doctor deliberately knows nothing about. The migration
87
+ notes are per boundary and derived from this file: the 0.16.0 `api()`
88
+ construction contract (with the one safe answer for an unannotated tool
89
+ written down — `readOnlyHint: false`, which is the routing it already had),
90
+ the `linear()`, `mixpanel()`, and Cloudflare provider changes, redirect
91
+ refusal and the response ceilings, and the fail-closed shipped defaults; then
92
+ 0.14's annotation-precedence change, 0.13's rewritten guide summaries, the
93
+ 0.11.0 executor requirement, 0.7.0's `verifyState` requirement and
94
+ core-owned routes for the pre-template deployments that still have to cross
95
+ them, and every removed option that throws with its migration. It closes with five refusals, because each is somebody's plausible
96
+ shortcut: no re-init over the top, no weakening a fail-closed default to get
97
+ green, no pinning back, no vendored internals, no second project shape.
98
+ Reachable from the README, from `operations.md`, and — absolutely, because
99
+ that reader has no copy of this repository — from the template's `AGENTS.md`
100
+ (#380).
101
+ - **A suite that keeps the guide honest.** `test/upgrade-guide.test.ts` pins
102
+ every claim its reader cannot check: the generated file inventory against
103
+ `templates/node/`, the seven tool names against the CLI's own list, each
104
+ named version boundary against a release that shipped, each removed option
105
+ against the release section that names its issue, the bump target against
106
+ this package's version, and the three places the guide is linked from. A template that gains a file now fails `npm run check` rather than
107
+ leaving an agent to guess which of the two is wrong (#380).
108
+ - **`@cloudflare/codemode` is a declared optional peer.** Every Workers
109
+ deployment installs the executor behind `execute_code` by hand, and until now
110
+ the only version range anywhere was a devDependency no consumer can read — a
111
+ fresh install resolved a minor ahead of what this repository tests, silently.
112
+ The manifest now publishes `^0.4.4 || ^0.5.0` for it, optional like
113
+ `@clerk/backend` and `quickjs-emscripten`, so a supported version installs in
114
+ silence and an unsupported one stops the install with something to act on
115
+ instead of becoming skew a Worker discovers in production. It still installs
116
+ with nothing: a default `npm install @zackbart/connecta` pulls no executor,
117
+ and the package smoke proves that, both halves of the range behavior, and
118
+ that the version this repository develops against stays inside the range it
119
+ publishes (#376).
120
+
121
+ ### Changed
122
+
123
+ - **Cloudflare's 404 is `not_found`.** A deployment branching on
124
+ `connector_call_failed` to detect an unknown zone or account id should read
125
+ `not_found` instead; retryability, the message, and its pointer to
126
+ `list_zones` / `list_accounts` are unchanged (#373).
127
+ - `cloudflare()` no longer names a bulk zone-settings read.
128
+ `GET /zones/{zoneId}/settings` and its `PATCH` sibling are published as
129
+ `deprecated: true`, Cloudflare offers no bulk replacement, and the tool that
130
+ wrapped the read projected nothing — it took a zone id and grew the payload
131
+ by wrapping an unpaginated array in a page object. Read one setting with
132
+ `get_zone_setting` and write one with `update_zone_setting`, both on the
133
+ supported `/zones/{zoneId}/settings/{settingId}` operations. An operator who
134
+ still wants the whole set can name the deprecated path explicitly through
135
+ `cloudflare_api_get`. The named surface is 47 tools plus the three escape
136
+ hatches ([#361](https://github.com/zackbart/connecta/issues/361)).
137
+ - The Cloudflare touched-endpoint manifest drops the deprecated row with the
138
+ tool, so `npm run drift:check -- --specs` is quiet about zone settings
139
+ because nothing calls the endpoint, not because a maintainer signed off on
140
+ calling it anyway.
141
+
142
+ ### Fixed
143
+
144
+ - **A search for a connector's own name stops claiming the deployment has no
145
+ such capability.** A connector's `id` — the address prefix an agent already
146
+ holds — and its `title` are displayed, never indexed, so `search_tools({
147
+ query: "inventory" })` against a connector called `inventory` matched no tool
148
+ and was answered with "No matching capability is configured in this
149
+ deployment", which was plainly false. Connector identity stays out of the
150
+ lexical index, because putting it in would move ranking for every query that
151
+ already matches tools; instead an unscoped miss whose terms name configured
152
+ connectors says so, names up to three of them by ID, and sends the caller to
153
+ a scoped browse. A term that matches nothing in the deployment still gets the
154
+ original sentence, unchanged. One `queryAnalysis.guidance` string differs; no
155
+ ranking, result, or field changed (#372).
156
+ - **Every relative link in the shipped Markdown resolves for the reader who
157
+ installed the package.** Ten of them pointed at `eval/`, `test/`, `scripts/`,
158
+ and the README hero — repository paths the tarball has never carried and, per
159
+ #346, should not start carrying. The link gate could not see any of them: it
160
+ read only `documentation/` targets, so the whole class was invisible and grew
161
+ with every trim. The policy is now stated once in the operations guide and
162
+ enforced over *every* relative link in packed Markdown: it either resolves
163
+ inside the tarball or it is cited as an absolute
164
+ `https://github.com/zackbart/connecta/blob/main/...` URL, which an outside
165
+ reader can follow and which `check:docs` resolves back to the checkout, so a
166
+ citation still fails when the file it names moves. The ten links were
167
+ rewritten that way, the README hero now loads from
168
+ `raw.githubusercontent.com` and still renders on npmjs.com, and the
169
+ repository reader loses no citation (#378).
170
+ - **`@zackbart/connecta/package.json` resolves.** The `exports` map listed
171
+ every code subpath and nothing else, so a bundler plugin, framework build
172
+ step, or version probe reaching for the installed manifest — a thing the
173
+ ecosystem broadly expects to work — got `ERR_PACKAGE_PATH_NOT_EXPORTED`
174
+ instead of the file. The manifest is now exported. It is a data file, so this
175
+ widens the published surface by exactly zero code paths: the root entry's
176
+ Workers purity boundary and the optional-peer subpaths are untouched. The
177
+ package-surface gate now asserts the whole subpath set, manifest included, so
178
+ neither this entry nor an unwanted one can arrive unnoticed (#374).
179
+ - **The published-surface rule says what it actually forbids.** `AGENTS.md`
180
+ claimed platform-specific storage adapters live in `examples/`, "not the
181
+ package", while the tarball has always carried `examples/worker` — Cloudflare
182
+ KV and D1 adapters included — because that example is the Workers starting
183
+ template a consumer copies. The invariant was never in danger: nothing under
184
+ `examples/` appears in the `exports` map, so those adapters are reference
185
+ source and not an importable subpath. The wording now draws the line where
186
+ the gates draw it — a platform-bound adapter must not reach `src/` or the
187
+ `exports` map — and says why the example ships, in `AGENTS.md`, the
188
+ operations guide, and the `scripts/check-package.mjs` comment. A new
189
+ assertion in `test/package-surface.test.ts` holds the instruction file and
190
+ the exports map to the same story (#377).
191
+
5
192
  ## 0.16.0 — 2026-08-12
6
193
 
7
194
  This is the agent-efficiency refocus. One release, sixteen merges, and a single
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # connecta
2
2
 
3
- ![A monochrome clay Connecta hub joining many tools](./assets/connecta-clay-hero.png)
3
+ ![A monochrome clay Connecta hub joining many tools](https://raw.githubusercontent.com/zackbart/connecta/main/assets/connecta-clay-hero.png)
4
4
 
5
5
  One place for AI agents to connect to the tools you choose.
6
6
 
@@ -98,6 +98,11 @@ activity ship as commented configuration, each one a variable and an
98
98
  uncommented block away. The generated `README.md` walks through all four, and
99
99
  the [Worker example](./examples/worker/) does the same for KV and D1.
100
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
+
101
106
  The template refuses to merge into an existing directory, so initialization
102
107
  cannot overwrite another project. Its generated programs have no filesystem,
103
108
  environment, arbitrary network, imports, or timers; only explicitly read-only
@@ -1,4 +1,4 @@
1
- import { compactDiscoverySchema, compactSchema, lexicalCorpusStatistics, lexicalQueryTerms, lexicalSearchQuery, rankTools, schemaObjectKeys, summarizeDiscoveryDescription, summarizeDescription, } from "./catalog.js";
1
+ import { compactDiscoverySchema, compactSchema, lexicalCorpusStatistics, lexicalQueryTerms, lexicalSearchQuery, matchesLexicalTerm, rankTools, schemaObjectKeys, summarizeDiscoveryDescription, summarizeDescription, } from "./catalog.js";
2
2
  import { mapSettledWithConcurrency, resolveDiscoveryConcurrency, } from "./concurrency.js";
3
3
  import { boundedEchoText, classifyCallError, framingError, } from "./errors.js";
4
4
  import { connectorGuide, connectorGuideRequired, connectorGuideSummary, connectorSkillName, } from "./skills.js";
@@ -10,6 +10,12 @@ export const MAX_DESCRIBE_ADDRESSES = 100;
10
10
  export const MAX_DISCOVERY_RESULT_BYTES = 256_000;
11
11
  const MAX_QUERY_TERMS = 8;
12
12
  const MAX_QUERY_TERM_LENGTH = 64;
13
+ /**
14
+ * Connector IDs a no-match search will name back. Three is enough to point at
15
+ * the connector the query already named without turning a miss into a listing
16
+ * of the deployment.
17
+ */
18
+ const MAX_IDENTITY_CONNECTORS = 3;
13
19
  const encoder = new TextEncoder();
14
20
  /** Clip one echoed query term without splitting a non-BMP code point. */
15
21
  function boundedQueryTerm(term) {
@@ -561,6 +567,36 @@ export class CatalogService {
561
567
  required: connectorGuideRequired(scopedConnector),
562
568
  }
563
569
  : undefined;
570
+ // A connector's own id — the address prefix the caller already has — and
571
+ // the title it is displayed under are the most natural first query terms,
572
+ // and neither is a document in the lexical index. Indexing them would move
573
+ // ranking for every query that already matches tools, so instead a search
574
+ // that matched nothing asks the same lexical question of connector
575
+ // identity and corrects its own sentence: the deployment plainly has this
576
+ // capability, and one scoped browse away are its tools. Unscoped only —
577
+ // guidance for an explicit scope already names that connector rather than
578
+ // claiming the deployment has nothing.
579
+ const identityConnectorIds = matches.length === 0 && !isBrowse && !unsearchableQuery && !scopedConnector
580
+ ? connectors
581
+ // The full query, not `analyzedTerms`: that cap exists to bound
582
+ // the serialized term fields, and ranking already reads every
583
+ // term. A ninth term is a real search term, and a search that
584
+ // ranked against it must not deny the connector it names.
585
+ .filter((connector) => queryTerms.some((term) => matchesLexicalTerm(connector.id, term) ||
586
+ (connector.title !== undefined &&
587
+ matchesLexicalTerm(connector.title, term))))
588
+ .map((connector) => connector.id)
589
+ : [];
590
+ const namedIdentityConnectors = identityConnectorIds
591
+ .slice(0, MAX_IDENTITY_CONNECTORS)
592
+ .map((id) => `"${id}"`)
593
+ .join(", ");
594
+ const unnamedIdentityConnectors = identityConnectorIds.length - MAX_IDENTITY_CONNECTORS;
595
+ const identityGuidance = identityConnectorIds.length === 0
596
+ ? undefined
597
+ : `No matching ${safetyLabel}capability was found${unavailableCatalogs === 0 ? "" : " in the catalogs that answered"}, but the query names configured connector${identityConnectorIds.length === 1 ? "" : "s"} ${namedIdentityConnectors}${unnamedIdentityConnectors > 0
598
+ ? ` and ${unnamedIdentityConnectors} more`
599
+ : ""}. Scope by connector and browse with an empty query to list the tools there.${filterRecovery}`;
564
600
  // A scope that resolved to nothing is the same silence one step earlier in
565
601
  // the lookup: no connector resolved, so no catalog was even attempted, so
566
602
  // no catalog failed and the unavailable path below never fires. Echo only
@@ -604,9 +640,10 @@ export class CatalogService {
604
640
  : scopedGuide?.required
605
641
  ? `No matching ${safetyLabel}capability was found on connector "${scopedConnector.id}". Fetch queryAnalysis.guide before calling, then refine terms or browse with an empty query.${filterRecovery}`
606
642
  : `No matching ${safetyLabel}capability was found on connector "${scopedConnector.id}". Refine terms or browse it with an empty query.${filterRecovery}`
607
- : unavailableCatalogs === 0
608
- ? `No matching ${safetyLabel}capability is configured in this deployment. Refine terms, scope by connector, or browse with an empty query.${filterRecovery}`
609
- : `No matching ${safetyLabel}capability was found in the catalogs that answered; ${unavailableCatalogs} connector catalog${unavailableCatalogs === 1 ? " was" : "s were"} unavailable. Refine terms, scope by connector, or browse with an empty query.${filterRecovery}`))
643
+ : (identityGuidance ??
644
+ (unavailableCatalogs === 0
645
+ ? `No matching ${safetyLabel}capability is configured in this deployment. Refine terms, scope by connector, or browse with an empty query.${filterRecovery}`
646
+ : `No matching ${safetyLabel}capability was found in the catalogs that answered; ${unavailableCatalogs} connector catalog${unavailableCatalogs === 1 ? " was" : "s were"} unavailable. Refine terms, scope by connector, or browse with an empty query.${filterRecovery}`))))
610
647
  : matchMode === "partial"
611
648
  ? scopedConnector
612
649
  ? `No single tool on connector "${scopedConnector.id}" matched every term. Split distinct intents into separate searches.`
package/dist/catalog.d.ts CHANGED
@@ -18,6 +18,17 @@ export interface RankedTool {
18
18
  exactName: boolean;
19
19
  matchedTermCount: number;
20
20
  }
21
+ /**
22
+ * Whether one query term matches a whole token of arbitrary text, under the
23
+ * same inflection rules the tool index uses.
24
+ *
25
+ * Connector identity — an `id` or a `title` — is deliberately not a document in
26
+ * that index: making it one would move ranking for every query that already
27
+ * matches tools. This lets a caller ask the index's question of a string that
28
+ * never became a document, which is what the no-match analysis needs to tell
29
+ * "nothing like this exists here" from "that word is a connector".
30
+ */
31
+ export declare function matchesLexicalTerm(text: string, term: string): boolean;
21
32
  export interface LexicalCorpusStatistics {
22
33
  documentCount: number;
23
34
  documentFrequency: ReadonlyMap<string, number>;
package/dist/catalog.js CHANGED
@@ -166,6 +166,26 @@ function matchingTokenCandidates(term) {
166
166
  }
167
167
  return candidates;
168
168
  }
169
+ /**
170
+ * Whether one query term matches a whole token of arbitrary text, under the
171
+ * same inflection rules the tool index uses.
172
+ *
173
+ * Connector identity — an `id` or a `title` — is deliberately not a document in
174
+ * that index: making it one would move ranking for every query that already
175
+ * matches tools. This lets a caller ask the index's question of a string that
176
+ * never became a document, which is what the no-match analysis needs to tell
177
+ * "nothing like this exists here" from "that word is a connector".
178
+ */
179
+ export function matchesLexicalTerm(text, term) {
180
+ const tokens = new Set(lexicalTokens(text));
181
+ if (tokens.size === 0)
182
+ return false;
183
+ for (const candidate of matchingTokenCandidates(term)) {
184
+ if (tokens.has(candidate))
185
+ return true;
186
+ }
187
+ return false;
188
+ }
169
189
  /**
170
190
  * Compute query-specific document frequencies across every available catalog.
171
191
  * The caller does this once per search and shares the result with each
package/dist/errors.d.ts CHANGED
@@ -1,5 +1,33 @@
1
- /** Machine-readable classification of a failed connector tool call. */
2
- export type ConnectorCallErrorCode = "timeout" | "auth_required" | "rate_limited" | "unavailable" | "invalid_args" | "input_required_unsupported" | "connector_call_failed";
1
+ /**
2
+ * Machine-readable classification of a failed connector tool call.
3
+ *
4
+ * A code earns its place by changing what the caller does next, never by
5
+ * naming a cause — the rule provider conventions call H11.
6
+ */
7
+ export type ConnectorCallErrorCode = "timeout" | "auth_required" | "rate_limited" | "unavailable" | "invalid_args"
8
+ /**
9
+ * The downstream answered, and the thing addressed is not there.
10
+ *
11
+ * Its own code because the next move is none of the others': not a retry,
12
+ * not `authorize_connector`, not a reshaped argument object, but
13
+ * re-addressing — look the identifier up again, or accept the absence and
14
+ * carry on. Inside `execute_code` a program reads that difference off a
15
+ * `connecta.batch` entry's `errorDetails.code` — continue past this one,
16
+ * abort on `connector_call_failed` — or lets the failure escape uncaught so
17
+ * the model sees the typed envelope. Never off a caught error: the guest
18
+ * bridge reduces a rejected host call to `new Error(message)` and drops
19
+ * every own property, and message prose cannot be classified.
20
+ *
21
+ * Use it only where the provider distinguishes absence from a permission
22
+ * gap. A status that means both "it is not there" and "you cannot see it" —
23
+ * Notion's `object_not_found` is the worked example — stays
24
+ * `connector_call_failed` with a message that states the ambiguity, because
25
+ * inventing certainty here is how an agent concludes a page was deleted when
26
+ * it was simply never shared. Addresses connecta itself cannot resolve are
27
+ * already framed as `unknown_address` or `unknown_tool` and never reach a
28
+ * connector, so this code is always about a resource the downstream owns.
29
+ */
30
+ | "not_found" | "input_required_unsupported" | "connector_call_failed";
3
31
  /** One bounded, payload-free explanation of an input-schema mismatch. */
4
32
  export interface ArgumentValidationIssue {
5
33
  /** JSON Pointer into the submitted arguments; "/" means the root value. */
package/dist/errors.js CHANGED
@@ -108,6 +108,7 @@ const RETRYABLE_BY_CODE = {
108
108
  unavailable: true,
109
109
  auth_required: false,
110
110
  invalid_args: false,
111
+ not_found: false,
111
112
  input_required_unsupported: false,
112
113
  connector_call_failed: false,
113
114
  };
@@ -235,8 +235,12 @@ function failureFor(status, headers, errors) {
235
235
  if (status === 400 || status === 409 || status === 422) {
236
236
  return new ConnectorCallError("invalid_args", `Cloudflare rejected the request (HTTP ${status}). ${detail}`);
237
237
  }
238
+ // Cloudflare's 404 is an honest absence: a token that may not touch a
239
+ // resource is refused with 401 or 403 above, not hidden behind a miss, so
240
+ // this is the unambiguous case `not_found` exists for — re-address, do not
241
+ // retry and do not re-authorize.
238
242
  if (status === 404) {
239
- return new ConnectorCallError("connector_call_failed", `Cloudflare found no such resource (HTTP 404). ${detail} Confirm the zone or account id with list_zones or list_accounts.`);
243
+ return new ConnectorCallError("not_found", `Cloudflare found no such resource (HTTP 404). ${detail} Confirm the zone or account id with list_zones or list_accounts.`);
240
244
  }
241
245
  if (status >= 500) {
242
246
  return new ConnectorCallError("unavailable", `Cloudflare is unavailable (HTTP ${status}). ${detail}`);
@@ -1401,27 +1405,11 @@ function buildTools(scope, authentication) {
1401
1405
  return args["raw"] === true ? result : projectZone(result);
1402
1406
  },
1403
1407
  },
1404
- {
1405
- name: "list_zone_settings",
1406
- description: "List the effective settings for a zone, including each setting's current value and whether the plan allows editing it.",
1407
- annotations: readOnly,
1408
- inputSchema: {
1409
- type: "object",
1410
- properties: {
1411
- zoneId: scopeProperty("zoneId", scope.zoneId),
1412
- },
1413
- required: scopeRequired("zoneId", scope.zoneId),
1414
- additionalProperties: false,
1415
- },
1416
- outputSchema: listOutputSchema("settings", OPEN_OBJECT_OUTPUT_SCHEMA),
1417
- handler: async (args, ctx) => {
1418
- const { result, resultInfo } = await callCloudflare(send, {
1419
- method: "GET",
1420
- path: `/zones/${encodePathSegment(zoneArg(args))}/settings`,
1421
- }, ctx);
1422
- return { settings: asArray(result), page: pageInfo(resultInfo) };
1423
- },
1424
- },
1408
+ // There is deliberately no bulk zone-settings read here. Cloudflare
1409
+ // publishes `GET /zones/{zone_id}/settings` as deprecated (#361), the
1410
+ // per-setting operations below are not, and the tool that wrapped the bulk
1411
+ // read projected nothing. See the Cloudflare guide's "What the named
1412
+ // surface deliberately leaves out".
1425
1413
  {
1426
1414
  name: "get_zone_setting",
1427
1415
  description: "Get one zone setting by its Cloudflare setting id, such as ssl, always_use_https, min_tls_version, brotli, or development_mode.",
@@ -1433,7 +1421,7 @@ function buildTools(scope, authentication) {
1433
1421
  settingId: {
1434
1422
  type: "string",
1435
1423
  minLength: 1,
1436
- description: "Cloudflare zone setting id from list_zone_settings.",
1424
+ description: "Cloudflare zone setting id, such as ssl, brotli, http3, or min_tls_version.",
1437
1425
  },
1438
1426
  },
1439
1427
  required: [...scopeRequired("zoneId", scope.zoneId), "settingId"],
@@ -1459,7 +1447,7 @@ function buildTools(scope, authentication) {
1459
1447
  settingId: {
1460
1448
  type: "string",
1461
1449
  minLength: 1,
1462
- description: "Cloudflare zone setting id from list_zone_settings.",
1450
+ description: "Cloudflare zone setting id, such as ssl, brotli, http3, or min_tls_version.",
1463
1451
  },
1464
1452
  value: {
1465
1453
  type: ["string", "number", "boolean", "array"],
@@ -3215,7 +3203,7 @@ Account purpose: ${purpose}
3215
3203
  - Prefer a named tool: its schema is complete, projected, and enough to call it without provider documentation. For an operation without a named tool, use \`cloudflare_api_get\` for GET, \`cloudflare_api_mutate\` for JSON POST/PUT/PATCH/DELETE, or \`cloudflare_api_upload\` for raw and multipart content. Raw tools take a path below \`/client/v4\`; their argument schemas are complete, but endpoint-specific query, header, and body fields come from Cloudflare's API reference. Use \`headers\` for endpoint-specific controls such as \`cf-r2-jurisdiction\`, \`Range\`, \`If-None-Match\`, and Cloudflare product metadata. \`Authorization\`, \`Cookie\`, \`Host\`, \`Content-Length\`, \`Content-Type\`, and \`Transfer-Encoding\` are connector-owned and refused: authentication, host, content type, and request framing are not the caller's to set.
3216
3204
  - The raw tools cover the wider control plane without weakening routing: GET is explicitly read-only; every mutation and upload is destructive and must cross the host's approval boundary. The configured Cloudflare credential remains the hard provider-side permission boundary. Absolute URLs, traversal, and query strings embedded in \`path\` are refused locally.
3217
3205
  - Useful raw paths include \`/accounts/{accountId}/images/v1\` (Images), \`/accounts/{accountId}/stream\` (Stream), \`/zones/{zoneId}/email/routing/rules\` (Email Routing), \`/accounts/{accountId}/d1/database\` (D1), and \`/accounts/{accountId}/queues\` (Queues). On GET, use \`responseType: "text"\` or \`"base64"\` for non-JSON content. Direct-upload endpoints can issue upload URLs; \`cloudflare_api_upload\` can also send explicit text, base64 bytes, or multipart fields/files.
3218
- - Two R2 areas are deliberately unnamed. Read a bucket's CORS policy with \`get_r2_cors\`, then change it with \`cloudflare_api_mutate\` — \`PUT\` or \`DELETE /accounts/{accountId}/r2/buckets/{bucketName}/cors\`, rule fields per Cloudflare's reference — and read account storage totals with \`cloudflare_api_get\` at \`/accounts/{accountId}/r2/metrics\`.
3206
+ - Three areas are deliberately unnamed. Read a bucket's CORS policy with \`get_r2_cors\`, then change it with \`cloudflare_api_mutate\` — \`PUT\` or \`DELETE /accounts/{accountId}/r2/buckets/{bucketName}/cors\`, rule fields per Cloudflare's reference — and read account storage totals with \`cloudflare_api_get\` at \`/accounts/{accountId}/r2/metrics\`. Zone settings are read one at a time with \`get_zone_setting\`: Cloudflare deprecated the bulk \`/zones/{zoneId}/settings\` read and published no replacement for it, so reach for the whole set through \`cloudflare_api_get\` only when one setting genuinely will not do.
3219
3207
  - Lists paginate with \`page\` and \`perPage\` and return a \`page\` object; request the next page only when \`page.hasMore\` is true. \`list_zone_rulesets\`, \`list_r2_buckets\`, \`list_r2_objects\`, and \`list_kv_keys\` page by cursor instead: pass \`cursor\`, continue while \`nextCursor\` is present, and expect no \`page\` object. Their schemas say so too. \`list_worker_scripts\` is unpaginated.
3220
3208
  - Results are projected to the fields that identify and describe a resource. Pass \`raw: true\` on a read when you genuinely need a field the projection drops.
3221
3209
  - ${authenticationLine}
package/dist/version.d.ts CHANGED
@@ -4,4 +4,4 @@
4
4
  * a bump that forgets this file fails the build rather than shipping a stale
5
5
  * version to `/health` and to downstream MCP handshakes.
6
6
  */
7
- export declare const CONNECTA_VERSION = "0.16.0";
7
+ export declare const CONNECTA_VERSION = "0.16.1";
package/dist/version.js CHANGED
@@ -4,4 +4,4 @@
4
4
  * a bump that forgets this file fails the build rather than shipping a stale
5
5
  * version to `/health` and to downstream MCP handshakes.
6
6
  */
7
- export const CONNECTA_VERSION = "0.16.0";
7
+ export const CONNECTA_VERSION = "0.16.1";
@@ -2,7 +2,7 @@
2
2
 
3
3
  Import `cloudflare()` independently from
4
4
  `@zackbart/connecta/providers/cloudflare`. It is a deliberate, hand-written
5
- surface over Cloudflare's v4 REST API. Fifty-two tools combine ergonomic,
5
+ surface over Cloudflare's v4 REST API. Fifty-one tools combine ergonomic,
6
6
  fully described operations for common work with three guarded escape hatches
7
7
  for the rest of Cloudflare's fast-moving control plane. Reads, JSON mutations,
8
8
  and raw/multipart uploads remain separate so safety routing does not depend on
@@ -140,7 +140,7 @@ projections:
140
140
 
141
141
  | Area | Reads | Writes |
142
142
  | --- | --- | --- |
143
- | Zones | discovery, details, settings, rulesets | update a setting |
143
+ | Zones | discovery, details, one setting at a time, rulesets | update a setting |
144
144
  | DNS/cache | list and get records | create, update, delete, targeted/full purge |
145
145
  | Workers | scripts, settings, deployments | delete a script |
146
146
  | KV | namespaces, keys, bulk values | create/rename/delete namespace, bulk write/delete |
@@ -158,9 +158,10 @@ asserts those properties rather than leaving them as a claim.
158
158
  A named tool is a permanent line item in every deployment's catalog, so the
159
159
  surface was measured against the escape hatches rather than assumed to beat
160
160
  them ([#350](https://github.com/zackbart/connecta/issues/350), evidence in
161
- [`eval/current-version/results/issue-350-evidence.md`](../eval/current-version/results/issue-350-evidence.md)).
162
- Two named tools lost that comparison, and a third followed one of them off the
163
- surface to keep a policy pair on one route:
161
+ [`eval/current-version/results/issue-350-evidence.md`](https://github.com/zackbart/connecta/blob/main/eval/current-version/results/issue-350-evidence.md)).
162
+ Two named tools lost that comparison, a third followed one of them off the
163
+ surface to keep a policy pair on one route, and a fourth left because
164
+ Cloudflare deprecated the only endpoint it could call:
164
165
 
165
166
  - **R2 CORS writes.** `set_r2_cors` declared its rule list as free-form objects
166
167
  — the untyped body this connection refuses everywhere else — so its schema
@@ -181,8 +182,31 @@ surface to keep a policy pair on one route:
181
182
  path, and returned the response untouched — `cloudflare_api_get` at
182
183
  `/accounts/{accountId}/r2/metrics` does the same thing without a permanent
183
184
  catalog line.
184
-
185
- The surviving 48 named tools all refuse malformed arguments locally, which is
185
+ - **The bulk zone-settings read.** `list_zone_settings` called
186
+ `GET /zones/{zoneId}/settings`, which Cloudflare's published document now
187
+ marks `deprecated: true` along with its `PATCH` sibling
188
+ ([#361](https://github.com/zackbart/connecta/issues/361)). There is no bulk
189
+ replacement to repoint it at: the supported operations are the per-setting
190
+ `GET` and `PATCH /zones/{zoneId}/settings/{settingId}` that `get_zone_setting`
191
+ and `update_zone_setting` already call. Keeping the tool would have meant
192
+ maintaining a name over a contract Cloudflare has announced it intends to
193
+ stop honouring — and #350 had already measured this one as the sharpest
194
+ passthrough on the surface: it took a zone id, projected nothing, and *grew*
195
+ the payload 22.7% by wrapping Cloudflare's settings array in a page object an
196
+ unpaginated endpoint never filled. Read one setting with `get_zone_setting`.
197
+ Where the whole set is genuinely wanted — an audit, a config diff — an
198
+ operator can still ask for it explicitly through `cloudflare_api_get` at
199
+ `/zones/{zoneId}/settings`, which is the honest place for a call whose
200
+ endpoint is on its way out: named by the caller, at the caller's risk, rather
201
+ than promised by connecta's catalog.
202
+
203
+ Because `scripts/drift/cloudflare-endpoints.json` records the endpoints this
204
+ connection *calls*, dropping the tool drops the row — so
205
+ `npm run drift:check -- --specs` is quiet about zone settings by construction
206
+ rather than by a recorded exception. A path reached only through a hatch is
207
+ named by the caller, so it was never a touched endpoint.
208
+
209
+ The surviving 47 named tools all refuse malformed arguments locally, which is
186
210
  the one thing no escape hatch can do: a hatch's path is an opaque string, so it
187
211
  can only check that a path is a path.
188
212
 
@@ -324,9 +348,17 @@ the failure message so the provider's own code number survives to the agent.
324
348
  | 401 or 403 | `auth_required`, not retryable | Stops and reports which permission is missing |
325
349
  | 400 with a credential-shaped code (1001, 6003, 6111, 9103, 9106, 9107) | `auth_required`, not retryable | Stops; the header or key is malformed, not the arguments |
326
350
  | 400, 409, 422 | `invalid_args`, not retryable | Repairs the arguments |
327
- | 404 | `connector_call_failed`, not retryable | Re-runs discovery for the id |
351
+ | 404 | `not_found`, not retryable | Re-runs discovery for the id |
328
352
  | 5xx or a transport error | `unavailable`, retryable | Retries |
329
353
 
354
+ The 404 row is the unambiguous half of the `not_found` rule
355
+ ([H11](./provider-conventions.md#h11--errors-are-mapped-to-what-the-caller-does-next)).
356
+ Cloudflare refuses a token that may not touch a resource with 401 or 403, so a
357
+ 404 is an absence rather than a permission gap wearing a miss, and an agent can
358
+ act on it: confirm the zone or account id with `list_zones` or `list_accounts`
359
+ and address the right one. Notion's 404, which proves neither, stays
360
+ `connector_call_failed`.
361
+
330
362
  The six credential-shaped codes deserve a caveat: Cloudflare publishes no
331
363
  official table mapping error codes to causes, so that set is assembled from
332
364
  community reports and probing, not from documentation. The same goes for the
@@ -13,7 +13,7 @@
13
13
  > constraint below was dropped.** The owner decided the default directly on
14
14
  > 2026-07-30 and [#224](https://github.com/zackbart/connecta/issues/224)
15
15
  > shipped it; the ethos records the eval-as-gate as `removed` and
16
- > [`eval/code-first-gate`](../eval/code-first-gate/README.md) continues as
16
+ > [`eval/code-first-gate`](https://github.com/zackbart/connecta/blob/main/eval/code-first-gate/README.md) continues as
17
17
  > measurement that nothing waits on.
18
18
 
19
19
  ## Executive summary
@@ -227,7 +227,7 @@ changing the default surface. Run at least 20 independent samples per task and
227
227
  model, with prompt variation.
228
228
 
229
229
  That suite was built under
230
- [`eval/code-first-gate`](../eval/code-first-gate/README.md). Its recorded results
230
+ [`eval/code-first-gate`](https://github.com/zackbart/connecta/blob/main/eval/code-first-gate/README.md). Its recorded results
231
231
  remain as measurement history; the runnable comparison was retired when #273
232
232
  removed the alternate deployment shapes.
233
233
 
@@ -329,8 +329,7 @@ not in the always-loaded usage skill, which `test/meta-tools.test.ts` caps at
329
329
  2,500 bytes — a budget the guide already spends nearly all of, so new text there
330
330
  displaces old rather than adding to what every request pays for.
331
331
 
332
- **E2.** The taxonomy. `retryable` is what connecta reports; `Y3` says what a
333
- program may do about it.
332
+ **E2.** The taxonomy: `retryable` is what connecta reports, `Y3` what a program may do.
334
333
 
335
334
  | Code | Raised when | `retryable` |
336
335
  | --- | --- | --- |
@@ -340,6 +339,7 @@ program may do about it.
340
339
  | `destructive_tool_requires_approval` | the tool is not explicitly read-only | false |
341
340
  | `auth_required` | the credential is missing, expired, or rejected | false |
342
341
  | `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, classified off `errorDetails` per `E1` and never off a caught error, 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
343
  | `input_required_unsupported` | a downstream asked for mid-call input | false |
344
344
  | `rate_limited` | the downstream reported a rate limit | true |
345
345
  | `unavailable` | the downstream is down or unreachable | true |