@zackbart/connecta 0.15.1 → 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 +40 -20
- package/CHANGELOG.md +653 -0
- package/README.md +25 -5
- package/bin/connecta.mjs +44 -3
- package/dist/access-tokens.d.ts +0 -1
- package/dist/access-tokens.js +0 -1
- package/dist/activity.d.ts +50 -2
- package/dist/activity.js +34 -1
- package/dist/apps-shell.d.ts +0 -1
- package/dist/apps-shell.js +0 -1
- package/dist/auth/bearer.d.ts +0 -1
- package/dist/auth/bearer.js +0 -1
- package/dist/auth/clerk.d.ts +0 -1
- package/dist/auth/clerk.js +45 -9
- package/dist/auth/downstream-oauth.d.ts +0 -1
- package/dist/auth/downstream-oauth.js +0 -1
- package/dist/call-admission.d.ts +0 -1
- package/dist/call-admission.js +0 -1
- package/dist/catalog-drift.d.ts +99 -0
- package/dist/catalog-drift.js +251 -0
- package/dist/catalog-fingerprint.d.ts +0 -1
- package/dist/catalog-fingerprint.js +0 -1
- package/dist/catalog-limits.d.ts +0 -1
- package/dist/catalog-limits.js +0 -1
- package/dist/catalog-service.d.ts +0 -1
- package/dist/catalog-service.js +41 -5
- package/dist/catalog.d.ts +11 -1
- package/dist/catalog.js +20 -1
- package/dist/concurrency.d.ts +0 -1
- package/dist/concurrency.js +0 -1
- package/dist/connector-scope.d.ts +0 -1
- package/dist/connector-scope.js +0 -1
- package/dist/connectors/api.d.ts +29 -21
- package/dist/connectors/api.js +45 -19
- package/dist/connectors/guarded-fetch.d.ts +106 -0
- package/dist/connectors/guarded-fetch.js +289 -0
- package/dist/connectors/remote-mcp.d.ts +0 -1
- package/dist/connectors/remote-mcp.js +0 -1
- package/dist/credentials.d.ts +0 -1
- package/dist/credentials.js +0 -1
- package/dist/errors.d.ts +30 -3
- package/dist/errors.js +1 -1
- package/dist/execute.d.ts +0 -1
- package/dist/execute.js +0 -1
- package/dist/executor-admission.d.ts +11 -1
- package/dist/executor-admission.js +28 -1
- package/dist/executor-result.d.ts +0 -1
- package/dist/executor-result.js +0 -1
- package/dist/executors/quickjs-child.d.ts +0 -1
- package/dist/executors/quickjs-child.js +0 -1
- package/dist/executors/quickjs-protocol.d.ts +0 -1
- package/dist/executors/quickjs-protocol.js +0 -1
- package/dist/executors/quickjs-runtime.d.ts +0 -1
- package/dist/executors/quickjs-runtime.js +0 -1
- package/dist/executors/quickjs.d.ts +0 -1
- package/dist/executors/quickjs.js +2 -1
- package/dist/favicon.d.ts +0 -1
- package/dist/favicon.js +0 -1
- package/dist/index.d.ts +2 -3
- package/dist/index.js +24 -7
- package/dist/invocation.d.ts +0 -1
- package/dist/invocation.js +0 -1
- package/dist/json-schema.d.ts +0 -1
- package/dist/json-schema.js +0 -1
- package/dist/mcp-result.d.ts +0 -1
- package/dist/mcp-result.js +0 -1
- package/dist/meta-tools.d.ts +0 -1
- package/dist/meta-tools.js +0 -1
- package/dist/node.d.ts +0 -1
- package/dist/node.js +0 -1
- package/dist/operator-ui/generated.d.ts +2 -3
- package/dist/operator-ui/generated.js +3 -4
- package/dist/operator-ui/model.d.ts +10 -1
- package/dist/operator-ui/model.js +0 -1
- package/dist/operator-ui/view.d.ts +167 -0
- package/dist/operator-ui/view.js +270 -0
- package/dist/providers/cloudflare.d.ts +0 -1
- package/dist/providers/cloudflare.js +261 -299
- package/dist/providers/linear.d.ts +26 -5
- package/dist/providers/linear.js +32 -61
- package/dist/providers/mixpanel.d.ts +35 -4
- package/dist/providers/mixpanel.js +53 -79
- package/dist/providers/notion.d.ts +0 -1
- package/dist/providers/notion.js +87 -42
- package/dist/providers/stripe.d.ts +13 -1
- package/dist/providers/stripe.js +31 -60
- package/dist/registry.d.ts +34 -2
- package/dist/registry.js +85 -5
- package/dist/retired-toolkits.d.ts +0 -1
- package/dist/retired-toolkits.js +0 -1
- package/dist/routes/access-tokens.d.ts +0 -1
- package/dist/routes/access-tokens.js +0 -1
- package/dist/routes/activity.d.ts +0 -1
- package/dist/routes/activity.js +0 -1
- package/dist/routes/credentials.d.ts +0 -1
- package/dist/routes/credentials.js +0 -1
- package/dist/routes/mcp.d.ts +0 -1
- package/dist/routes/mcp.js +0 -1
- package/dist/routes/oauth.d.ts +0 -1
- package/dist/routes/oauth.js +0 -1
- package/dist/routes/shared.d.ts +2 -1
- package/dist/routes/shared.js +0 -1
- package/dist/routes/ui.d.ts +0 -1
- package/dist/routes/ui.js +0 -1
- package/dist/server.d.ts +0 -1
- package/dist/server.js +11 -1
- package/dist/skills.d.ts +0 -1
- package/dist/skills.js +0 -1
- package/dist/storage/file.d.ts +0 -1
- package/dist/storage/file.js +0 -1
- package/dist/storage/memory.d.ts +0 -1
- package/dist/storage/memory.js +0 -1
- package/dist/timeout.d.ts +0 -1
- package/dist/timeout.js +0 -1
- package/dist/tool-safety.d.ts +0 -1
- package/dist/tool-safety.js +0 -1
- package/dist/types.d.ts +45 -1
- package/dist/types.js +0 -1
- package/dist/ui.d.ts +0 -1
- package/dist/ui.js +13 -137
- package/dist/validate.d.ts +13 -15
- package/dist/validate.js +24 -11
- package/dist/version.d.ts +1 -2
- package/dist/version.js +1 -2
- package/documentation/architecture.md +186 -5
- package/documentation/auth.md +11 -0
- package/documentation/call-admission.md +142 -6
- package/documentation/cloudflare.md +92 -10
- package/documentation/code-first-exploration.md +2 -2
- package/documentation/code-mode.md +14 -14
- package/documentation/connector-guides.md +172 -5
- package/documentation/connectors.md +89 -0
- package/documentation/linear.md +20 -2
- package/documentation/mcp-ui-design.md +2 -2
- package/documentation/meta-tools.md +7 -1
- package/documentation/mixpanel.md +42 -9
- package/documentation/notion.md +23 -2
- package/documentation/operations.md +325 -5
- package/documentation/operator-ui.md +120 -5
- package/documentation/provider-audit.md +160 -0
- package/documentation/provider-conventions.md +667 -0
- package/documentation/request-admission.md +167 -5
- package/documentation/rich-output-design.md +1 -2
- package/documentation/stripe.md +15 -0
- package/documentation/upgrading.md +454 -0
- package/ethos.md +14 -9
- package/examples/worker/README.md +102 -12
- package/examples/worker/src/index.ts +46 -1
- package/examples/worker/wrangler.jsonc +14 -1
- package/package.json +10 -6
- package/templates/node/.dockerignore +10 -0
- package/templates/node/.env.example +40 -3
- package/templates/node/AGENTS.md +16 -1
- package/templates/node/Dockerfile +49 -0
- package/templates/node/README.md +117 -2
- package/templates/node/docker-compose.yml +39 -0
- package/templates/node/package.json +1 -1
- package/templates/node/src/file-activity.ts +182 -0
- package/templates/node/src/index.ts +80 -4
- package/assets/connecta-clay-hero.png +0 -0
- package/dist/access-tokens.d.ts.map +0 -1
- package/dist/access-tokens.js.map +0 -1
- package/dist/activity.d.ts.map +0 -1
- package/dist/activity.js.map +0 -1
- package/dist/apps-shell.d.ts.map +0 -1
- package/dist/apps-shell.js.map +0 -1
- package/dist/auth/bearer.d.ts.map +0 -1
- package/dist/auth/bearer.js.map +0 -1
- package/dist/auth/clerk.d.ts.map +0 -1
- package/dist/auth/clerk.js.map +0 -1
- package/dist/auth/downstream-oauth.d.ts.map +0 -1
- package/dist/auth/downstream-oauth.js.map +0 -1
- package/dist/call-admission.d.ts.map +0 -1
- package/dist/call-admission.js.map +0 -1
- package/dist/catalog-fingerprint.d.ts.map +0 -1
- package/dist/catalog-fingerprint.js.map +0 -1
- package/dist/catalog-limits.d.ts.map +0 -1
- package/dist/catalog-limits.js.map +0 -1
- package/dist/catalog-service.d.ts.map +0 -1
- package/dist/catalog-service.js.map +0 -1
- package/dist/catalog.d.ts.map +0 -1
- package/dist/catalog.js.map +0 -1
- package/dist/concurrency.d.ts.map +0 -1
- package/dist/concurrency.js.map +0 -1
- package/dist/connector-scope.d.ts.map +0 -1
- package/dist/connector-scope.js.map +0 -1
- package/dist/connectors/api.d.ts.map +0 -1
- package/dist/connectors/api.js.map +0 -1
- package/dist/connectors/remote-mcp.d.ts.map +0 -1
- package/dist/connectors/remote-mcp.js.map +0 -1
- package/dist/credentials.d.ts.map +0 -1
- package/dist/credentials.js.map +0 -1
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/execute.d.ts.map +0 -1
- package/dist/execute.js.map +0 -1
- package/dist/executor-admission.d.ts.map +0 -1
- package/dist/executor-admission.js.map +0 -1
- package/dist/executor-result.d.ts.map +0 -1
- package/dist/executor-result.js.map +0 -1
- package/dist/executors/quickjs-child.d.ts.map +0 -1
- package/dist/executors/quickjs-child.js.map +0 -1
- package/dist/executors/quickjs-protocol.d.ts.map +0 -1
- package/dist/executors/quickjs-protocol.js.map +0 -1
- package/dist/executors/quickjs-runtime.d.ts.map +0 -1
- package/dist/executors/quickjs-runtime.js.map +0 -1
- package/dist/executors/quickjs.d.ts.map +0 -1
- package/dist/executors/quickjs.js.map +0 -1
- package/dist/favicon.d.ts.map +0 -1
- package/dist/favicon.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/invocation.d.ts.map +0 -1
- package/dist/invocation.js.map +0 -1
- package/dist/json-schema.d.ts.map +0 -1
- package/dist/json-schema.js.map +0 -1
- package/dist/mcp-result.d.ts.map +0 -1
- package/dist/mcp-result.js.map +0 -1
- package/dist/meta-tools.d.ts.map +0 -1
- package/dist/meta-tools.js.map +0 -1
- package/dist/node.d.ts.map +0 -1
- package/dist/node.js.map +0 -1
- package/dist/operator-ui/generated.d.ts.map +0 -1
- package/dist/operator-ui/generated.js.map +0 -1
- package/dist/operator-ui/model.d.ts.map +0 -1
- package/dist/operator-ui/model.js.map +0 -1
- package/dist/providers/cloudflare.d.ts.map +0 -1
- package/dist/providers/cloudflare.js.map +0 -1
- package/dist/providers/linear.d.ts.map +0 -1
- package/dist/providers/linear.js.map +0 -1
- package/dist/providers/mixpanel.d.ts.map +0 -1
- package/dist/providers/mixpanel.js.map +0 -1
- package/dist/providers/notion.d.ts.map +0 -1
- package/dist/providers/notion.js.map +0 -1
- package/dist/providers/stripe.d.ts.map +0 -1
- package/dist/providers/stripe.js.map +0 -1
- package/dist/registry.d.ts.map +0 -1
- package/dist/registry.js.map +0 -1
- package/dist/retired-toolkits.d.ts.map +0 -1
- package/dist/retired-toolkits.js.map +0 -1
- package/dist/routes/access-tokens.d.ts.map +0 -1
- package/dist/routes/access-tokens.js.map +0 -1
- package/dist/routes/activity.d.ts.map +0 -1
- package/dist/routes/activity.js.map +0 -1
- package/dist/routes/credentials.d.ts.map +0 -1
- package/dist/routes/credentials.js.map +0 -1
- package/dist/routes/mcp.d.ts.map +0 -1
- package/dist/routes/mcp.js.map +0 -1
- package/dist/routes/oauth.d.ts.map +0 -1
- package/dist/routes/oauth.js.map +0 -1
- package/dist/routes/shared.d.ts.map +0 -1
- package/dist/routes/shared.js.map +0 -1
- package/dist/routes/ui.d.ts.map +0 -1
- package/dist/routes/ui.js.map +0 -1
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js.map +0 -1
- package/dist/skills.d.ts.map +0 -1
- package/dist/skills.js.map +0 -1
- package/dist/storage/file.d.ts.map +0 -1
- package/dist/storage/file.js.map +0 -1
- package/dist/storage/memory.d.ts.map +0 -1
- package/dist/storage/memory.js.map +0 -1
- package/dist/timeout.d.ts.map +0 -1
- package/dist/timeout.js.map +0 -1
- package/dist/tool-safety.d.ts.map +0 -1
- package/dist/tool-safety.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/ui.d.ts.map +0 -1
- package/dist/ui.js.map +0 -1
- package/dist/validate.d.ts.map +0 -1
- package/dist/validate.js.map +0 -1
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
- package/examples/node/README.md +0 -52
- package/examples/node/src/index.ts +0 -71
- package/src/access-tokens.ts +0 -289
- package/src/activity.ts +0 -234
- package/src/apps-shell.ts +0 -370
- package/src/auth/bearer.ts +0 -61
- package/src/auth/clerk.ts +0 -553
- package/src/auth/downstream-oauth.ts +0 -575
- package/src/call-admission.ts +0 -521
- package/src/catalog-fingerprint.ts +0 -52
- package/src/catalog-limits.ts +0 -11
- package/src/catalog-service.ts +0 -1153
- package/src/catalog.ts +0 -790
- package/src/concurrency.ts +0 -39
- package/src/connector-scope.ts +0 -66
- package/src/connectors/api.ts +0 -159
- package/src/connectors/remote-mcp.ts +0 -1090
- package/src/credentials.ts +0 -428
- package/src/errors.ts +0 -364
- package/src/execute.ts +0 -1406
- package/src/executor-admission.ts +0 -328
- package/src/executor-result.ts +0 -107
- package/src/executors/quickjs-child.ts +0 -172
- package/src/executors/quickjs-protocol.ts +0 -99
- package/src/executors/quickjs-runtime.ts +0 -470
- package/src/executors/quickjs.ts +0 -776
- package/src/favicon.ts +0 -53
- package/src/index.ts +0 -666
- package/src/invocation.ts +0 -655
- package/src/json-schema.ts +0 -11
- package/src/mcp-result.ts +0 -43
- package/src/meta-tools.ts +0 -1607
- package/src/node.ts +0 -206
- package/src/operator-ui/browser.css +0 -423
- package/src/operator-ui/browser.ts +0 -1281
- package/src/operator-ui/generated.ts +0 -4
- package/src/operator-ui/model.ts +0 -111
- package/src/providers/cloudflare.ts +0 -3999
- package/src/providers/linear.ts +0 -302
- package/src/providers/mixpanel.ts +0 -229
- package/src/providers/notion.ts +0 -1879
- package/src/providers/stripe.ts +0 -307
- package/src/registry.ts +0 -1085
- package/src/retired-toolkits.ts +0 -14
- package/src/routes/access-tokens.ts +0 -115
- package/src/routes/activity.ts +0 -215
- package/src/routes/credentials.ts +0 -254
- package/src/routes/mcp.ts +0 -429
- package/src/routes/oauth.ts +0 -322
- package/src/routes/shared.ts +0 -247
- package/src/routes/ui.ts +0 -152
- package/src/server.ts +0 -195
- package/src/skills.ts +0 -271
- package/src/storage/file.ts +0 -123
- package/src/storage/memory.ts +0 -44
- package/src/timeout.ts +0 -84
- package/src/tool-safety.ts +0 -15
- package/src/types.ts +0 -523
- package/src/ui.ts +0 -744
- package/src/validate.ts +0 -387
- package/src/version.ts +0 -7
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
# Provider conventions
|
|
2
|
+
|
|
3
|
+
The five maintained prebuilt connections grew one at a time, and until now
|
|
4
|
+
"excellent provider" meant whatever the last author thought. This document
|
|
5
|
+
writes the judgment down so it can be argued with, audited, and reused.
|
|
6
|
+
|
|
7
|
+
There are two genuinely different provider shapes, and one convention set
|
|
8
|
+
cannot honestly cover both:
|
|
9
|
+
|
|
10
|
+
- **Hand-written HTTP providers** — `api()` surfaces where Connecta owns every
|
|
11
|
+
tool name, schema, projection, and error. Today: Cloudflare, Notion.
|
|
12
|
+
- **Hosted-MCP proxies** — `remoteMcp()` wrappers around a server somebody else
|
|
13
|
+
operates, where the names, schemas, results, and error prose arrive as they
|
|
14
|
+
are. Today: Linear, Stripe, Mixpanel.
|
|
15
|
+
|
|
16
|
+
The governing principle for every convention below is the same: **keep the
|
|
17
|
+
model that interacts with connecta as efficient as possible.** A convention
|
|
18
|
+
earns its place by reducing what an agent spends discovering, selecting,
|
|
19
|
+
calling, and reading. It does not earn its place by being tidy.
|
|
20
|
+
|
|
21
|
+
Nothing here overrides [ethos.md](../ethos.md). In particular, prebuilt
|
|
22
|
+
connections remain ordinary `Connector` instances with no extra privileges,
|
|
23
|
+
their annotations fill in downstream silence rather than replacing it, and no
|
|
24
|
+
tool is ever generated from a schema document.
|
|
25
|
+
|
|
26
|
+
## How to read a convention
|
|
27
|
+
|
|
28
|
+
Every convention is one rule, one reason, and one cost. The cost is drawn from
|
|
29
|
+
a fixed vocabulary of four, because those are the four things an agent actually
|
|
30
|
+
pays:
|
|
31
|
+
|
|
32
|
+
| Cost | What it means |
|
|
33
|
+
| --- | --- |
|
|
34
|
+
| **discovery tokens** | bytes the agent reads before it can call anything |
|
|
35
|
+
| **wrong-tool selection** | it picked the wrong tool, connector, or mode |
|
|
36
|
+
| **argument retries** | the call was made and rejected, so it must be made again |
|
|
37
|
+
| **result size** | bytes the agent reads back, and the round trips paging them |
|
|
38
|
+
|
|
39
|
+
A convention that cannot name one of those four is a preference, not a
|
|
40
|
+
convention, and does not belong in this document.
|
|
41
|
+
|
|
42
|
+
## What discovery actually shows
|
|
43
|
+
|
|
44
|
+
Several conventions are budgets, and the budgets are not arbitrary — they are
|
|
45
|
+
the points at which Connecta's own surface starts dropping characters on the
|
|
46
|
+
floor. From `src/catalog.ts` and `src/catalog-service.ts`:
|
|
47
|
+
|
|
48
|
+
- **A tool description is cut to 160 characters in `search_tools`** and to 240
|
|
49
|
+
in the describe path, both with a trailing `…`, unless the caller passes
|
|
50
|
+
`fullDescriptions: true`. Prose past those points reaches an agent only when
|
|
51
|
+
it pays for the expansion.
|
|
52
|
+
- **A compact schema renders into at most 1,024 UTF-8 bytes**, and any single
|
|
53
|
+
enum node into at most 256. Past either cap the renderer keeps what fits and
|
|
54
|
+
degrades the rest — a prefix of the enum plus `unknown`, a required-first
|
|
55
|
+
object with `unknown` types, or `unknown /* truncated */` — and flags the
|
|
56
|
+
match, which costs a describe round trip to recover.
|
|
57
|
+
- **`inputKeys`, `requiredInputKeys`, and `outputKeys` come only from bounded
|
|
58
|
+
plain-object schemas.** A top-level `anyOf` has no keys to list, so a caller
|
|
59
|
+
learns nothing about the arguments without expanding the schema.
|
|
60
|
+
- **A guide summary is capped at 120 characters**, defaulting to the guide's
|
|
61
|
+
first meaningful body line.
|
|
62
|
+
- **Search returns a connector's `id`, `title`, `guide`, and `guideSummary` —
|
|
63
|
+
never its `description`.** The description reaches an agent only as the
|
|
64
|
+
fallback summary for a guide with no usable body line. Routing facts belong
|
|
65
|
+
in the title and the guide's first line; a routing fact that lives only in
|
|
66
|
+
the connector description has been written into a field the model does not
|
|
67
|
+
read. Neither the `id` nor the `title` is a lexical document, so a term drawn
|
|
68
|
+
from one of them is not a search hit — it is a no-match whose guidance names
|
|
69
|
+
the connector and points at a scoped browse.
|
|
70
|
+
|
|
71
|
+
Two construction-time checks enforce the floor beneath all of this. The hard
|
|
72
|
+
one is `api()` itself: since
|
|
73
|
+
[#340](https://github.com/zackbart/connecta/issues/340) a hand-written tool
|
|
74
|
+
throws unless it carries a non-empty description, an explicit boolean
|
|
75
|
+
`annotations.readOnlyHint`, and an `inputSchema` — where it declares one — the
|
|
76
|
+
validator can compile. The soft one is `Registry.checkConventions()`, which
|
|
77
|
+
warns about a connector with no description, and about a static tool from any
|
|
78
|
+
other source with no description or no `inputSchema`.
|
|
79
|
+
|
|
80
|
+
## Hand-written HTTP providers
|
|
81
|
+
|
|
82
|
+
Connecta owns the whole surface here, which means every miss is ours. These
|
|
83
|
+
apply to `api()`-based prebuilt connections (Cloudflare, Notion) and are the
|
|
84
|
+
bar any future one is written to.
|
|
85
|
+
|
|
86
|
+
None of them asks an author to re-derive transport safety. URL confinement,
|
|
87
|
+
query and body construction, `ctx.signal`, redirect refusal, credential
|
|
88
|
+
shadowing, bounded response reads, and network-failure normalization are the
|
|
89
|
+
[guarded fetch transport](./connectors.md#the-guarded-fetch-transport)'s job
|
|
90
|
+
([#341](https://github.com/zackbart/connecta/issues/341)). What the conventions
|
|
91
|
+
below still demand — H11's error mapping and H12's credential test above all —
|
|
92
|
+
is the provider knowledge no shared helper can hold.
|
|
93
|
+
|
|
94
|
+
### H1 — Identity is deployment-owned; the provider supplies everything else
|
|
95
|
+
|
|
96
|
+
The constructor takes an `id`, a required `purpose`, an optional `title`, and
|
|
97
|
+
optional `instructions` appended to — never replacing — the maintained guide.
|
|
98
|
+
A blank `purpose` throws at construction.
|
|
99
|
+
|
|
100
|
+
*Why:* an agent choosing between two instances of the same provider has only
|
|
101
|
+
the title and the guide summary to choose from. *Cost:* wrong-tool selection.
|
|
102
|
+
|
|
103
|
+
### H2 — Names are `verb_object`, and the verb is the safety class
|
|
104
|
+
|
|
105
|
+
`snake_case`, a leading verb from the small set the surface already uses
|
|
106
|
+
(`list_`, `get_`, `search_`, `create_`, `update_`, `delete_`, plus a provider's
|
|
107
|
+
own vocabulary such as `purge_` or `trash_`), and a noun that matches the
|
|
108
|
+
provider's own word for the thing. A read never opens with a write verb, and a
|
|
109
|
+
write never hides behind a neutral one. Escape hatches are named
|
|
110
|
+
`<provider>_api_<class>` so they sort together and read as generic.
|
|
111
|
+
|
|
112
|
+
*Why:* the name is the only thing lexical search indexes besides the
|
|
113
|
+
description, and it is what an agent skims first. A connector's `id` and
|
|
114
|
+
`title` are displayed, not indexed — a search for one of them matches no tool,
|
|
115
|
+
and the miss is answered by guidance naming that connector rather than by a
|
|
116
|
+
ranked result. *Cost:* wrong-tool selection.
|
|
117
|
+
|
|
118
|
+
### H3 — The selection sentence fits in 160 characters; the whole description in 240
|
|
119
|
+
|
|
120
|
+
Sentence one says what the tool returns or does, in the imperative, and is
|
|
121
|
+
complete inside 160 characters (roughly 40 tokens). Everything else — the
|
|
122
|
+
constraint, the disqualifier, the handoff — fits in the remaining 80 characters
|
|
123
|
+
(240 total, roughly 60 tokens). Detail that does not fit belongs in a property
|
|
124
|
+
description or the usage guide, both of which are fetched only when needed.
|
|
125
|
+
|
|
126
|
+
*Why:* search cuts at 160 and describe at 240 unless the caller passes
|
|
127
|
+
`fullDescriptions: true`, so anything past the budget is delivered only to an
|
|
128
|
+
agent that spends a second, larger read to get it. *Cost:* discovery tokens.
|
|
129
|
+
|
|
130
|
+
### H4 — The description names the disqualifier, not the pitch
|
|
131
|
+
|
|
132
|
+
Say what the tool will not do when an agent is likely to assume it does:
|
|
133
|
+
`search` "finds pages and data sources by **title**. Never searches content."
|
|
134
|
+
One clause of honest negative space outperforms three of capability.
|
|
135
|
+
|
|
136
|
+
*Why:* the cheapest wrong call is the one never made. *Cost:*
|
|
137
|
+
wrong-tool selection.
|
|
138
|
+
|
|
139
|
+
### H5 — Input schemas are complete, closed, and bounded
|
|
140
|
+
|
|
141
|
+
Every tool carries a hand-written `inputSchema`: a plain object at the top
|
|
142
|
+
level, `additionalProperties: false`, an accurate `required` list, an `enum` on
|
|
143
|
+
every constrained field, explicit numeric bounds on every page size and count,
|
|
144
|
+
and a description on every property — nested objects and array items included,
|
|
145
|
+
because a caller composing an array element is reading that element's fields,
|
|
146
|
+
not the parent's prose. `api()` enforces the enforceability half for free since
|
|
147
|
+
[#340](https://github.com/zackbart/connecta/issues/340): a schema the validator
|
|
148
|
+
cannot compile throws at construction, and one that only reveals itself on
|
|
149
|
+
first use fails the call rather than silently admitting unvalidated input — in
|
|
150
|
+
a surface we wrote ourselves, an unenforceable schema is our bug.
|
|
151
|
+
|
|
152
|
+
*Why:* a complete schema is the difference between one call and a call, a
|
|
153
|
+
rejection, and a repair. *Cost:* argument retries.
|
|
154
|
+
|
|
155
|
+
### H6 — A local bound says whose bound it is
|
|
156
|
+
|
|
157
|
+
When a schema's bound is the provider's, the description says so; when the
|
|
158
|
+
bound is narrower than the provider's, the description says that too. A local
|
|
159
|
+
cap that an agent is told about beats a page size that fails somewhere inside
|
|
160
|
+
the provider — but only if the agent is told.
|
|
161
|
+
|
|
162
|
+
*Why:* an unexplained refusal reads as a bug and gets retried. *Cost:*
|
|
163
|
+
argument retries.
|
|
164
|
+
|
|
165
|
+
### H7 — Schemas fit the compact renderer, or selection does not depend on the part that is cut
|
|
166
|
+
|
|
167
|
+
Keep the common path's compact input and output shapes inside 1,024 bytes and
|
|
168
|
+
each enum node inside 256. Where a legitimate enum genuinely cannot fit — 21
|
|
169
|
+
DNS record types — the truncation is acceptable only if the tool's name and
|
|
170
|
+
description already carry enough for selection, so the caller expands the
|
|
171
|
+
schema to *call*, not to *choose*.
|
|
172
|
+
|
|
173
|
+
*Why:* a truncated compact shape costs a describe round trip. *Cost:* discovery
|
|
174
|
+
tokens.
|
|
175
|
+
|
|
176
|
+
### H8 — Every tool declares an `outputSchema`
|
|
177
|
+
|
|
178
|
+
Declared outputs are what produce `outputKeys` and the `fields` projection's
|
|
179
|
+
`availableFields`, and they let a program reduce a result without first
|
|
180
|
+
fetching one to look at. Connecta measured *declared* output schemas at 0 of 30
|
|
181
|
+
tools on one real deployment and 3 of 30 on another
|
|
182
|
+
([#282](https://github.com/zackbart/connecta/issues/282)) — nearly every tool an
|
|
183
|
+
agent meets is a shape it can only learn by calling. A maintained provider has
|
|
184
|
+
no excuse to join that majority.
|
|
185
|
+
|
|
186
|
+
*Why:* an agent that knows the shape projects before it reads. *Cost:* result
|
|
187
|
+
size.
|
|
188
|
+
|
|
189
|
+
### H9 — Every read projects, and says what it dropped
|
|
190
|
+
|
|
191
|
+
Reads return the provider's payload flattened and renamed, with plan,
|
|
192
|
+
permission, and presentation noise removed. Where the dropped detail can
|
|
193
|
+
matter, the tool takes `raw: true` and returns the untouched response; where
|
|
194
|
+
the provider itself truncated something, the projection surfaces that fact and
|
|
195
|
+
the id needed to fetch the rest, rather than handing back a confident partial.
|
|
196
|
+
The argument and result vocabulary is consistent within a connector, and the
|
|
197
|
+
mapping from the provider's own names is either identity or one mechanical rule
|
|
198
|
+
stated in the guide.
|
|
199
|
+
|
|
200
|
+
*Why:* projection is the largest single lever on what an agent reads, and a
|
|
201
|
+
silent truncation is worse than a large result. *Cost:* result size.
|
|
202
|
+
|
|
203
|
+
### H10 — Pagination is one convention per connector, with one field to branch on
|
|
204
|
+
|
|
205
|
+
List tools take an explicit page argument and a cursor, default to a page size
|
|
206
|
+
smaller than the provider's maximum because a first read should be cheap, and
|
|
207
|
+
return exactly one branchable signal — `hasMore` beside a cursor. Cursors are
|
|
208
|
+
opaque: passed back verbatim, never parsed or constructed. Where an endpoint
|
|
209
|
+
paginates differently from the rest of the connector, the schema and the guide
|
|
210
|
+
both say so instead of letting an agent discover it.
|
|
211
|
+
|
|
212
|
+
*Why:* the loop condition should not be a research project. *Cost:* result size.
|
|
213
|
+
|
|
214
|
+
### H11 — Errors are mapped to what the caller does next
|
|
215
|
+
|
|
216
|
+
The typed failure code is chosen by the caller's next move, not by the
|
|
217
|
+
provider's name for what happened: an authorization gap an operator must fix is
|
|
218
|
+
not `auth_required` if `authorize_connector` cannot fix it; a retryable failure
|
|
219
|
+
carries `retryAfterMs` when the provider says how long; an ambiguous provider
|
|
220
|
+
code gets a message that states the ambiguity rather than picking the
|
|
221
|
+
convenient reading. A call that can only fail is refused locally as
|
|
222
|
+
`invalid_args` before the round trip. Provider error prose is never parsed to
|
|
223
|
+
invent a classification.
|
|
224
|
+
|
|
225
|
+
**A downstream 404 is `not_found` — when the provider means it.** The code
|
|
226
|
+
exists because the next move is none of the others': you do not wait, you do
|
|
227
|
+
not send the agent to `authorize_connector`, you do not repair the argument
|
|
228
|
+
object. You re-address — look the id up again, or accept the absence and carry
|
|
229
|
+
on — and a program looping over ids inside `execute_code` can continue past
|
|
230
|
+
`not_found` where `connector_call_failed` would have to abort the run, reading
|
|
231
|
+
the code off a `connecta.batch` entry's `errorDetails` rather than off a caught
|
|
232
|
+
error, which the guest bridge has already stripped to prose. That
|
|
233
|
+
control-flow difference is the H11 test being met; it is not a label for the
|
|
234
|
+
cause.
|
|
235
|
+
|
|
236
|
+
The qualifier is the whole rule. Map a status to `not_found` only where the
|
|
237
|
+
provider distinguishes absence from a permission gap. Where it does not —
|
|
238
|
+
Notion returns `object_not_found` both for an object that is gone and for one
|
|
239
|
+
that was never shared with the integration, and will not say which — the honest
|
|
240
|
+
code stays `connector_call_failed` (or `auth_required`, where a credential
|
|
241
|
+
really is the fix) and the message states the ambiguity, exactly as the
|
|
242
|
+
paragraph above requires. Cloudflare is the other side of the pair: a token
|
|
243
|
+
that may not touch a resource is refused with 401 or 403, so its 404 is an
|
|
244
|
+
absence and maps to `not_found`. Neither connector's mapping changed shape when
|
|
245
|
+
the code arrived; one of them changed codes.
|
|
246
|
+
|
|
247
|
+
Two boundaries. `not_found` is about a resource the *downstream* owns: an
|
|
248
|
+
address connecta cannot resolve is already framed as `unknown_address` or
|
|
249
|
+
`unknown_tool` and never reaches a connector. And it never appears on the
|
|
250
|
+
hosted-MCP proxy path, because `P1` forbids re-shaping downstream framing and
|
|
251
|
+
prose is never parsed to invent a classification — a proxied server's own
|
|
252
|
+
missing-resource error arrives as that server wrote it. The two paths do not
|
|
253
|
+
diverge on the rule; they diverge on who is entitled to apply it, which is the
|
|
254
|
+
same split every other code already has.
|
|
255
|
+
|
|
256
|
+
*Why:* a misrouted error sends an agent down a recovery path that cannot
|
|
257
|
+
succeed. *Cost:* argument retries.
|
|
258
|
+
|
|
259
|
+
### H12 — One operator credential, one cheap test, no probing
|
|
260
|
+
|
|
261
|
+
The connection declares its credential slot with a labeled field per secret and
|
|
262
|
+
implements `testCredential`/`testCredentials` with the cheapest call that proves
|
|
263
|
+
the secret is live, reporting the identity or workspace it authenticated as.
|
|
264
|
+
Connecta does not check credentials behind an operator's back; the test runs
|
|
265
|
+
when a human asks, and everything else fails loudly at use.
|
|
266
|
+
|
|
267
|
+
*Why:* "which account is this?" answered once at configuration time is a
|
|
268
|
+
question no agent has to answer by calling something. *Cost:* wrong-tool
|
|
269
|
+
selection.
|
|
270
|
+
|
|
271
|
+
### H13 — The guide carries only what a schema cannot
|
|
272
|
+
|
|
273
|
+
`usageGuide` uses the structured form: `content`, an explicit `summary`, and
|
|
274
|
+
`required: true` only when correct use depends on a sequence or convention no
|
|
275
|
+
complete schema can express. Imperative bullets, decision first, exact tool and
|
|
276
|
+
argument names, constraints with their numbers. The first content line is the
|
|
277
|
+
routing fact, because it is the summary fallback. Nothing in the guide restates
|
|
278
|
+
a schema.
|
|
279
|
+
|
|
280
|
+
*Why:* the guide is fetched into a live context window, so every line that
|
|
281
|
+
repeats a schema is paid for twice. *Cost:* discovery tokens.
|
|
282
|
+
|
|
283
|
+
### H14 — A named tool must beat the escape hatch, and the escape hatch splits by safety
|
|
284
|
+
|
|
285
|
+
Guarded raw access is an accepted shape, not a required one — a small provider
|
|
286
|
+
whose surface is genuinely finite may deliberately have none, and say so. Where
|
|
287
|
+
a provider is large and fast-moving enough to need one, it is split by safety
|
|
288
|
+
class: a GET-only tool that is explicitly read-only, a JSON mutation tool that
|
|
289
|
+
is always destructive, and an upload tool that is always destructive. The split
|
|
290
|
+
is Connecta's, never an agent-supplied HTTP method. Paths are provider-relative
|
|
291
|
+
and confined; the connector owns authentication, host, content type, and
|
|
292
|
+
framing. This is not schema ingestion — a fixed handful of stable tools, never
|
|
293
|
+
one per operation — and a *named* tool earns its place only by beating the
|
|
294
|
+
hatch on schema, projection, or safety routing.
|
|
295
|
+
|
|
296
|
+
*Why:* every named tool costs catalog bytes forever, and a thin wrapper around
|
|
297
|
+
a call the hatch already makes costs them for nothing. *Cost:* discovery
|
|
298
|
+
tokens.
|
|
299
|
+
|
|
300
|
+
## Hosted-MCP proxies
|
|
301
|
+
|
|
302
|
+
Here the downstream owns the tool names, descriptions, input schemas, result
|
|
303
|
+
shapes, pagination, and error prose. Conventions that legislate those things
|
|
304
|
+
would be fiction. What Connecta owns is the endpoint, the credential, the
|
|
305
|
+
classification, the connector's own identity, the guide, and the budget — so
|
|
306
|
+
that is what these conventions cover.
|
|
307
|
+
|
|
308
|
+
### P1 — Normalize by adding, never by rewriting
|
|
309
|
+
|
|
310
|
+
A proxy may add annotations, a title, a guide, and an admission policy. It does
|
|
311
|
+
not rewrite a downstream tool's name, description, or schema, and it does not
|
|
312
|
+
re-shape a downstream result. A rewritten description drifts silently away from
|
|
313
|
+
the schema it describes, and the agent believes the description.
|
|
314
|
+
|
|
315
|
+
*Why:* the catalog must stay a true report of what the downstream will accept.
|
|
316
|
+
*Cost:* argument retries.
|
|
317
|
+
|
|
318
|
+
### P2 — Identity is deployment-owned
|
|
319
|
+
|
|
320
|
+
Identical to H1: `id`, required `purpose`, optional `title`, and `instructions`
|
|
321
|
+
appended to the maintained guide, never replacing it, and never able to change
|
|
322
|
+
the safety classification.
|
|
323
|
+
|
|
324
|
+
*Why:* two instances of the same provider are told apart only by title and
|
|
325
|
+
guide summary. *Cost:* wrong-tool selection.
|
|
326
|
+
|
|
327
|
+
### P3 — The fact that decides routing goes in the title and the guide's first line
|
|
328
|
+
|
|
329
|
+
Whichever variant an agent must not get wrong — production versus sandbox,
|
|
330
|
+
read-only versus read-write, region, account — appears in the default `title`
|
|
331
|
+
and as the first content line of the guide. It may also appear in the
|
|
332
|
+
`description`; it may never appear *only* there, because search never returns
|
|
333
|
+
the description.
|
|
334
|
+
|
|
335
|
+
*Why:* the model reads title and guide summary at browse time and nothing else.
|
|
336
|
+
*Cost:* wrong-tool selection.
|
|
337
|
+
|
|
338
|
+
### P4 — Endpoint selection is a constructor option with the safest honest default
|
|
339
|
+
|
|
340
|
+
Where the provider publishes more than one endpoint, the option selects between
|
|
341
|
+
them and the default is the safe one. Where the provider publishes one endpoint
|
|
342
|
+
and the environment rides the credential, the mode is required with no default,
|
|
343
|
+
and construction throws when a recognizable credential contradicts the declared
|
|
344
|
+
mode. Deprecated transports stay unreachable.
|
|
345
|
+
|
|
346
|
+
*Why:* a provider-enforced scope limit is a stronger guarantee than any
|
|
347
|
+
annotation Connecta applies, and a wrong-mode write is not recoverable by
|
|
348
|
+
retrying. *Cost:* wrong-tool selection.
|
|
349
|
+
|
|
350
|
+
### P5 — Classification is a reviewed allowlist that fails closed
|
|
351
|
+
|
|
352
|
+
Reads are listed by name, writes are listed by name with their destructive
|
|
353
|
+
verdict, and anything unlisted is not read-only. The lists are supersets:
|
|
354
|
+
hosted catalogs vary by plan and feature flags, so a classified name a
|
|
355
|
+
workspace never returns costs nothing while an unclassified new one fails
|
|
356
|
+
closed onto `call_destructive_tool`. The classification fills in downstream
|
|
357
|
+
silence and otherwise preserves explicit annotations, with the single
|
|
358
|
+
fail-closed exception the [ethos](../ethos.md) accepted-prebuilt row records
|
|
359
|
+
([#315](https://github.com/zackbart/connecta/issues/315)) — a release-reviewed
|
|
360
|
+
destructive verdict outranks a contradictory `readOnlyHint: true`, because that
|
|
361
|
+
release independently established that the tool mutates existing state. An
|
|
362
|
+
additive write leaves `destructiveHint` unset.
|
|
363
|
+
|
|
364
|
+
*Why:* the fail-closed read-only invariant is not negotiable, and inflated
|
|
365
|
+
destructive copy trains humans to approve without reading. *Cost:* wrong-tool
|
|
366
|
+
selection.
|
|
367
|
+
|
|
368
|
+
### P6 — The guide says the catalog is not a fixed set
|
|
369
|
+
|
|
370
|
+
The guide tells the agent to search this connector for what the workspace
|
|
371
|
+
actually exposes rather than assuming a documented tool exists, and names the
|
|
372
|
+
plan- or beta-gated areas where absence is expected.
|
|
373
|
+
|
|
374
|
+
*Why:* a hosted catalog varies per account, and an agent that assumes ours is
|
|
375
|
+
complete spends calls proving it is not. *Cost:* wrong-tool selection.
|
|
376
|
+
|
|
377
|
+
### P7 — The guide carries the reduction advice the schemas cannot
|
|
378
|
+
|
|
379
|
+
A proxy cannot project a downstream result, so the guide tells the agent to
|
|
380
|
+
page with the cursor rather than raising the page size, and to reduce inside
|
|
381
|
+
`execute_code` before returning anything. Structured form, explicit `summary`,
|
|
382
|
+
`required: true` only for a genuine cross-tool sequence or a generic wrapper.
|
|
383
|
+
|
|
384
|
+
*Why:* the only projection available is the one the program writes. *Cost:*
|
|
385
|
+
result size.
|
|
386
|
+
|
|
387
|
+
### P8 — Identity resolution comes before action
|
|
388
|
+
|
|
389
|
+
Where a downstream's write arguments take ids, the guide names the read tools
|
|
390
|
+
that produce them and says not to guess. Where the provider has a
|
|
391
|
+
human-readable identifier alongside a UUID, the guide says which is which.
|
|
392
|
+
|
|
393
|
+
*Why:* a guessed id is a call, a rejection, and a repair. *Cost:* argument
|
|
394
|
+
retries.
|
|
395
|
+
|
|
396
|
+
### P9 — Authentication defaults to OAuth, with a documented headless alternative
|
|
397
|
+
|
|
398
|
+
OAuth per connector instance, stored in connector-scoped storage, is the
|
|
399
|
+
default. The provider's own headless credential — a personal API key, a
|
|
400
|
+
restricted key, a service account — is supported through explicit `headers`
|
|
401
|
+
auth, documented as a secret rather than configuration, and paired with the
|
|
402
|
+
narrowest mode the deployment can use. `requireHttps` is set. Recovery from an
|
|
403
|
+
expired authorization is the ordinary `auth_required` → `authorize_connector`
|
|
404
|
+
route.
|
|
405
|
+
|
|
406
|
+
*Why:* one route back from an expired credential is what keeps a failed call
|
|
407
|
+
from becoming an abandoned task. *Cost:* wrong-tool selection.
|
|
408
|
+
|
|
409
|
+
### P10 — There is no credential test; the equivalent check happens at construction
|
|
410
|
+
|
|
411
|
+
A proxy declares no operator credential slot and implements neither
|
|
412
|
+
`testCredential` nor `testCredentials`. `remoteMcp()` has no `credential`
|
|
413
|
+
option, and neither shape of proxy credential is vault-managed: OAuth lives in
|
|
414
|
+
connector-scoped storage and is exercised by the authorization flow itself,
|
|
415
|
+
while a headless key arrives as deployment configuration in `headers`, so there
|
|
416
|
+
is nothing for the operator credentials page to hold or test. H12's guarantee is
|
|
417
|
+
still owed, and a proxy pays it in two other places: construction throws when a
|
|
418
|
+
recognizable credential contradicts the declared mode (P4), and a dead or
|
|
419
|
+
revoked credential fails loudly at use as `auth_required` with the
|
|
420
|
+
`authorize_connector` route attached (P9). Connecta never probes a downstream to
|
|
421
|
+
see whether a credential is still alive — that shape is `removed` in the ethos
|
|
422
|
+
([#179](https://github.com/zackbart/connecta/issues/179)). A provider that later
|
|
423
|
+
does take a vault-managed secret inherits H12 whole.
|
|
424
|
+
|
|
425
|
+
*Why:* an unasked-for liveness probe spends a call on every deployment to answer
|
|
426
|
+
a question only a misconfigured one has. *Cost:* result size.
|
|
427
|
+
|
|
428
|
+
### P11 — Connecta classifies the transport; the downstream owns the tool error
|
|
429
|
+
|
|
430
|
+
Connecta maps what it can see from outside the tool: an authorization failure to
|
|
431
|
+
`auth_required`, a session or scope teardown, a timeout, and a capability the
|
|
432
|
+
proxy will not relay (`input_required`, task-required execution) to an explicit
|
|
433
|
+
refusal that says so. A tool-level failure the downstream returns — a validation
|
|
434
|
+
complaint, a not-found, a plan restriction — is passed back as it arrived. The
|
|
435
|
+
proxy does not read downstream error prose to invent a Connecta classification,
|
|
436
|
+
and does not repackage a downstream error as `invalid_args`, because it has no
|
|
437
|
+
schema of its own to have validated against. Where a downstream reliably reports
|
|
438
|
+
a retryable condition, the guide says how to recognize it rather than the code
|
|
439
|
+
guessing.
|
|
440
|
+
|
|
441
|
+
*Why:* a transport failure and a rejected argument need different next moves,
|
|
442
|
+
and prose-sniffing routes the second one down the first one's path. *Cost:*
|
|
443
|
+
argument retries.
|
|
444
|
+
|
|
445
|
+
### P12 — Declare an admission budget only when the provider documents a number
|
|
446
|
+
|
|
447
|
+
Where the provider publishes a rate limit, transcribe it as a rolling-window
|
|
448
|
+
budget and say in the guide that it is a per-runtime approximation, not an
|
|
449
|
+
enforcement. Where the provider documents nothing, or documents a limit metered
|
|
450
|
+
per user in a way a per-runtime counter cannot approximate, declare no budget
|
|
451
|
+
and leave the number to the operator who knows the account — with a documented
|
|
452
|
+
example of how to supply one. A `maxConcurrency` beside a budget is a choice
|
|
453
|
+
Connecta made and is labeled as one.
|
|
454
|
+
|
|
455
|
+
*Why:* a hardcoded ceiling either throttles a healthy deployment or fails to
|
|
456
|
+
protect a busy one, and both look like the provider being flaky. *Cost:*
|
|
457
|
+
argument retries.
|
|
458
|
+
|
|
459
|
+
### P13 — A drifting downstream must be visible, not absorbed
|
|
460
|
+
|
|
461
|
+
The classification lists name what a release reviewed, and they are the
|
|
462
|
+
manifest the runtime drift check compares against — one structure per provider,
|
|
463
|
+
built once by `vettedCatalog()` and used both to classify and to compare, so
|
|
464
|
+
the annotation a caller gets and the verdict a check reads can never disagree.
|
|
465
|
+
When the downstream changes underneath them, the correct outcome is a loud
|
|
466
|
+
unclassified tool on the approval path and a maintained record of the drift —
|
|
467
|
+
never a quiet re-guess. The runtime half is
|
|
468
|
+
[the runtime drift policy](#the-runtime-drift-policy) below; the release-time
|
|
469
|
+
half is [the maintainer-run drift check](#the-maintainer-run-drift-check).
|
|
470
|
+
|
|
471
|
+
*Why:* an allowlist nobody can tell is stale is an allowlist that is wrong.
|
|
472
|
+
*Cost:* wrong-tool selection.
|
|
473
|
+
|
|
474
|
+
## The runtime drift policy
|
|
475
|
+
|
|
476
|
+
Detection rides a refresh; it never causes one
|
|
477
|
+
([#343](https://github.com/zackbart/connecta/issues/343)). The comparison
|
|
478
|
+
happens inside the wrapper's `listTools`, on the listing the downstream just
|
|
479
|
+
returned to serve a request the deployment already made, before the
|
|
480
|
+
classification is applied — so what it reads is the downstream's own word, not
|
|
481
|
+
connecta's fill-in. There is no scheduled job, no background request, no
|
|
482
|
+
credential probe, and no automatic issue filing. Proactive credential liveness
|
|
483
|
+
stays removed ([#179](https://github.com/zackbart/connecta/issues/179)); this
|
|
484
|
+
is the shape that does not become it.
|
|
485
|
+
|
|
486
|
+
**What a manifest holds.** Every tool name a release reviewed, the verdict it
|
|
487
|
+
reviewed it as (`read-only`, `additive`, `destructive`), and — where a release
|
|
488
|
+
actually read them — a digest of that tool's input and output schemas. Today
|
|
489
|
+
the three proxies ship names and verdicts and no digests, because no release
|
|
490
|
+
has read a live schema and written it down, and an invented digest reports a
|
|
491
|
+
change that never happened. `npm run drift:check -- --record` reads them from a
|
|
492
|
+
live catalog and prints the block a release pastes in; until a release does,
|
|
493
|
+
a manifest without digests counts no schema changes, which is the honest answer
|
|
494
|
+
rather than a silent zero.
|
|
495
|
+
|
|
496
|
+
**What it counts.** Four categories, and only counts:
|
|
497
|
+
|
|
498
|
+
| Category | What it means |
|
|
499
|
+
| --- | --- |
|
|
500
|
+
| unclassified additions | the downstream serves a tool no release classified; it already fails closed onto `call_destructive_tool` |
|
|
501
|
+
| names no longer served | a classified name is absent from this catalog |
|
|
502
|
+
| annotation conflicts | the downstream *explicitly* contradicts a vetted verdict — `readOnlyHint: false` or `destructiveHint: true` on a vetted read, `readOnlyHint: true` on a vetted write |
|
|
503
|
+
| schema changes | a recorded digest no longer matches the schemas that arrived |
|
|
504
|
+
|
|
505
|
+
Silence is never a conflict: filling it in is what the classification is for.
|
|
506
|
+
A non-zero "no longer served" count is the expected reading on a plan-gated
|
|
507
|
+
workspace, because P5's lists are deliberate supersets — it is triage input,
|
|
508
|
+
not an alarm.
|
|
509
|
+
|
|
510
|
+
**Where it surfaces.** Connector status carries the counts and the time they
|
|
511
|
+
were observed; `/health` carries the same per connector, which is where
|
|
512
|
+
`connecta doctor` reads them, and doctor reports drift without failing on it.
|
|
513
|
+
Both reads are projections — four counts and a bounded timestamp, rebuilt from
|
|
514
|
+
whatever the connector seam returned, because `/health` is unauthenticated and
|
|
515
|
+
`Connector.catalogDrift()` is third-party code.
|
|
516
|
+
One activity event per *change* in the counts — an identical report every TTL
|
|
517
|
+
is a heartbeat, not news — carrying the connector id and four integers. The
|
|
518
|
+
event type has nowhere to put a tool name, a schema, an argument, a result, or
|
|
519
|
+
downstream error prose, which is the same construction guarantee the tool-call
|
|
520
|
+
event makes. Which tool drifted is deliberately absent from the runtime: it is
|
|
521
|
+
answered by the maintainer-run check, with a live catalog in front of it.
|
|
522
|
+
|
|
523
|
+
**How far an observation reaches.** One runtime, and no further. The
|
|
524
|
+
observation lives in the isolate or process that served the refresh; unlike the
|
|
525
|
+
catalog, it is not persisted, so nothing carries it across a Workers isolate, a
|
|
526
|
+
restart, or a second Node process. Status and `/health` therefore answer for
|
|
527
|
+
the instance that took the request: on Workers a `connecta doctor` run will
|
|
528
|
+
usually land on an isolate that has served no refresh and print nothing, and
|
|
529
|
+
behind more than one process it is a coin flip. Read an empty report as *this
|
|
530
|
+
runtime has observed nothing*, never as *nothing drifted* — the durable record
|
|
531
|
+
of a finding is the activity event a sink already stored, and naming the tool
|
|
532
|
+
is still the maintainer-run check's job.
|
|
533
|
+
|
|
534
|
+
**What a finding obliges.** A contradicted vetted verdict — the downstream
|
|
535
|
+
calling a release-reviewed destructive tool `readOnlyHint: true`, or a vetted
|
|
536
|
+
read `destructiveHint: true` — blocks that provider's next release until a
|
|
537
|
+
human has re-reviewed the tool. Everything else enters ordinary issue triage.
|
|
538
|
+
No finding changes what a caller may reach: an unclassified tool fails closed
|
|
539
|
+
whether or not anybody noticed it arrived.
|
|
540
|
+
|
|
541
|
+
## The maintainer-run drift check
|
|
542
|
+
|
|
543
|
+
`npm run drift:check` is the other half
|
|
544
|
+
([#351](https://github.com/zackbart/connecta/issues/351)): a human at a laptop,
|
|
545
|
+
before a release, with local credentials and the published specifications in
|
|
546
|
+
front of them. It lives in
|
|
547
|
+
[`scripts/drift-check.mjs`](https://github.com/zackbart/connecta/blob/main/scripts/drift-check.mjs) and ships nowhere —
|
|
548
|
+
`scripts/` is outside the package, no runtime module imports it, and nothing it
|
|
549
|
+
reads becomes a runtime input.
|
|
550
|
+
|
|
551
|
+
**Hosted-MCP catalogs.** `--hosted` lists each proxy's live catalog with the
|
|
552
|
+
maintainer's own key and diffs it against the same `vettedCatalog()` manifest
|
|
553
|
+
the connector classifies from, reporting tools *by name*: added, no longer
|
|
554
|
+
served, annotation conflicts with what the downstream actually claimed, and —
|
|
555
|
+
once a manifest records schema digests — which tool's schemas moved. The names
|
|
556
|
+
live here rather than in the runtime because the runtime's counts are
|
|
557
|
+
payload-free by construction, and a name has no reader there anyway. It then
|
|
558
|
+
compares its own totals against `detectCatalogDrift()`: two readings of one
|
|
559
|
+
manifest that disagree mean one of them is lying, which is worth failing over.
|
|
560
|
+
One credential per provider comes from the environment —
|
|
561
|
+
`CONNECTA_DRIFT_LINEAR_KEY`, `CONNECTA_DRIFT_STRIPE_KEY`,
|
|
562
|
+
`CONNECTA_DRIFT_MIXPANEL_KEY` — and a missing or dead one stops the run with a
|
|
563
|
+
message naming it rather than reporting an empty catalog as mass removal.
|
|
564
|
+
|
|
565
|
+
**Touched endpoints.** A hand-written provider is written against a published
|
|
566
|
+
OpenAPI document and calls a few dozen of its operations, so
|
|
567
|
+
[`scripts/drift/`](https://github.com/zackbart/connecta/tree/main/scripts/drift) commits exactly those: method, path, the
|
|
568
|
+
specification revision a release reviewed the endpoint at, whether the
|
|
569
|
+
operation was deprecated at that revision, and a digest of that endpoint's
|
|
570
|
+
contract. `--specs` fetches each provider's published document and reports four
|
|
571
|
+
things per touched endpoint — the path is gone, the method is gone, the
|
|
572
|
+
operation's deprecation changed, or its contract changed since the recorded
|
|
573
|
+
revision. Everything else in the document is ignored, which is the point: a
|
|
574
|
+
Cloudflare release that rewrites 2,000 operations connecta never calls is not
|
|
575
|
+
news, and a revision bump that left the touched contracts alone reports
|
|
576
|
+
nothing.
|
|
577
|
+
|
|
578
|
+
Deprecation is reported as a *transition*, not a state: a deprecation a
|
|
579
|
+
maintainer has read and recorded stops being news, and an operation that comes
|
|
580
|
+
back off the deprecation list is its own finding. Without that, a single
|
|
581
|
+
reviewed deprecation would fail every release forever, and the check could
|
|
582
|
+
never reach the "no drift" state its exit code is for.
|
|
583
|
+
|
|
584
|
+
A contract digest covers the parameters, the request body, and the success
|
|
585
|
+
responses, with local `$ref`s inlined so a change inside a shared component is
|
|
586
|
+
visible, and with descriptions, examples, and `x-` extensions stripped so a
|
|
587
|
+
reworded document is not a finding. Inlining runs before a response's `content`
|
|
588
|
+
is read, because a whole response object is often a reference itself —
|
|
589
|
+
Cloudflare writes several of connecta's touched responses that way — and
|
|
590
|
+
reading through the reference would digest the response contract as nothing at
|
|
591
|
+
all. Two bounds are deliberate: a `$ref` cycle stays a reference rather than an
|
|
592
|
+
infinite walk, and failure responses are excluded because an error body is
|
|
593
|
+
H11's business, mapped from the status. `--record` rewrites the manifests from
|
|
594
|
+
the documents on hand; run it when a finding has been reviewed, and read the
|
|
595
|
+
diff before committing it.
|
|
596
|
+
|
|
597
|
+
Narrowing is checked against the half being run: `--specs --provider linear`
|
|
598
|
+
and `--hosted --provider notion` exit 2 rather than checking nothing and
|
|
599
|
+
reporting no drift, because a false green from a plausible typo is the one
|
|
600
|
+
failure mode a release-time exit code cannot afford.
|
|
601
|
+
|
|
602
|
+
**What it never does.** No downstream credential reaches CI. No scheduled job,
|
|
603
|
+
no background traffic in a deployment, no automatic issue filing. A finding is
|
|
604
|
+
read by a human and becomes a GitHub issue they wrote, because the decision a
|
|
605
|
+
finding needs — the provider moved this endpoint, or connecta has to stop
|
|
606
|
+
calling it — is not one a diff can make. Published specifications remain drift
|
|
607
|
+
evidence and nothing else: no tool is generated from one, which is the
|
|
608
|
+
[ethos](../ethos.md)'s refusal, not a detail of this script.
|
|
609
|
+
|
|
610
|
+
## What the audit checks
|
|
611
|
+
|
|
612
|
+
The provider audit ([#342](https://github.com/zackbart/connecta/issues/342))
|
|
613
|
+
runs this document against each of the five providers and returns a verdict per
|
|
614
|
+
convention: **meets**, **misses** (with the fix), or **not applicable** (with
|
|
615
|
+
the reason). A convention is never quietly skipped, and an accepted miss is
|
|
616
|
+
recorded as a provider-specific exception with its argument, not left blank.
|
|
617
|
+
Its five reports live in [provider-audit.md](./provider-audit.md), and the
|
|
618
|
+
mechanically checkable half of the hand-written bar runs on every test run in
|
|
619
|
+
[`test/provider-conventions.test.ts`](https://github.com/zackbart/connecta/blob/main/test/provider-conventions.test.ts) —
|
|
620
|
+
so a convention that was met once stays met, or fails loudly.
|
|
621
|
+
|
|
622
|
+
Hand-written providers are audited against H1–H14; hosted-MCP proxies against
|
|
623
|
+
P1–P13. Applying a hand-written convention to a proxy is a category error, not
|
|
624
|
+
a finding.
|
|
625
|
+
|
|
626
|
+
Most of the bar is mechanically checkable against the shipped surface rather
|
|
627
|
+
than by reading:
|
|
628
|
+
|
|
629
|
+
| Convention | Mechanical check |
|
|
630
|
+
| --- | --- |
|
|
631
|
+
| H1, P2 | constructor throws on a blank `purpose`; `instructions` appear appended to the guide |
|
|
632
|
+
| H2 | every tool name matches `^[a-z][a-z0-9_]*$` and opens with a verb from the connector's own set |
|
|
633
|
+
| H3 | first sentence ≤ 160 characters; whole description ≤ 240 |
|
|
634
|
+
| H5 | every tool has an `inputSchema` that is a closed plain object with a `required` list, and every property at every depth — nested objects and array items included — carries a description or sits on a recorded exception list; `api()` refuses to construct one it cannot enforce |
|
|
635
|
+
| H7 | every compact input and output render stays inside 1,024 bytes, or the tool is on a recorded exception list |
|
|
636
|
+
| H8 | every tool declares an `outputSchema` |
|
|
637
|
+
| H9 | every read either projects or documents why it does not; `raw: true` exists wherever the projection drops something recoverable |
|
|
638
|
+
| H10 | every list tool has bounded page arguments, a default below the provider maximum, and exactly one `hasMore`-shaped signal |
|
|
639
|
+
| H11 | every mapped provider status has a test asserting the resulting code and retryability |
|
|
640
|
+
| H12 | `credential` declared with labeled fields, and `testCredential`/`testCredentials` implemented |
|
|
641
|
+
| H13, P7 | `usageGuide` uses the structured form with an explicit `summary`; `required` is set only with a stated reason |
|
|
642
|
+
| H14 | escape hatches split GET / JSON-mutate / upload, and the GET tool is annotated read-only |
|
|
643
|
+
| P1 | the wrapper's `listTools` changes annotations only |
|
|
644
|
+
| P3 | the routing fact appears in the default `title` and the guide's first content line |
|
|
645
|
+
| P4 | endpoint or mode option exists, with the documented default (or no default, where none is safe) |
|
|
646
|
+
| P5 | reads and writes are named lists; an unlisted tool resolves to not-read-only; a reviewed destructive name beats a contradictory `readOnlyHint: true` |
|
|
647
|
+
| P6, P8 | the guide contains the catalog-varies note and the id-resolution rule |
|
|
648
|
+
| P9 | `auth` defaults to OAuth and `requireHttps` is set |
|
|
649
|
+
| P10 | no `credential`, `testCredential`, or `testCredentials` on the wrapper; the mode/key contradiction throws at construction instead |
|
|
650
|
+
| P11 | an authorization failure surfaces as `auth_required`; a downstream tool error is returned unchanged, with no code chosen from its prose |
|
|
651
|
+
| P12 | a declared budget matches a citable documented limit, or the absence is justified in the guide |
|
|
652
|
+
| P13 | classification lists are maintained in one place per provider and built into the manifest the wrapper classifies from, so the drift check compares against the same fact the caller is served |
|
|
653
|
+
|
|
654
|
+
The remainder — H4, H6, and the judgment in H14 about whether a named tool
|
|
655
|
+
beats the escape hatch — is a reading, and the audit reports it as one. The
|
|
656
|
+
Cloudflare keep/prune half of that judgment was made in
|
|
657
|
+
[#350](https://github.com/zackbart/connecta/issues/350): 30 keep, 18 improve,
|
|
658
|
+
3 prune, measured per tool in
|
|
659
|
+
[`eval/current-version/results/issue-350-evidence.md`](https://github.com/zackbart/connecta/blob/main/eval/current-version/results/issue-350-evidence.md).
|
|
660
|
+
Its eighteen `improve` rows are H8 and H9 misses on tools that clearly earn
|
|
661
|
+
their place, so they are this audit's work, not a second removal argument.
|
|
662
|
+
|
|
663
|
+
Each provider's own guide ([Cloudflare](./cloudflare.md),
|
|
664
|
+
[Linear](./linear.md), [Mixpanel](./mixpanel.md), [Notion](./notion.md),
|
|
665
|
+
[Stripe](./stripe.md)) is part of the audited surface: documentation moves with
|
|
666
|
+
the work, and a guide describing a surface that shipped differently is itself a
|
|
667
|
+
miss.
|