@zackbart/connecta 0.15.1 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +28 -15
- package/CHANGELOG.md +466 -0
- package/README.md +19 -4
- 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 +0 -1
- package/dist/catalog.d.ts +0 -1
- package/dist/catalog.js +0 -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 +0 -1
- package/dist/errors.js +0 -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 +250 -276
- 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 +58 -8
- package/documentation/code-mode.md +12 -12
- 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/mixpanel.md +42 -9
- package/documentation/notion.md +16 -0
- package/documentation/operations.md +283 -5
- package/documentation/operator-ui.md +120 -5
- package/documentation/provider-audit.md +160 -0
- package/documentation/provider-conventions.md +631 -0
- package/documentation/request-admission.md +167 -5
- package/documentation/rich-output-design.md +1 -2
- package/documentation/stripe.md +15 -0
- package/ethos.md +13 -8
- package/examples/worker/README.md +96 -12
- package/examples/worker/src/index.ts +46 -1
- package/examples/worker/wrangler.jsonc +14 -1
- package/package.json +4 -5
- package/templates/node/.dockerignore +10 -0
- package/templates/node/.env.example +40 -3
- package/templates/node/AGENTS.md +11 -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
|
@@ -1,7 +1,143 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Downstream call admission
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
One admitted MCP request can fan out into many downstream calls. A program
|
|
4
|
+
calls tools from loops, from `connecta.batch`, and from branches the caller
|
|
5
|
+
never saw. The deployment-wide request pool
|
|
6
|
+
([request admission](./request-admission.md)) bounds the envelope; it cannot
|
|
7
|
+
see anything inside it. A provider that publishes "60 requests per minute"
|
|
8
|
+
needs a bound that counts calls, not requests.
|
|
9
|
+
|
|
10
|
+
So a connector may declare `callAdmission`: an optional per-runtime policy
|
|
11
|
+
around its own `Connector.callTool` attempts.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
const projects = api("projects", {
|
|
15
|
+
description: "Projects — issues and reports",
|
|
16
|
+
callAdmission: {
|
|
17
|
+
// Plural-ready public shape; this release requires exactly one rule.
|
|
18
|
+
rules: [{
|
|
19
|
+
maxConcurrency: 5,
|
|
20
|
+
maxQueueSize: 10, // default 32
|
|
21
|
+
queueTimeoutMs: 2_000, // default 5_000
|
|
22
|
+
retryAfterMs: 1_000, // concurrency-overload hint
|
|
23
|
+
budget: { kind: "rolling-window", maxCalls: 60, windowMs: 60 * 60_000 },
|
|
24
|
+
partitionKey({ toolName, args }) {
|
|
25
|
+
return projectIdFor(toolName, args);
|
|
26
|
+
},
|
|
27
|
+
}],
|
|
28
|
+
maxPartitions: 1_024, // default
|
|
29
|
+
},
|
|
30
|
+
tools: [/* … */],
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`api()`, `remoteMcp()`, and a hand-written `Connector` all accept it. Maintained
|
|
35
|
+
prebuilt connections set it themselves — but only when the provider documents a
|
|
36
|
+
number, which is convention
|
|
37
|
+
[P12](./provider-conventions.md#p12--declare-an-admission-budget-only-when-the-provider-documents-a-number).
|
|
38
|
+
An invented budget is a throttle with no source.
|
|
39
|
+
|
|
40
|
+
## Policy contract
|
|
41
|
+
|
|
42
|
+
`maxConcurrency` and `budget` are independently optional, but a rule must
|
|
43
|
+
declare at least one — a rule that bounds nothing is a typo, not a policy.
|
|
44
|
+
Queue settings require `maxConcurrency`, because there is nothing to queue
|
|
45
|
+
behind without it, and `maxQueueSize: 0` is the fail-fast shape. Numeric bounds
|
|
46
|
+
are finite whole numbers: concurrency, timeouts, budget values, and
|
|
47
|
+
`maxPartitions` are positive; queue size and `retryAfterMs` may be zero.
|
|
48
|
+
`budget.kind` must be `"rolling-window"`. Everything invalid throws during
|
|
49
|
+
registry construction.
|
|
50
|
+
|
|
51
|
+
The container is an array so a later release can add atomic rules over
|
|
52
|
+
different partition dimensions, but **exactly one rule** is accepted today.
|
|
53
|
+
Zero or several throw rather than imply semantics the runtime does not have.
|
|
54
|
+
Multiple rules cannot be faked as sequential leases: consuming one rolling
|
|
55
|
+
token before a later rule refuses would charge a call that never reached the
|
|
56
|
+
provider — the exact accounting error a budget exists to prevent.
|
|
57
|
+
|
|
58
|
+
Omitting `partitionKey` gives one connector-wide partition. A callback runs
|
|
59
|
+
synchronously on model-supplied arguments and must return a non-secret string
|
|
60
|
+
of at most 128 UTF-8 bytes. A throw or an invalid return is a typed local
|
|
61
|
+
`connector_call_failed`; neither the callback text nor the arguments are
|
|
62
|
+
surfaced. The callback is operator code and may abort the caller synchronously,
|
|
63
|
+
so cancellation is rechecked after it returns — otherwise a cancelled call
|
|
64
|
+
could still consume a budget entry.
|
|
65
|
+
|
|
66
|
+
State retains the returned key, bounded timestamps, counters, signals, and
|
|
67
|
+
promise continuations. Nothing copies arguments into the limiter; the queued
|
|
68
|
+
waiter closure deliberately captures the `AbortSignal` rather than the input
|
|
69
|
+
object, because closing over the input would retain its `args`. At most
|
|
70
|
+
`maxPartitions` states are live, and an idle state is evicted only once its
|
|
71
|
+
active calls, queue, and rolling-window history are all empty — eviction must
|
|
72
|
+
not be a way to reset a live budget. Exhausted partition capacity is
|
|
73
|
+
`rate_limited` with the configured retry hint, not a silent unbounded map.
|
|
74
|
+
|
|
75
|
+
## Attempt semantics
|
|
76
|
+
|
|
77
|
+
The registry owns the limiter, and `InvocationService.invoke` acquires a permit
|
|
78
|
+
immediately before `Connector.callTool` and releases it in `finally`. Both call
|
|
79
|
+
paths — top-level `call_tool` and a program's `connecta.call` or
|
|
80
|
+
`connecta.batch` — reach that same seam, so a program cannot buy itself a
|
|
81
|
+
second limit by taking the other route.
|
|
82
|
+
|
|
83
|
+
- **A retry is another attempt.** It reacquires and can consume another budget
|
|
84
|
+
entry, because the provider counts it that way. Backoff never holds a
|
|
85
|
+
concurrency permit.
|
|
86
|
+
- **A proactive short-window `rate_limited` refusal** participates in the
|
|
87
|
+
ordinary retry policy and counts as an attempt. Activity records the final
|
|
88
|
+
outcome and the attempt count.
|
|
89
|
+
- **A queued cancellation consumes nothing.** It is removed from the queue with
|
|
90
|
+
no rolling-window entry charged.
|
|
91
|
+
- **Caller cancellation is terminal.** It is non-retryable, releases its
|
|
92
|
+
permit, records a `cancelled` activity outcome, and is not connector-health
|
|
93
|
+
evidence.
|
|
94
|
+
- **Budget exhaustion fails immediately** as `rate_limited`, `retryable: true`,
|
|
95
|
+
with the exact `retryAfterMs` to the next free slot. Concurrency overflow
|
|
96
|
+
uses the configured hint instead, because there is no exact answer.
|
|
97
|
+
- **Local refusals are not connector health failures.** They are activity
|
|
98
|
+
errors, but no provider call happened, so poisoning the connector's health
|
|
99
|
+
with them would report the limiter's success as the downstream's failure.
|
|
100
|
+
`isCallAdmissionError` is what keeps `recordFailure` out of that path.
|
|
101
|
+
|
|
102
|
+
Only tool execution is covered. Catalog `listTools`, status probes, credential
|
|
103
|
+
checks, and authorization operations stay outside the budget: they are not the
|
|
104
|
+
calls a provider is rate-limiting, and charging discovery for them would make
|
|
105
|
+
a program's first search cost it capacity to act.
|
|
106
|
+
|
|
107
|
+
Queue admission and connector execution have separate clocks. `queueTimeoutMs`
|
|
108
|
+
bounds only the wait for a permit; a per-attempt `timeoutMs` starts after
|
|
109
|
+
admission. A saturated call can therefore take up to their sum. With
|
|
110
|
+
`diagnostics: true`, `admissionMs` reports the permit wait and `connectorMs`
|
|
111
|
+
the admitted attempt — which is the only way to tell "the provider is slow"
|
|
112
|
+
from "we are throttling ourselves".
|
|
113
|
+
|
|
114
|
+
## Enforcement scope
|
|
115
|
+
|
|
116
|
+
This is deliberately **per-runtime**. It completely contains fan-out inside one
|
|
117
|
+
request, including a wide `connecta.batch` in one Worker isolate. A rolling
|
|
118
|
+
budget is exact inside one Node process or Worker isolate, and best-effort
|
|
119
|
+
across isolates, replicas, and restarts.
|
|
120
|
+
|
|
121
|
+
KV cannot coordinate the invariant without atomic operations, and this release
|
|
122
|
+
adds no Durable Object or other distributed coordinator. The async typed permit
|
|
123
|
+
is the seam a coordinator would slot into later; until then the guide says what
|
|
124
|
+
the bound actually is rather than implying a global one.
|
|
125
|
+
|
|
126
|
+
## Observations
|
|
127
|
+
|
|
128
|
+
`/health` exposes payload-free aggregates at
|
|
129
|
+
`admission.downstreamCalls.connectors.<id>`: retained partition count, current
|
|
130
|
+
active and queued gauges, cumulative admitted/queued/rejected/rate-limited/
|
|
131
|
+
cancelled counts, and queue-wait count, total, and maximum. The open endpoint
|
|
132
|
+
never exposes partition keys, tool arguments, or results — a partition key can
|
|
133
|
+
be a customer identifier, which is precisely why it stays out of an unauthenticated
|
|
134
|
+
payload. Ordinary payload-free activity records the final call outcome and its
|
|
135
|
+
typed error code.
|
|
136
|
+
|
|
137
|
+
## Tests that enforce this
|
|
138
|
+
|
|
139
|
+
| Invariant | Suite |
|
|
140
|
+
| --- | --- |
|
|
141
|
+
| Independent partitions, exact rolling-window reset and retry, queued cancellation charging no budget, synchronous cancel during partition derivation, validated values snapshotted rather than read from mutable config, bounded partition state and contained `partitionKey` failures, empty and multi-rule policies refused | `test/call-admission.test.ts` (controller) |
|
|
142
|
+
| One base-registry limiter shared by direct and program calls, batch bounds with input order preserved, cancellation threading, no dispatch or retry or health poisoning after cancellation, short proactive windows retried without poisoning health, payload-free `/health` aggregates | `test/call-admission.test.ts` (integration, Node + Workers) |
|
|
143
|
+
| Where provider budgets are allowed to come from at all | [provider conventions P12](./provider-conventions.md#p12--declare-an-admission-budget-only-when-the-provider-documents-a-number), [provider audit](./provider-audit.md) |
|
|
@@ -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-
|
|
5
|
+
surface over Cloudflare's v4 REST API. Fifty-two 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
|
|
@@ -144,7 +144,7 @@ projections:
|
|
|
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 |
|
|
147
|
-
| R2 | buckets, object metadata,
|
|
147
|
+
| R2 | buckets, object metadata, CORS | create/update/delete bucket, delete object |
|
|
148
148
|
| Pages | projects, deployments, domains | retry/rollback/delete deployments, add/delete domains, purge build cache, delete project |
|
|
149
149
|
|
|
150
150
|
Every named tool carries a complete hand-written input schema: closed
|
|
@@ -153,6 +153,39 @@ every constrained field, endpoint-specific pagination bounds, and a description
|
|
|
153
153
|
on every property. `test/cloudflare-provider.test.ts` walks the surface and
|
|
154
154
|
asserts those properties rather than leaving them as a claim.
|
|
155
155
|
|
|
156
|
+
### What the named surface deliberately leaves out
|
|
157
|
+
|
|
158
|
+
A named tool is a permanent line item in every deployment's catalog, so the
|
|
159
|
+
surface was measured against the escape hatches rather than assumed to beat
|
|
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:
|
|
164
|
+
|
|
165
|
+
- **R2 CORS writes.** `set_r2_cors` declared its rule list as free-form objects
|
|
166
|
+
— the untyped body this connection refuses everywhere else — so its schema
|
|
167
|
+
validated the bucket name and waved through the part of the call that
|
|
168
|
+
actually fails. It also returned Cloudflare's response unprojected and was
|
|
169
|
+
destructive either way, which left nothing for it to beat
|
|
170
|
+
`cloudflare_api_mutate` on. `delete_r2_cors` did not lose that comparison:
|
|
171
|
+
it returned a fixed `{deleted: true}` behind a closed output schema and
|
|
172
|
+
refused every malformed argument locally. It went anyway, because naming only
|
|
173
|
+
the delete would mean one CORS policy is set through the raw route and
|
|
174
|
+
cleared through a named tool. Read a policy with `get_r2_cors`; change one
|
|
175
|
+
with `cloudflare_api_mutate` at
|
|
176
|
+
`PUT`/`DELETE /accounts/{accountId}/r2/buckets/{bucketName}/cors`. This is the
|
|
177
|
+
same split the [DNS record types](#dns-record-types) already use: structured
|
|
178
|
+
bodies stay readable through named tools and writable through the
|
|
179
|
+
approval-gated raw route.
|
|
180
|
+
- **R2 account metrics.** `get_r2_metrics` took one account id, put it in a
|
|
181
|
+
path, and returned the response untouched — `cloudflare_api_get` at
|
|
182
|
+
`/accounts/{accountId}/r2/metrics` does the same thing without a permanent
|
|
183
|
+
catalog line.
|
|
184
|
+
|
|
185
|
+
The surviving 48 named tools all refuse malformed arguments locally, which is
|
|
186
|
+
the one thing no escape hatch can do: a hatch's path is an opaque string, so it
|
|
187
|
+
can only check that a path is a path.
|
|
188
|
+
|
|
156
189
|
### The whole-v4 escape hatch
|
|
157
190
|
|
|
158
191
|
Cloudflare adds products and endpoints faster than a curated connector should
|
|
@@ -171,8 +204,14 @@ method classification into user input:
|
|
|
171
204
|
|
|
172
205
|
All three accept explicit endpoint-specific headers, which supports R2
|
|
173
206
|
jurisdictions, conditional requests, encryption controls, and object metadata.
|
|
174
|
-
|
|
175
|
-
|
|
207
|
+
`Authorization`, `Cookie`, `Host`, `Content-Length`, `Content-Type`, and
|
|
208
|
+
`Transfer-Encoding` remain connector-owned and are refused: authentication,
|
|
209
|
+
host selection, content type, and request framing are not the caller's to set.
|
|
210
|
+
That refused list lives in the usage guide rather than in each header property
|
|
211
|
+
description, because the compact renderer inlines a property description once
|
|
212
|
+
per tool that shares it, and repeating it three times pushed
|
|
213
|
+
`cloudflare_api_upload`'s compact input shape past the 1,024-byte discovery
|
|
214
|
+
budget.
|
|
176
215
|
|
|
177
216
|
Paths are relative to `/client/v4`. Absolute URLs, protocol-relative paths,
|
|
178
217
|
`..` traversal, fragments, and embedded query strings are refused locally;
|
|
@@ -191,8 +230,8 @@ comes from Cloudflare's API reference.
|
|
|
191
230
|
|
|
192
231
|
### Where the `perPage` bounds come from
|
|
193
232
|
|
|
194
|
-
`
|
|
195
|
-
before it reaches Cloudflare. That is only a favor when the bound is really
|
|
233
|
+
`api()` enforces every hand-written schema, so an out-of-range `perPage` is
|
|
234
|
+
refused locally before it reaches Cloudflare. That is only a favor when the bound is really
|
|
196
235
|
Cloudflare's, so the schemas record which ones are and the descriptions say so
|
|
197
236
|
out loud:
|
|
198
237
|
|
|
@@ -260,8 +299,12 @@ Paginated lists add a `page` object derived from `result_info`:
|
|
|
260
299
|
field to branch on.
|
|
261
300
|
|
|
262
301
|
Some endpoints do not work that way, and the schemas say so rather than leaving
|
|
263
|
-
an agent to discover it. `
|
|
264
|
-
`list_kv_keys` paginate by cursor and return
|
|
302
|
+
an agent to discover it. `list_zone_rulesets`, `list_r2_buckets`,
|
|
303
|
+
`list_r2_objects`, and `list_kv_keys` paginate by cursor and return
|
|
304
|
+
`nextCursor` instead of `page`. On those four the `cursor` argument says the
|
|
305
|
+
endpoint pages by cursor rather than page number, and the `nextCursor` result
|
|
306
|
+
says it is the only signal and that no `page` object is coming — so the loop
|
|
307
|
+
condition is legible from either end of one tool, without fetching the guide.
|
|
265
308
|
`list_worker_scripts` reports no counters at all and omits `page` entirely.
|
|
266
309
|
|
|
267
310
|
Projected resource reads expose `raw: true` where the provider's larger object
|
|
@@ -329,3 +372,10 @@ dashboard traffic of a human sharing the account is counted by Cloudflare but
|
|
|
329
372
|
not by Connecta. `maxConcurrency` is the bound that actually protects a shared
|
|
330
373
|
credential, because a single `execute_code` program can fan out far faster than
|
|
331
374
|
the window notices.
|
|
375
|
+
|
|
376
|
+
## Conventions
|
|
377
|
+
|
|
378
|
+
This connection is audited against
|
|
379
|
+
[the provider conventions](./provider-conventions.md). Its verdict per
|
|
380
|
+
convention, including every recorded exception, is the Cloudflare section of
|
|
381
|
+
[the provider audit](./provider-audit.md).
|
|
@@ -8,10 +8,9 @@ it is specified in prose first and implemented second — the same discipline th
|
|
|
8
8
|
[MCP spec bump](./mcp-2026-07-28.md) followed.
|
|
9
9
|
|
|
10
10
|
Two executors implement this document: QuickJS in a child process on Node, and
|
|
11
|
-
`DynamicWorkerExecutor` from `@cloudflare/codemode` on Workers. Divergence
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
implement a third executor from this document without reading either.
|
|
11
|
+
`DynamicWorkerExecutor` from `@cloudflare/codemode` on Workers. Divergence between
|
|
12
|
+
them is a bug unless it appears in [Executor exceptions](#executor-exceptions),
|
|
13
|
+
which names the reason. Anyone can implement a third from this document alone.
|
|
15
14
|
|
|
16
15
|
The [code-first exploration](./code-first-exploration.md) is the evidence behind
|
|
17
16
|
the direction; [`ethos.md`](../ethos.md) carries the verdicts. Where its prototype
|
|
@@ -43,7 +42,7 @@ const connecta = createConnecta({
|
|
|
43
42
|
CPU, wall-time, memory, stack, queue, result, log, and IPC bounds are configured
|
|
44
43
|
on the executor. Server bundlers must keep the `@zackbart/connecta/quickjs`
|
|
45
44
|
package files external so the child entry stays on disk. The
|
|
46
|
-
[Node
|
|
45
|
+
[Node template](../templates/node/README.md) carries the complete setup.
|
|
47
46
|
|
|
48
47
|
On Cloudflare Workers, the Worker Loader binding provides the required sandbox:
|
|
49
48
|
|
|
@@ -54,9 +53,8 @@ createConnecta({
|
|
|
54
53
|
});
|
|
55
54
|
```
|
|
56
55
|
|
|
57
|
-
Dynamic Workers require the Workers Paid plan. The
|
|
58
|
-
[Worker example](../examples/worker/README.md#code-mode)
|
|
59
|
-
required binding and package setup.
|
|
56
|
+
Dynamic Workers require the Workers Paid plan. The complete required binding and
|
|
57
|
+
package setup is in the [Worker example](../examples/worker/README.md#code-mode).
|
|
60
58
|
|
|
61
59
|
## What an executor must implement
|
|
62
60
|
|
|
@@ -64,6 +62,7 @@ The host side of the seam is two types in `src/types.ts` and nothing else.
|
|
|
64
62
|
|
|
65
63
|
```ts
|
|
66
64
|
interface Executor {
|
|
65
|
+
readonly name?: string; // what /health and doctor report
|
|
67
66
|
execute(code: string, providers: ExecutorProvider[]): Promise<ExecuteResult>;
|
|
68
67
|
close?(): void | Promise<void>;
|
|
69
68
|
}
|
|
@@ -113,11 +112,12 @@ Connecta passes exactly one provider, named `connecta`. An executor must:
|
|
|
113
112
|
|
|
114
113
|
Optionally implement `AdmittingExecutor` (`acquire()` returning a lease whose
|
|
115
114
|
`execute` runs once) for bounded admission (`L7`) and `close()` for shutdown;
|
|
116
|
-
connecta wraps a plain `Executor` with `withExecutorAdmission` otherwise.
|
|
115
|
+
connecta wraps a plain `Executor` with `withExecutorAdmission` otherwise. The
|
|
116
|
+
optional `name` — else a class's constructor name, which a minifier may rewrite
|
|
117
|
+
— is what [`/health` and `doctor`](./operations.md#the-cli) report.
|
|
117
118
|
|
|
118
|
-
Note what is *not* on this list: [emitted output](#emitted-output) asks
|
|
119
|
-
|
|
120
|
-
(`M8`).
|
|
119
|
+
Note what is *not* on this list: [emitted output](#emitted-output) asks nothing
|
|
120
|
+
of an executor — `connecta.emit` is just another provider function (`M8`).
|
|
121
121
|
|
|
122
122
|
## The program
|
|
123
123
|
|
|
@@ -1,7 +1,174 @@
|
|
|
1
1
|
# Connector guides
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
Descriptions and schemas say *what* a connector's tools are. They do not say
|
|
4
|
+
which tool to prefer, which id format an address quirk expects, how the service
|
|
5
|
+
paginates, or how hard you may hammer it. A connector's optional `usageGuide`
|
|
6
|
+
is where that goes — deployment-owned configuration, served by the `skills`
|
|
7
|
+
meta-tool as `connector:<id>` and returned verbatim, with a bounded `summary`
|
|
8
|
+
surfaced in discovery so an agent learns the guide exists at the moment it
|
|
9
|
+
matters. See [meta-tools](./meta-tools.md#connector-guide-selection) for the
|
|
10
|
+
discovery contract and [connectors](./connectors.md#catalog-contract) for how a
|
|
11
|
+
guide is configured.
|
|
12
|
+
|
|
13
|
+
## The shape of a guide
|
|
14
|
+
|
|
15
|
+
`usageGuide` accepts a bare markdown string, which is still the shortest
|
|
16
|
+
correct configuration, or the structured form:
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
const notion = remoteMcp("notion", {
|
|
20
|
+
url: "https://mcp.notion.com/mcp",
|
|
21
|
+
description: "Notion — pages, databases, comments",
|
|
22
|
+
auth: { type: "oauth" },
|
|
23
|
+
usageGuide: {
|
|
24
|
+
content: `# Notion usage
|
|
25
|
+
|
|
26
|
+
Search before listing: \`notion.search\` covers pages and databases in one call.
|
|
27
|
+
|
|
28
|
+
- Page ids are dashed UUIDs. Strip the trailing slug from a pasted URL first.
|
|
29
|
+
- Paginate with \`start_cursor\`; \`page_size\` is capped at 100.
|
|
30
|
+
- Writes replace blocks wholesale — read the block, merge, then write.
|
|
31
|
+
`,
|
|
32
|
+
summary: "Search before listing; dashed-UUID page ids; cursor pagination",
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The field is on the `Connector` interface, not on a factory, so it works
|
|
38
|
+
identically for `api()`, `remoteMcp()`, a prebuilt connection, and a
|
|
39
|
+
hand-written connector. It is deployment-owned configuration like everything
|
|
40
|
+
else here: an edit and a redeploy, never a runtime registration.
|
|
41
|
+
|
|
42
|
+
`content` is returned byte for byte by `skills({ name: "connector:<id>" })`.
|
|
43
|
+
`summary` is normalized and capped at 120 characters for discovery. Omit it and
|
|
44
|
+
connecta derives the same bounded line the skills listing uses: the first
|
|
45
|
+
meaningful body line, with frontmatter, fences, rules, comments, and table rows
|
|
46
|
+
skipped, a heading used only when the guide has no body, and the connector's
|
|
47
|
+
description as the last resort. A derived summary is usually worse than a
|
|
48
|
+
written one — it was written to open a document, not to answer "is this guide
|
|
49
|
+
relevant to what I am about to do".
|
|
50
|
+
|
|
51
|
+
`connector:<id>` is the only address for a guide, and built-in skill names are
|
|
52
|
+
bare identifiers, so a guide can never shadow or be shadowed by `usage`: a
|
|
53
|
+
connector whose id is literally `usage` is listed as `connector:usage`, and
|
|
54
|
+
`skills({ name: "usage" })` still returns the built-in guide. Every miss —
|
|
55
|
+
unknown name, unknown connector, connector with no guide — is an explicit
|
|
56
|
+
error. Nothing silently falls back to the generic guide, because a generic
|
|
57
|
+
answer to a specific question is worse than no answer.
|
|
58
|
+
|
|
59
|
+
Discovery text is conditional on the deployment actually having a guide. The
|
|
60
|
+
guide sentences in the `skills`, `search_tools`, `call_destructive_tool`, and
|
|
61
|
+
`execute_code` descriptions appear only when at least one visible connector
|
|
62
|
+
declares one — the connector set is fixed at construction, so this is stable
|
|
63
|
+
per deployment, and a deployment with no guides pays no always-loaded context
|
|
64
|
+
for a feature it does not use. The built-in `usage` skill is the deliberate
|
|
65
|
+
exception: it stays byte-identical across every deployment, including its
|
|
66
|
+
per-connector-guides section, so an agent that has read it once in a task never
|
|
67
|
+
needs a deployment-local copy of it.
|
|
68
|
+
|
|
69
|
+
## What belongs in a guide
|
|
70
|
+
|
|
71
|
+
The test is not "is this true" but "can a schema carry it". If a schema can,
|
|
72
|
+
fix the schema instead — a constraint in the schema is enforced, is visible at
|
|
73
|
+
the moment of the call, and costs nothing to a caller who never fetches the
|
|
74
|
+
guide.
|
|
75
|
+
|
|
76
|
+
So a guide carries:
|
|
77
|
+
|
|
78
|
+
- **Tool preference.** Which of two plausible tools is the right one, and the
|
|
79
|
+
fact that decides it.
|
|
80
|
+
- **Sequence.** What must happen first — resolving a name to an id, listing a
|
|
81
|
+
parent before a child.
|
|
82
|
+
- **Identity and address quirks.** The id format an address expects, the
|
|
83
|
+
difference between the id in the URL and the id the API wants.
|
|
84
|
+
- **Pagination conventions.** The cursor field, the page cap, whether the
|
|
85
|
+
connector has more than one convention.
|
|
86
|
+
- **Units and aliases.** What a bare number means; what the service calls the
|
|
87
|
+
thing the caller calls something else.
|
|
88
|
+
- **Reduction advice.** Which fields matter, for a downstream whose results are
|
|
89
|
+
large and whose schemas you do not control.
|
|
90
|
+
- **Rate-limit etiquette**, with the number.
|
|
91
|
+
|
|
92
|
+
And a guide does not carry: anything readable off the schema, background on
|
|
93
|
+
what the service is, marketing, a tool list, or a second copy of the tool
|
|
94
|
+
descriptions. It is fetched into a live context window — every line that
|
|
95
|
+
repeats the schema is a line that displaced one that did not.
|
|
96
|
+
|
|
97
|
+
**Style.** Write for the agent, not the operator; the built-in `usage` skill
|
|
98
|
+
(`src/skills.ts`) is the model. Concise and imperative, leading with the
|
|
99
|
+
decision rather than the background ("Search before listing", not "Notion has a
|
|
100
|
+
search API"). Prefer short bullets to prose. Name exact tool addresses and
|
|
101
|
+
argument names. State a constraint with its number.
|
|
102
|
+
|
|
103
|
+
## The `required` rule
|
|
104
|
+
|
|
105
|
+
`required: true` on the structured form means: fetch this guide before every
|
|
106
|
+
operation on this connector. It surfaces as `guideRequired: true` with
|
|
107
|
+
`guideRequiredReasons: ["connector_required"]` on discovery results.
|
|
108
|
+
|
|
109
|
+
It is an instruction, not a gate. Nothing refuses the call — connecta tells the
|
|
110
|
+
agent to read the guide first and then believes it, because a server-side
|
|
111
|
+
refusal here would be a policy engine, which
|
|
112
|
+
[`ethos.md`](../ethos.md) refuses.
|
|
113
|
+
|
|
114
|
+
Reserve it for connectors whose correct arguments or sequence *cannot* be
|
|
115
|
+
expressed by the downstream tool schema at all: generic API wrappers whose one
|
|
116
|
+
broad tool name carries no endpoint vocabulary, and cross-operation conventions
|
|
117
|
+
no single schema can state. Two categories already produce the flag on their
|
|
118
|
+
own and must not be hand-declared for it — an unannotated or write-capable tool
|
|
119
|
+
(`approval_required`) and a compact schema that was capped
|
|
120
|
+
(`schema_truncated`). `connector_required` and `approval_required` survive
|
|
121
|
+
exact schema expansion; `schema_truncated` clears once describe returns the
|
|
122
|
+
exact shape.
|
|
123
|
+
|
|
124
|
+
The failure mode of over-declaring is quiet and expensive: an agent that must
|
|
125
|
+
fetch a guide before every call pays that fetch on the calls where the schema
|
|
126
|
+
was already complete and unambiguous. `required` earns its cost on connectors
|
|
127
|
+
where the alternative is a wrong call, not on connectors where it is a slightly
|
|
128
|
+
slower right one.
|
|
129
|
+
|
|
130
|
+
## Provider conventions
|
|
131
|
+
|
|
132
|
+
The maintained prebuilt connections come in two shapes, and each has its own
|
|
133
|
+
convention set — including the shape of its usage guide, which is one of the
|
|
134
|
+
few things both shapes fully own:
|
|
135
|
+
|
|
136
|
+
- [Hand-written HTTP providers](./provider-conventions.md#hand-written-http-providers)
|
|
137
|
+
(H1–H14) — `api()` surfaces where Connecta owns every name, schema,
|
|
138
|
+
projection, and error. Cloudflare and Notion.
|
|
139
|
+
- [Hosted-MCP proxies](./provider-conventions.md#hosted-mcp-proxies) (P1–P13) —
|
|
140
|
+
`remoteMcp()` wrappers where the downstream owns the catalog and Connecta
|
|
141
|
+
owns the endpoint, credential, classification, guide, and budget. Linear,
|
|
142
|
+
Stripe, and Mixpanel.
|
|
143
|
+
|
|
144
|
+
Both sets are judged by one measure: what the convention saves the model that
|
|
145
|
+
interacts with connecta, priced in discovery tokens, wrong-tool selection,
|
|
146
|
+
argument retries, or result size. The same document defines
|
|
147
|
+
[what a provider audit checks](./provider-conventions.md#what-the-audit-checks),
|
|
148
|
+
so [#342](https://github.com/zackbart/connecta/issues/342) can run against it
|
|
149
|
+
convention by convention rather than by taste.
|
|
150
|
+
|
|
151
|
+
Two of those conventions decide how a guide is written, and they differ by
|
|
152
|
+
shape. A hand-written provider's guide carries only what a schema cannot,
|
|
153
|
+
because it owns the schemas and should fix them instead
|
|
154
|
+
([H13](./provider-conventions.md#h13--the-guide-carries-only-what-a-schema-cannot)).
|
|
155
|
+
A proxy's guide carries the reduction and identity-resolution advice its
|
|
156
|
+
schemas will never carry, because it cannot change them
|
|
157
|
+
([P7](./provider-conventions.md#p7--the-guide-carries-the-reduction-advice-the-schemas-cannot),
|
|
158
|
+
[P8](./provider-conventions.md#p8--identity-resolution-comes-before-action)).
|
|
159
|
+
|
|
160
|
+
## Tests that enforce this
|
|
161
|
+
|
|
162
|
+
`test/meta-tools.test.ts` owns the guide behavior end to end: the skills
|
|
163
|
+
listing carrying one entry per guided connector, summaries derived from the
|
|
164
|
+
first meaningful line and falling back to the connector description when the
|
|
165
|
+
guide is all markup, whitespace-only guides treated as no guide, content
|
|
166
|
+
returned verbatim including surrounding padding, identical content in two
|
|
167
|
+
deployments staying isolated, every miss erroring rather than falling back to
|
|
168
|
+
the generic guide with an identically labelled skills list on each branch, the
|
|
169
|
+
`guide` pointer in search output, and `guideRequired` appearing for
|
|
170
|
+
connector-required conventions, approval-bound tools, and truncated schemas —
|
|
171
|
+
and being absent from a search that asked for no schemas. `test/server.test.ts`
|
|
172
|
+
owns the conditional half: it compares a guide-free deployment's four tool
|
|
173
|
+
descriptions against a guided one's, and asserts the `usage` skill is
|
|
174
|
+
byte-identical between them.
|
|
@@ -111,6 +111,11 @@ export const connecta = createConnecta({
|
|
|
111
111
|
All three are ordinary `Connector` instances by the time the registry sees
|
|
112
112
|
them. Nothing in the list is privileged by how it was authored.
|
|
113
113
|
|
|
114
|
+
What a maintained connection owes an agent is written down rather than
|
|
115
|
+
inherited from whoever wrote it last:
|
|
116
|
+
[provider conventions](./provider-conventions.md), one set for hand-written
|
|
117
|
+
HTTP surfaces and one for hosted-MCP proxies.
|
|
118
|
+
|
|
114
119
|
Maintained provider guides:
|
|
115
120
|
|
|
116
121
|
- [Cloudflare](./cloudflare.md)
|
|
@@ -119,6 +124,90 @@ Maintained provider guides:
|
|
|
119
124
|
- [Notion](./notion.md)
|
|
120
125
|
- [Stripe](./stripe.md)
|
|
121
126
|
|
|
127
|
+
## The `api()` construction contract
|
|
128
|
+
|
|
129
|
+
`api()` is the path every custom integration takes, and whatever it accepts is
|
|
130
|
+
what an agent eventually reads. Three things are refused at construction rather
|
|
131
|
+
than discovered in production
|
|
132
|
+
([#340](https://github.com/zackbart/connecta/issues/340)):
|
|
133
|
+
|
|
134
|
+
- **A tool with no description.** Discovery has nothing else to route on, and a
|
|
135
|
+
guess costs a call.
|
|
136
|
+
- **A tool with no explicit boolean `annotations.readOnlyHint`.** `true`
|
|
137
|
+
declares a read and admits the tool to `call_tool` and `execute_code`;
|
|
138
|
+
`false` declares work that crosses `call_destructive_tool`, where the host
|
|
139
|
+
can ask a human. Connecta never infers the classification from a tool name,
|
|
140
|
+
description, schema, HTTP method, or the other annotations — an unclassified
|
|
141
|
+
tool is a bug in the deployment, not a puzzle to solve.
|
|
142
|
+
- **An `inputSchema` the validator cannot compile.** Declaring one is optional;
|
|
143
|
+
declaring one that cannot be enforced is not. A schema that only reveals
|
|
144
|
+
itself on first use — an unresolvable `$ref`, say — fails that call as
|
|
145
|
+
non-retryable `invalid_args` rather than forwarding raw arguments to the
|
|
146
|
+
handler. `validateArgs: false` still opts out of enforcement for deployments
|
|
147
|
+
that want loose coercion; it does not opt out of the schema being real.
|
|
148
|
+
|
|
149
|
+
None of this reaches a proxied catalog: hosted-MCP tools arrive as their
|
|
150
|
+
downstream wrote them, and an unannotated or contradictory one stays
|
|
151
|
+
fail-closed onto `call_destructive_tool`. The contract binds the surfaces we
|
|
152
|
+
write, not the catalogs we relay.
|
|
153
|
+
|
|
154
|
+
## The guarded fetch transport
|
|
155
|
+
|
|
156
|
+
Every hand-written HTTP surface re-derives the same safety machinery, and two
|
|
157
|
+
of them had already derived it slightly differently. `src/connectors/guarded-fetch.ts`
|
|
158
|
+
is that machinery extracted once ([#341](https://github.com/zackbart/connecta/issues/341)):
|
|
159
|
+
one `guardedFetch({ provider, baseUrl, headers, maxResponseBytes, authenticate })`
|
|
160
|
+
factory returning the transport a connector sends every request through.
|
|
161
|
+
|
|
162
|
+
What it owns is mechanical and provider-independent:
|
|
163
|
+
|
|
164
|
+
- **Confinement.** A request path is provider-relative, carries no query or
|
|
165
|
+
fragment, and is re-checked against the base origin and path prefix *after*
|
|
166
|
+
`new URL` normalization — because normalization is how a path escapes a
|
|
167
|
+
prefix, not something to trust before it.
|
|
168
|
+
- **Construction.** Query parameters are encoded rather than concatenated, a
|
|
169
|
+
JSON body is serialized with the `Content-Type` to match, and a pre-framed
|
|
170
|
+
body gets none, so `fetch` still picks the multipart boundary.
|
|
171
|
+
- **Credential forwarding.** `authenticate` is called once per request and its
|
|
172
|
+
headers are applied last; a request header wearing one of their names is
|
|
173
|
+
refused rather than allowed to shadow it. A 3xx is refused outright — a
|
|
174
|
+
redirect is an instruction to re-send the credential to whatever origin the
|
|
175
|
+
`Location` names, and a confinement a redirect can undo was never one.
|
|
176
|
+
- **Bounded reads.** `maxResponseBytes` is required, not defaulted: what counts
|
|
177
|
+
as an absurd response is a fact about the API, not about HTTP. A declared
|
|
178
|
+
`Content-Length` past the ceiling fails before a byte is read, and a
|
|
179
|
+
streaming body is abandoned at the ceiling rather than buffered past it.
|
|
180
|
+
- **Normalization.** An unreachable provider becomes a retryable `unavailable`
|
|
181
|
+
instead of whatever `TypeError` the runtime threw, and `ctx.signal` rides
|
|
182
|
+
every request.
|
|
183
|
+
|
|
184
|
+
What it deliberately does not own is meaning. It never reads a status code and
|
|
185
|
+
never invents an authentication scheme: the provider's `authenticate` callback
|
|
186
|
+
supplies the headers, and the provider's mapper turns one `GuardedResponse`
|
|
187
|
+
into a result or a typed failure. That split is not fastidiousness. Notion's
|
|
188
|
+
403 means a capability the integration was never granted — re-authorizing
|
|
189
|
+
cannot fix it — while Cloudflare's means a token scope, and the two want
|
|
190
|
+
opposite next moves. A helper that guessed would be wrong for one of them.
|
|
191
|
+
|
|
192
|
+
Cloudflare and Notion both run on it. Their existing suites carried over
|
|
193
|
+
unchanged, which proves the migration kept the behavior those suites cover —
|
|
194
|
+
not that nothing changed. Three things did, and the changelog names them: a
|
|
195
|
+
3xx is refused where both providers used to follow it, both now fail past
|
|
196
|
+
their byte ceiling, and `cloudflare()`'s `baseUrl` is validated at
|
|
197
|
+
construction. Each suite gained one test for the ceiling, because the one
|
|
198
|
+
guard the helper was written to add is the one a provider's own mapper can
|
|
199
|
+
most easily disarm: a bare `catch` around `response.json()` swallows the
|
|
200
|
+
transport's refusal along with a parse error, and turns a response nobody was
|
|
201
|
+
allowed to read into an empty success. A mapper re-throws
|
|
202
|
+
`ConnectorCallError` and swallows only what it recognizes.
|
|
203
|
+
|
|
204
|
+
It is **not exported this release**: the two migrations proved the shape
|
|
205
|
+
preserves behavior for connectors that already had this machinery, not that it
|
|
206
|
+
is the right shape for an author starting from nothing, and an unexported
|
|
207
|
+
symbol costs nothing to reshape while a published one is a promise.
|
|
208
|
+
The provider audit ([#342](https://github.com/zackbart/connecta/issues/342))
|
|
209
|
+
supplies the third caller that would settle it.
|
|
210
|
+
|
|
122
211
|
## MCP version skew
|
|
123
212
|
|
|
124
213
|
Connecta deliberately sits between protocol generations
|
package/documentation/linear.md
CHANGED
|
@@ -11,6 +11,7 @@ import { linear } from "@zackbart/connecta/providers/linear";
|
|
|
11
11
|
const tracker = linear("product_tracker", {
|
|
12
12
|
title: "Product issue tracking",
|
|
13
13
|
purpose: "Issue and project planning for the platform team",
|
|
14
|
+
access: "read-write",
|
|
14
15
|
instructions: "File bugs into the Platform team unless the request names another.",
|
|
15
16
|
});
|
|
16
17
|
```
|
|
@@ -23,13 +24,21 @@ cannot change the connector's safety classification.
|
|
|
23
24
|
|
|
24
25
|
## Access modes
|
|
25
26
|
|
|
26
|
-
Linear publishes two hosted endpoints, and `access` selects between them
|
|
27
|
+
Linear publishes two hosted endpoints, and `access` selects between them. It is
|
|
28
|
+
**required**, with no default:
|
|
27
29
|
|
|
28
30
|
| `access` | Endpoint | OAuth scopes |
|
|
29
31
|
| --- | --- | --- |
|
|
30
|
-
| `"read-write"`
|
|
32
|
+
| `"read-write"` | `https://mcp.linear.app/mcp` | `read`, `write` |
|
|
31
33
|
| `"read-only"` | `https://mcp.linear.app/mcp/readonly` | `read` |
|
|
32
34
|
|
|
35
|
+
There is no safe default to pick. Defaulting to `"read-write"` hands a
|
|
36
|
+
deployment write access it never asked for; defaulting to `"read-only"` turns a
|
|
37
|
+
deployment that does write into one whose every write fails at Linear — at
|
|
38
|
+
runtime, where no agent can repair it. So the operator declares which one this
|
|
39
|
+
is, and a deployment that forgot fails at construction with a message naming
|
|
40
|
+
both options.
|
|
41
|
+
|
|
33
42
|
Read-only is not a client-side filter. The endpoint advertises the `read` scope
|
|
34
43
|
alone, so the token minted for it cannot reach Linear's write APIs — a stronger
|
|
35
44
|
guarantee than any annotation Connecta applies. A deployment that only reports
|
|
@@ -63,6 +72,7 @@ also accepts a bearer token or a personal API key passed directly in the
|
|
|
63
72
|
```ts
|
|
64
73
|
linear("automation_tracker", {
|
|
65
74
|
purpose: "Headless release reporting",
|
|
75
|
+
access: "read-only",
|
|
66
76
|
auth: {
|
|
67
77
|
type: "headers",
|
|
68
78
|
headers: { Authorization: env.LINEAR_API_KEY },
|
|
@@ -130,6 +140,7 @@ one. An operator who knows their workspace can supply one explicitly:
|
|
|
130
140
|
```ts
|
|
131
141
|
linear("product_tracker", {
|
|
132
142
|
purpose: "Issue and project planning for the platform team",
|
|
143
|
+
access: "read-write",
|
|
133
144
|
callAdmission: {
|
|
134
145
|
rules: [
|
|
135
146
|
{ budget: { kind: "rolling-window", maxCalls: 1_000, windowMs: 3_600_000 } },
|
|
@@ -141,3 +152,10 @@ linear("product_tracker", {
|
|
|
141
152
|
A budget-only rule needs no queue. If you add `maxConcurrency` you are asking
|
|
142
153
|
for a queue, and the admission controller then requires the rest of the queue
|
|
143
154
|
settings at construction.
|
|
155
|
+
|
|
156
|
+
## Conventions
|
|
157
|
+
|
|
158
|
+
This connection is audited against
|
|
159
|
+
[the provider conventions](./provider-conventions.md). Its verdict per
|
|
160
|
+
convention, including every recorded exception, is the Linear section of
|
|
161
|
+
[the provider audit](./provider-audit.md).
|