@zackbart/connecta 0.24.3 → 0.24.4
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 +18 -20
- package/CHANGELOG.md +64 -1
- package/README.md +5 -6
- package/dist/branding.d.ts +31 -2
- package/dist/branding.js +116 -8
- package/dist/connectors/api.d.ts +1 -1
- package/dist/connectors/api.js +10 -2
- package/dist/connectors/guarded-fetch.d.ts +5 -1
- package/dist/connectors/guarded-fetch.js +34 -4
- package/dist/connectors/remote-mcp.js +8 -4
- package/dist/errors.d.ts +11 -3
- package/dist/errors.js +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +12 -1
- package/dist/meta-tools.js +105 -29
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/view.d.ts +38 -1
- package/dist/operator-ui/view.js +71 -0
- package/dist/providers/cloudflare.d.ts +14 -2
- package/dist/providers/cloudflare.js +107 -16
- package/dist/providers/linear.d.ts +26 -4
- package/dist/providers/linear.js +19 -4
- package/dist/providers/mixpanel.d.ts +16 -3
- package/dist/providers/mixpanel.js +13 -2
- package/dist/providers/notion.d.ts +8 -1
- package/dist/providers/notion.js +83 -10
- package/dist/providers/revenuecat.d.ts +30 -4
- package/dist/providers/revenuecat.js +42 -4
- package/dist/providers/stripe.d.ts +7 -1
- package/dist/providers/stripe.js +30 -4
- package/dist/providers/vercel.js +11 -1
- package/dist/registry.d.ts +12 -4
- package/dist/registry.js +22 -8
- package/dist/types.d.ts +37 -0
- package/dist/ui.js +18 -10
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +193 -181
- package/documentation/auth.md +197 -176
- package/documentation/code-mode.md +426 -321
- package/documentation/meta-tools.md +356 -416
- package/examples/worker/AGENTS.md +2 -1
- package/examples/worker/README.md +12 -10
- package/examples/worker/src/index.ts +12 -15
- package/package.json +1 -2
- package/templates/node/.env.example +3 -3
- package/templates/node/AGENTS.md +5 -4
- package/templates/node/README.md +2 -1
- package/templates/node/package.json +1 -1
- package/templates/node/src/index.ts +23 -22
- package/documentation/call-admission.md +0 -158
- package/documentation/cloudflare.md +0 -471
- package/documentation/connector-guides.md +0 -176
- package/documentation/connectors.md +0 -431
- package/documentation/linear.md +0 -193
- package/documentation/mixpanel.md +0 -160
- package/documentation/notion.md +0 -308
- package/documentation/operations.md +0 -359
- package/documentation/operator-ui.md +0 -135
- package/documentation/optional-modules-upgrade.md +0 -243
- package/documentation/provider-conventions.md +0 -729
- package/documentation/request-admission.md +0 -204
- package/documentation/revenuecat.md +0 -305
- package/documentation/storage-and-credentials.md +0 -254
- package/documentation/stripe.md +0 -262
- package/documentation/upgrading.md +0 -768
- package/documentation/vercel.md +0 -241
|
@@ -37,7 +37,8 @@ This repository is deployment configuration, not a copy of Connecta itself.
|
|
|
37
37
|
A configured Connecta bearer cannot cross the Access edge alone. Connecta-issued
|
|
38
38
|
tokens are removed. UI, vault, and activity use explicit optional imports;
|
|
39
39
|
auth changes require code-derived shared or personal management permissions.
|
|
40
|
-
-
|
|
40
|
+
- After configuration changes, typecheck: inside the connecta repository that
|
|
41
|
+
is `npm run check:examples`; a copied deployment runs its own `tsc --noEmit`.
|
|
41
42
|
After deployment, connect both Claude and ChatGPT to `<PUBLIC_URL>/mcp` and
|
|
42
43
|
complete their browser authorization flows before calling setup complete.
|
|
43
44
|
|
|
@@ -8,7 +8,7 @@ Workers Paid plan.
|
|
|
8
8
|
This is also the **starting template for a deployment**: a real deployment
|
|
9
9
|
should be its own repository that pins an exact `@zackbart/connecta` version and
|
|
10
10
|
owns only its connector configuration, auth policy, domain, bindings,
|
|
11
|
-
migrations, and secrets.
|
|
11
|
+
migrations, and secrets.
|
|
12
12
|
|
|
13
13
|
## Files
|
|
14
14
|
|
|
@@ -17,14 +17,15 @@ migrations, and secrets. See [the Cloudflare guide](../../documentation/cloudfla
|
|
|
17
17
|
| `src/index.ts` | the Worker entrypoint — connector and auth configuration |
|
|
18
18
|
| `src/cloudflare-kv.ts` | `KVStorage` over Workers KV (deployment-owned, not a package export) |
|
|
19
19
|
| `src/d1-activity.ts` | `ActivityStore` over D1 (deployment-owned; see below) |
|
|
20
|
+
| `src/d1-activity-row.ts` | the row ↔ event mapping `d1-activity.ts` uses, including friction derived from `error_code` for rows written before that column existed |
|
|
20
21
|
| `wrangler.jsonc` | Worker name, vars, bindings, `compatibility_flags` |
|
|
21
22
|
|
|
22
23
|
`cloudflare-kv.ts` and `d1-activity.ts` deliberately live here rather than in
|
|
23
24
|
the package: storage backends are deployment-owned, so the package ships only
|
|
24
25
|
the generic `KVStorage` and `ActivityStore` contracts. Workers KV is eventually
|
|
25
26
|
consistent across locations; use a strongly consistent `KVStorage` adapter when
|
|
26
|
-
OAuth disconnect
|
|
27
|
-
|
|
27
|
+
OAuth disconnect or credential rotation must become globally visible
|
|
28
|
+
immediately.
|
|
28
29
|
|
|
29
30
|
## Deploy
|
|
30
31
|
|
|
@@ -144,9 +145,12 @@ See [inbound identity](../../documentation/auth.md#principals-visibility-and-ope
|
|
|
144
145
|
### UI and encrypted credentials
|
|
145
146
|
|
|
146
147
|
Import `operatorUi` from `@zackbart/connecta/ui` and set `ui: operatorUi()`.
|
|
147
|
-
Branding belongs in `operatorUi({ branding })
|
|
148
|
-
|
|
149
|
-
|
|
148
|
+
Branding belongs in `operatorUi({ branding })`, including `branding.theme`:
|
|
149
|
+
`accent`, `radius`, `fontFamily`, `monoFamily`, and `colorScheme`. Every other
|
|
150
|
+
color is mixed from those, so one accent themes the whole page. A value that
|
|
151
|
+
fails its gate falls back to the default, and the startup warning names it. Omit
|
|
152
|
+
the import and option to serve no UI routes; OAuth callbacks still work in core
|
|
153
|
+
for authorized interactive MCP callers.
|
|
150
154
|
|
|
151
155
|
Import `encryptedCredentialVault` from `@zackbart/connecta/credentials` and set
|
|
152
156
|
`vault: encryptedCredentialVault(storage, env.CREDENTIAL_ENCRYPTION_KEY)` when
|
|
@@ -172,8 +176,7 @@ does not create credentials or permissions by itself.
|
|
|
172
176
|
Interactive MCP clients use Access Managed OAuth. Unattended clients use Access
|
|
173
177
|
service tokens when needed. Connecta-issued `cta_` tokens and their management
|
|
174
178
|
routes are removed; a configured Connecta bearer cannot cross the Access edge
|
|
175
|
-
alone.
|
|
176
|
-
if an older deployment still issues tokens.
|
|
179
|
+
alone.
|
|
177
180
|
|
|
178
181
|
Activity uses `activityHistory({ store: d1ActivityStore(env.ACTIVITY_DB) })`
|
|
179
182
|
from `@zackbart/connecta/activity`. Enable the database and bindings described
|
|
@@ -295,8 +298,7 @@ To enable it:
|
|
|
295
298
|
});
|
|
296
299
|
```
|
|
297
300
|
|
|
298
|
-
Events carry no arguments, results, generated code, or raw error messages
|
|
299
|
-
[activity history](../../documentation/operator-ui.md).
|
|
301
|
+
Events carry no arguments, results, generated code, or raw error messages.
|
|
300
302
|
The Worker entrypoint already forwards `ctx` to `connecta.fetch`, which lets
|
|
301
303
|
async activity writes settle on `waitUntil`.
|
|
302
304
|
|
|
@@ -11,9 +11,9 @@ import { encryptedCredentialVault } from "@zackbart/connecta/credentials";
|
|
|
11
11
|
* wrangler.jsonc backs the seven-tool surface.
|
|
12
12
|
*
|
|
13
13
|
* The operator surface is wired here except for activity history, which needs
|
|
14
|
-
* a database this example does not create for you: sign-in
|
|
15
|
-
* vault
|
|
16
|
-
*
|
|
14
|
+
* a database this example does not create for you: sign-in and the credential
|
|
15
|
+
* vault are on, and activity is three commented lines below. README.md
|
|
16
|
+
* § "Select optional modules" walks through all three.
|
|
17
17
|
*
|
|
18
18
|
* Setup (this example has no package.json of its own — it self-references the
|
|
19
19
|
* installed `@zackbart/connecta` package):
|
|
@@ -90,22 +90,19 @@ function build(env: Env) {
|
|
|
90
90
|
// activityAccess: ({ id }) => id === "ACCESS_USER_UUID",
|
|
91
91
|
// },
|
|
92
92
|
// Connectors that declare a `credential` slot become editable by every
|
|
93
|
-
// signed-in human who can see that connector
|
|
94
|
-
// with this key before anything reaches KV. A saved replacement takes
|
|
93
|
+
// signed-in human who can see that connector, inside its connection on /,
|
|
94
|
+
// encrypted with this key before anything reaches KV. A saved replacement takes
|
|
95
95
|
// effect on the next call — no redeploy, and no liveness probe:
|
|
96
96
|
// credentials fail at use.
|
|
97
97
|
//
|
|
98
|
-
// The key is the vault, not the
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
98
|
+
// The key is the vault, not the form: a credential form appears only on a
|
|
99
|
+
// connector that declares a slot. Neither connector below does — Notion
|
|
100
|
+
// here carries a deployment-owned static header and echo has no secret at
|
|
101
|
+
// all — so this example ships the vault ready and nothing to fill in.
|
|
102
|
+
// Declare a slot (see the commented shape on `echo`, or use a provider
|
|
103
|
+
// connector like `notion()`, which declares its own) and the form appears
|
|
104
|
+
// on the next load.
|
|
105
105
|
vault: encryptedCredentialVault(storage, env.CREDENTIAL_ENCRYPTION_KEY),
|
|
106
|
-
// Eligible human operators can create named, revocable MCP Bearer tokens
|
|
107
|
-
// at /tokens. Under Worker-level Access those tokens are a rollback tool,
|
|
108
|
-
// not standalone edge credentials: Access still runs before connecta.
|
|
109
106
|
ui: operatorUi(),
|
|
110
107
|
identity: { credentialAdministration: () => "all", personalConnection: () => "all" },
|
|
111
108
|
// Payload-free activity at /activity, off until a database exists to hold
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zackbart/connecta",
|
|
3
|
-
"version": "0.24.
|
|
3
|
+
"version": "0.24.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "One MCP to rule them all — a single MCP endpoint aggregating many downstream connectors behind a code-first surface of seven meta-tools.",
|
|
@@ -129,7 +129,6 @@
|
|
|
129
129
|
"check:examples": "tsc -p tsconfig.template.node.json && tsc -p tsconfig.examples.worker.json",
|
|
130
130
|
"check:security": "npm audit --omit=dev --audit-level=moderate",
|
|
131
131
|
"check:package": "node scripts/check-package.mjs",
|
|
132
|
-
"load:admission": "npm run build && node eval/admission-load.mjs",
|
|
133
132
|
"providers:check": "tsx scripts/drift-check.mjs --specs --docs",
|
|
134
133
|
"drift:check": "tsx scripts/drift-check.mjs",
|
|
135
134
|
"check": "npm run check:docs && npm run check:operator-ui && npm run check:lint && npm run check:unused && npm run typecheck && npm run test && npm run build && npm run check:examples",
|
|
@@ -23,14 +23,14 @@ CONNECTA_STATE_FILE=
|
|
|
23
23
|
# Each of these is read only once the matching block in src/index.ts is
|
|
24
24
|
# uncommented. Setting one on its own changes nothing.
|
|
25
25
|
|
|
26
|
-
# Operator sign-in with Clerk. Only a signed-in operator may write a
|
|
27
|
-
#
|
|
26
|
+
# Operator sign-in with Clerk. Only a signed-in operator may write a
|
|
27
|
+
# credential; the bearer above is a client key, not an admin key.
|
|
28
28
|
# Also run `npm install @clerk/backend` — it is an optional peer.
|
|
29
29
|
CLERK_PUBLISHABLE_KEY=
|
|
30
30
|
CLERK_SECRET_KEY=
|
|
31
31
|
|
|
32
32
|
# Credential vault. Base64-encoded 32-byte AES key encrypting the values an
|
|
33
|
-
# operator saves
|
|
33
|
+
# operator saves inside a connection on /. Generate one with:
|
|
34
34
|
# node -e "console.log(crypto.randomBytes(32).toString('base64'))"
|
|
35
35
|
# Keep it here or in a secret store — never in the state file it protects, and
|
|
36
36
|
# never in source control. Losing it makes stored credentials unreadable.
|
package/templates/node/AGENTS.md
CHANGED
|
@@ -19,7 +19,6 @@ This repository is deployment configuration, not a copy of Connecta itself.
|
|
|
19
19
|
alone never grants it. Configured bearer auth is a client option, not a human
|
|
20
20
|
management identity. Connecta-issued access tokens are removed.
|
|
21
21
|
`src/file-activity.ts` remains the deployment-owned history store.
|
|
22
|
-
|
|
23
22
|
- Run `npm run typecheck` after configuration changes. With the server running,
|
|
24
23
|
run `CONNECTA_TOKEN=... npm run doctor` before calling setup complete.
|
|
25
24
|
- `Dockerfile` and `docker-compose.yml` containerize *this* source; they are
|
|
@@ -27,9 +26,11 @@ This repository is deployment configuration, not a copy of Connecta itself.
|
|
|
27
26
|
`src/index.ts`, never in a divergent container entrypoint.
|
|
28
27
|
- Moving this deployment to a newer Connecta is its own procedure, and it is
|
|
29
28
|
not a re-`init` — `connecta init` refuses to merge into an existing path on
|
|
30
|
-
purpose.
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
purpose. Bump the exact `@zackbart/connecta` pin in `package.json`, then read
|
|
30
|
+
the [changelog](https://github.com/zackbart/connecta/blob/main/CHANGELOG.md)
|
|
31
|
+
for every release in between — it also ships at
|
|
32
|
+
`node_modules/@zackbart/connecta/CHANGELOG.md`. Each release opens with what
|
|
33
|
+
breaks and what a deployment can ignore.
|
|
33
34
|
|
|
34
35
|
Do not add alternate entrypoints, policy layers, generated connector catalogs,
|
|
35
36
|
or runtime connector registration. Keep the deployment small enough to review
|
package/templates/node/README.md
CHANGED
|
@@ -74,7 +74,8 @@ connector where each person should connect their own downstream account.
|
|
|
74
74
|
### Credential vault
|
|
75
75
|
|
|
76
76
|
Import `encryptedCredentialVault` from `@zackbart/connecta/credentials`, then
|
|
77
|
-
set `vault: encryptedCredentialVault(storage, credentialKey)
|
|
77
|
+
set `vault: encryptedCredentialVault(storage, credentialKey)` — `storage` is
|
|
78
|
+
the `fileStorage` binding `src/index.ts` already passes to `createConnecta`. Set
|
|
78
79
|
`CONNECTA_CREDENTIAL_KEY` to a base64 32-byte AES key:
|
|
79
80
|
|
|
80
81
|
```sh
|
|
@@ -9,10 +9,10 @@ import { operatorUi } from "@zackbart/connecta/ui";
|
|
|
9
9
|
* storage, and public origin. Add application logic only inside deliberate
|
|
10
10
|
* api() connector handlers.
|
|
11
11
|
*
|
|
12
|
-
* The
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* The optional modules — operator sign-in, credential vault, activity — ship
|
|
13
|
+
* here as commented configuration, because each needs a secret or a retention
|
|
14
|
+
* decision this file cannot make for you. Uncomment the block you want;
|
|
15
|
+
* README.md § "Select optional modules" walks through all three.
|
|
16
16
|
*
|
|
17
17
|
* Environment (see .env.example):
|
|
18
18
|
* CONNECTA_TOKEN required inbound bearer token
|
|
@@ -44,12 +44,12 @@ const port = Number(process.env.PORT ?? 8787);
|
|
|
44
44
|
// empty state path or public origin is worse than the local default.
|
|
45
45
|
const stateFile = process.env.CONNECTA_STATE_FILE || "./.connecta-state.json";
|
|
46
46
|
const publicUrl = process.env.PUBLIC_URL || `http://localhost:${port}`;
|
|
47
|
+
const storage = fileStorage(stateFile);
|
|
47
48
|
|
|
48
49
|
// Operator sign-in. A bearer token is a client key: it may call tools and read
|
|
49
50
|
// connector status, but only a Clerk-authenticated human may write a visible
|
|
50
|
-
// connector's credential
|
|
51
|
-
//
|
|
52
|
-
// bearer to read them — and Credentials and Tokens stay read-only.
|
|
51
|
+
// connector's credential. Without this block the operator UI still renders —
|
|
52
|
+
// an operator pastes the bearer to read it — and connections stay read-only.
|
|
53
53
|
// const clerkPublishableKey = process.env.CLERK_PUBLISHABLE_KEY;
|
|
54
54
|
// const clerkSecretKey = process.env.CLERK_SECRET_KEY;
|
|
55
55
|
// if (!clerkPublishableKey || !clerkSecretKey) {
|
|
@@ -59,7 +59,7 @@ const publicUrl = process.env.PUBLIC_URL || `http://localhost:${port}`;
|
|
|
59
59
|
// }
|
|
60
60
|
|
|
61
61
|
const connecta = createConnecta({
|
|
62
|
-
storage
|
|
62
|
+
storage,
|
|
63
63
|
auth: [
|
|
64
64
|
bearerToken(token, { subjectId: "operator" }),
|
|
65
65
|
// clerkAuth({
|
|
@@ -71,20 +71,24 @@ const connecta = createConnecta({
|
|
|
71
71
|
// // allowedDomains: ["acme.com"],
|
|
72
72
|
// }),
|
|
73
73
|
],
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
74
|
+
// Code-owned identity resolvers. The two management permissions default to
|
|
75
|
+
// none, so the template grants them. The commented pair is the optional
|
|
76
|
+
// member/operator split for Clerk-backed deployments: connector access is
|
|
77
|
+
// derived from the authenticated identity and cannot be selected by an MCP
|
|
78
|
+
// argument. Leave them commented for the all-visible,
|
|
79
|
+
// all-interactive-users-read-activity behavior.
|
|
80
|
+
identity: {
|
|
81
|
+
credentialAdministration: () => "all",
|
|
82
|
+
personalConnection: () => "all",
|
|
83
|
+
// connectorAccess: ({ principal }) =>
|
|
84
|
+
// principal?.id === "user_admin" ? "all" : ["time"],
|
|
85
|
+
// activityAccess: ({ id }) => id === "user_admin",
|
|
86
|
+
},
|
|
83
87
|
publicUrl,
|
|
84
88
|
// Required: model-written programs run in a bounded QuickJS child.
|
|
85
89
|
executor: quickJsExecutor(),
|
|
86
90
|
// Credential vault. A connector that declares a `credential` slot becomes
|
|
87
|
-
// editable
|
|
91
|
+
// editable inside its connection on /, and its value is encrypted in the state file
|
|
88
92
|
// with this key — so keep the key out of that file and out of source:
|
|
89
93
|
// node -e "console.log(crypto.randomBytes(32).toString('base64'))"
|
|
90
94
|
// Rotating a credential takes effect on the next call; no restart.
|
|
@@ -100,13 +104,10 @@ const connecta = createConnecta({
|
|
|
100
104
|
// deploymentId: "production",
|
|
101
105
|
// }),
|
|
102
106
|
ui: operatorUi(),
|
|
103
|
-
identity: { credentialAdministration: () => "all", personalConnection: () => "all" },
|
|
104
107
|
connectors: [
|
|
105
108
|
api("time", {
|
|
106
109
|
description: "Time — current timestamp",
|
|
107
|
-
//
|
|
108
|
-
// until at least one connector declares a slot, vault or no vault. A
|
|
109
|
-
// connector that needs an operator-managed secret adds one here —
|
|
110
|
+
// A connector that needs an operator-managed secret declares a slot —
|
|
110
111
|
// credential: { label: "API token" },
|
|
111
112
|
// — and reads it inside a handler with `await ctx.credential?.get()`.
|
|
112
113
|
// Telling the time needs no secret, so this one declares nothing.
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
# Downstream call admission
|
|
2
|
-
|
|
3
|
-
One admitted MCP request can fan out into many downstream calls. A program
|
|
4
|
-
calls tools from loops, from parallel `connecta.call` calls, 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 program calls through `connecta.call`,
|
|
80
|
-
reach that same seam, so a program cannot buy itself a
|
|
81
|
-
second limit by taking the other route.
|
|
82
|
-
|
|
83
|
-
- **A caller retry is another call.** It reacquires and can consume another
|
|
84
|
-
budget entry. Connecta returns retry hints without waiting or retrying.
|
|
85
|
-
- **A proactive short-window `rate_limited` refusal** returns its retry hint
|
|
86
|
-
without waiting or dispatching. Activity records one failed admission attempt;
|
|
87
|
-
a caller may reissue after the window.
|
|
88
|
-
- **A queued cancellation consumes nothing.** It is removed from the queue with
|
|
89
|
-
no rolling-window entry charged.
|
|
90
|
-
- **Caller cancellation is terminal.** It is non-retryable, releases its
|
|
91
|
-
permit, records a `cancelled` activity outcome, and is not connector-health
|
|
92
|
-
evidence.
|
|
93
|
-
- **Budget exhaustion fails immediately** as `rate_limited`, `retryable: true`,
|
|
94
|
-
with the exact `retryAfterMs` to the next free slot. Concurrency overflow
|
|
95
|
-
uses the configured hint instead, because there is no exact answer.
|
|
96
|
-
- **Local refusals are not connector health failures.** They are activity
|
|
97
|
-
errors, but no provider call happened, so poisoning the connector's health
|
|
98
|
-
with them would report the limiter's success as the downstream's failure.
|
|
99
|
-
`isCallAdmissionError` is what keeps `recordFailure` out of that path.
|
|
100
|
-
|
|
101
|
-
Only tool execution is covered. Catalog `listTools`, status probes, credential
|
|
102
|
-
checks, and authorization operations stay outside the budget: they are not the
|
|
103
|
-
calls a provider is rate-limiting, and charging discovery for them would make
|
|
104
|
-
a program's first search cost it capacity to act.
|
|
105
|
-
|
|
106
|
-
`queueTimeoutMs` bounds only the wait for a permit. The per-call `timeoutMs`
|
|
107
|
-
is one deadline over catalog resolution, the permit wait, and the connector
|
|
108
|
-
call together, so a saturated call can never take longer than `timeoutMs`;
|
|
109
|
-
the queue timeout may only end it sooner. 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
|
-
Shared connectors use the root registry's controller. Personal connectors use
|
|
117
|
-
the owning principal's registry, so two accounts each receive their own budget;
|
|
118
|
-
all requests and call paths for one principal still share it. `close()` rejects
|
|
119
|
-
queued and future admissions on both kinds of controller, including a personal
|
|
120
|
-
registry constructed after shutdown.
|
|
121
|
-
|
|
122
|
-
The principal cache is bounded at 1,024 registries. Eviction closes an idle
|
|
123
|
-
registry so an older request view cannot use an orphaned controller. A registry
|
|
124
|
-
with active calls, queued calls, or unexpired rolling entries is never evicted.
|
|
125
|
-
If all 1,024 are occupied, a new principal view fails closed with HTTP 403 until
|
|
126
|
-
one drains. Cache churn cannot reset a live account's quota.
|
|
127
|
-
|
|
128
|
-
This is deliberately **per-runtime**. It completely contains fan-out inside one
|
|
129
|
-
request, including parallel `connecta.call` calls in one Worker isolate. A rolling
|
|
130
|
-
budget is exact inside one Node process or Worker isolate, and best-effort
|
|
131
|
-
across isolates, replicas, and restarts.
|
|
132
|
-
|
|
133
|
-
KV cannot coordinate the invariant without atomic operations, and this release
|
|
134
|
-
adds no Durable Object or other distributed coordinator. The async typed permit
|
|
135
|
-
is the seam a coordinator would slot into later; until then the guide says what
|
|
136
|
-
the bound actually is rather than implying a global one.
|
|
137
|
-
|
|
138
|
-
## Observations
|
|
139
|
-
|
|
140
|
-
`/health` exposes one payload-free aggregate at
|
|
141
|
-
`admission.downstreamCalls.aggregate`, summed across root and retained personal
|
|
142
|
-
controllers: rule and retained partition counts, current
|
|
143
|
-
active and queued gauges, cumulative admitted/queued/rejected/rate-limited/
|
|
144
|
-
cancelled counts, and queue-wait count, total, and maximum. The open endpoint
|
|
145
|
-
never exposes connector ids, principal ids, partition keys, tool arguments, or results — a partition key can
|
|
146
|
-
be a customer identifier, which is precisely why it stays out of an unauthenticated
|
|
147
|
-
payload. Queue-wait maxima take the maximum, other counters sum, and `closed`
|
|
148
|
-
means every included controller is closed. Evicted idle controllers no longer
|
|
149
|
-
contribute to these runtime snapshots. Ordinary payload-free activity records the final call outcome and its
|
|
150
|
-
typed error code.
|
|
151
|
-
|
|
152
|
-
## Tests that enforce this
|
|
153
|
-
|
|
154
|
-
| Invariant | Suite |
|
|
155
|
-
| --- | --- |
|
|
156
|
-
| 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) |
|
|
157
|
-
| Shared and personal limiters shared by direct and program calls, independent principal budgets, shutdown covering both, eviction preserving live budgets, promise concurrency with input order preserved, cancellation threading, no dispatch or retry or health poisoning after cancellation, retry hints returned without waiting or poisoning health, payload-free `/health` aggregates | `test/call-admission.test.ts` (integration, Node + Workers) |
|
|
158
|
-
| 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](https://github.com/zackbart/connecta/blob/main/records/provider-audit.md) |
|