@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.
Files changed (67) hide show
  1. package/AGENTS.md +18 -20
  2. package/CHANGELOG.md +64 -1
  3. package/README.md +5 -6
  4. package/dist/branding.d.ts +31 -2
  5. package/dist/branding.js +116 -8
  6. package/dist/connectors/api.d.ts +1 -1
  7. package/dist/connectors/api.js +10 -2
  8. package/dist/connectors/guarded-fetch.d.ts +5 -1
  9. package/dist/connectors/guarded-fetch.js +34 -4
  10. package/dist/connectors/remote-mcp.js +8 -4
  11. package/dist/errors.d.ts +11 -3
  12. package/dist/errors.js +2 -1
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.js +12 -1
  15. package/dist/meta-tools.js +105 -29
  16. package/dist/operator-ui/generated.js +2 -2
  17. package/dist/operator-ui/view.d.ts +38 -1
  18. package/dist/operator-ui/view.js +71 -0
  19. package/dist/providers/cloudflare.d.ts +14 -2
  20. package/dist/providers/cloudflare.js +107 -16
  21. package/dist/providers/linear.d.ts +26 -4
  22. package/dist/providers/linear.js +19 -4
  23. package/dist/providers/mixpanel.d.ts +16 -3
  24. package/dist/providers/mixpanel.js +13 -2
  25. package/dist/providers/notion.d.ts +8 -1
  26. package/dist/providers/notion.js +83 -10
  27. package/dist/providers/revenuecat.d.ts +30 -4
  28. package/dist/providers/revenuecat.js +42 -4
  29. package/dist/providers/stripe.d.ts +7 -1
  30. package/dist/providers/stripe.js +30 -4
  31. package/dist/providers/vercel.js +11 -1
  32. package/dist/registry.d.ts +12 -4
  33. package/dist/registry.js +22 -8
  34. package/dist/types.d.ts +37 -0
  35. package/dist/ui.js +18 -10
  36. package/dist/version.d.ts +1 -1
  37. package/dist/version.js +1 -1
  38. package/documentation/architecture.md +193 -181
  39. package/documentation/auth.md +197 -176
  40. package/documentation/code-mode.md +426 -321
  41. package/documentation/meta-tools.md +356 -416
  42. package/examples/worker/AGENTS.md +2 -1
  43. package/examples/worker/README.md +12 -10
  44. package/examples/worker/src/index.ts +12 -15
  45. package/package.json +1 -2
  46. package/templates/node/.env.example +3 -3
  47. package/templates/node/AGENTS.md +5 -4
  48. package/templates/node/README.md +2 -1
  49. package/templates/node/package.json +1 -1
  50. package/templates/node/src/index.ts +23 -22
  51. package/documentation/call-admission.md +0 -158
  52. package/documentation/cloudflare.md +0 -471
  53. package/documentation/connector-guides.md +0 -176
  54. package/documentation/connectors.md +0 -431
  55. package/documentation/linear.md +0 -193
  56. package/documentation/mixpanel.md +0 -160
  57. package/documentation/notion.md +0 -308
  58. package/documentation/operations.md +0 -359
  59. package/documentation/operator-ui.md +0 -135
  60. package/documentation/optional-modules-upgrade.md +0 -243
  61. package/documentation/provider-conventions.md +0 -729
  62. package/documentation/request-admission.md +0 -204
  63. package/documentation/revenuecat.md +0 -305
  64. package/documentation/storage-and-credentials.md +0 -254
  65. package/documentation/stripe.md +0 -262
  66. package/documentation/upgrading.md +0 -768
  67. 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
- - Run the repository's `npm run check:examples` after configuration changes.
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. See [the Cloudflare guide](../../documentation/cloudflare.md).
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, credential rotation, or access-token issuance/revocation must
27
- become globally visible immediately.
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 })`. Omit the import and option to
148
- serve no UI routes; OAuth callbacks still work in core for authorized
149
- interactive MCP callers.
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. See the [migration guide](../../documentation/upgrading.md#0240-optional-modules)
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 — see
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, the credential
15
- * vault, and access-token issuance are on, and activity is three commented
16
- * lines below. README.md § "The operator surface" walks through all four.
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 at /credentials, encrypted
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 page: /credentials is a list of connector
99
- // slots, so it stays hidden until a connector declares one. Neither
100
- // connector below does — Notion here carries a deployment-owned static
101
- // header and echo has no secret at all — so this example ships the vault
102
- // ready and the page empty. Declare a slot (see the commented shape on
103
- // `echo`, or use a provider connector like `notion()`, which declares its
104
- // own) and the page appears on the next load.
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",
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 credential
27
- # or issue an access token; the bearer above is a client key, not an admin key.
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 at /credentials. Generate one with:
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.
@@ -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. Follow
31
- [the upgrade guide](https://github.com/zackbart/connecta/blob/main/documentation/upgrading.md),
32
- which also ships at `node_modules/@zackbart/connecta/documentation/upgrading.md`.
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
@@ -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)`. Set
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
@@ -15,7 +15,7 @@
15
15
  "typecheck": "tsc --noEmit"
16
16
  },
17
17
  "dependencies": {
18
- "@zackbart/connecta": "0.24.3",
18
+ "@zackbart/connecta": "0.24.4",
19
19
  "quickjs-emscripten": "0.32.0"
20
20
  },
21
21
  "devDependencies": {
@@ -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 operator surface — sign-in, credential vault, access tokens, activity —
13
- * ships here as commented configuration, because each half needs a secret or a
14
- * retention decision this file cannot make for you. Uncomment the block you
15
- * want; README.md § "Turn on the operator surface" walks through all four.
12
+ * The optional modulesoperator 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, and only an operator may issue an access token.
51
- // Without this block the operator pages still renderan operator pastes the
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 itand 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: fileStorage(stateFile),
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
- // Optional member/operator split for Clerk-backed Docker deployments.
75
- // Connector access is derived from the authenticated identity and cannot be
76
- // selected by an MCP argument. Omit this block for the legacy all-visible,
77
- // all-interactive-users-are-operators behavior.
78
- // identity: {
79
- // connectorAccess: ({ principal }) =>
80
- // principal?.id === "user_admin" ? "all" : ["time"],
81
- // activityAccess: ({ id }) => id === "user_admin",
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 at /credentials, and its value is encrypted in the state file
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
- // /credentials lists connectors, not deployments: the page stays hidden
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) |