@zackbart/connecta 0.24.3 → 0.25.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.
Files changed (145) hide show
  1. package/AGENTS.md +58 -30
  2. package/CHANGELOG.md +317 -1
  3. package/README.md +5 -6
  4. package/dist/advertised-schema.js +53 -0
  5. package/dist/auth/downstream-oauth.js +490 -106
  6. package/dist/branding.d.ts +31 -2
  7. package/dist/branding.js +116 -8
  8. package/dist/call-admission.js +139 -113
  9. package/dist/catalog-service.js +141 -105
  10. package/dist/catalog.js +258 -33
  11. package/dist/concurrency.js +24 -23
  12. package/dist/connector-scope.js +6 -37
  13. package/dist/connectors/api.d.ts +3 -3
  14. package/dist/connectors/api.js +10 -2
  15. package/dist/connectors/guarded-fetch.js +34 -4
  16. package/dist/connectors/remote-mcp.d.ts +6 -5
  17. package/dist/connectors/remote-mcp.js +325 -245
  18. package/dist/credential-contract.d.ts +9 -0
  19. package/dist/credential-rules.js +2 -2
  20. package/dist/credentials.d.ts +6 -1
  21. package/dist/credentials.js +45 -3
  22. package/dist/errors.d.ts +11 -3
  23. package/dist/errors.js +2 -1
  24. package/dist/execute.js +363 -257
  25. package/dist/executor-admission.js +67 -50
  26. package/dist/executors/quickjs-runtime.js +4 -2
  27. package/dist/executors/quickjs.js +251 -210
  28. package/dist/fix-prompt.js +37 -0
  29. package/dist/index.d.ts +11 -2
  30. package/dist/index.js +63 -21
  31. package/dist/invocation.js +324 -285
  32. package/dist/meta-tools.js +304 -114
  33. package/dist/oauth-callback-outcome.js +94 -0
  34. package/dist/oauth-sealing.js +29 -0
  35. package/dist/operator-ui/fix-prompts.js +87 -0
  36. package/dist/operator-ui/generated.js +2 -2
  37. package/dist/operator-ui/model.d.ts +46 -1
  38. package/dist/operator-ui/setup-commands.js +53 -0
  39. package/dist/operator-ui/view.js +109 -2
  40. package/dist/program-source.js +205 -0
  41. package/dist/providers/cloudflare.d.ts +14 -2
  42. package/dist/providers/cloudflare.js +107 -16
  43. package/dist/providers/linear.d.ts +26 -4
  44. package/dist/providers/linear.js +19 -4
  45. package/dist/providers/mixpanel.d.ts +16 -3
  46. package/dist/providers/mixpanel.js +13 -2
  47. package/dist/providers/notion.d.ts +8 -1
  48. package/dist/providers/notion.js +85 -12
  49. package/dist/providers/revenuecat.d.ts +30 -4
  50. package/dist/providers/revenuecat.js +70 -5
  51. package/dist/providers/stripe.d.ts +10 -3
  52. package/dist/providers/stripe.js +30 -4
  53. package/dist/providers/vercel.js +12 -2
  54. package/dist/registry.d.ts +13 -5
  55. package/dist/registry.js +317 -258
  56. package/dist/routes/activity.js +120 -137
  57. package/dist/routes/credentials.js +174 -163
  58. package/dist/routes/mcp.js +129 -70
  59. package/dist/routes/oauth-management.js +60 -71
  60. package/dist/routes/oauth.js +44 -16
  61. package/dist/routes/operator.js +82 -0
  62. package/dist/routes/shared.d.ts +2 -0
  63. package/dist/routes/ui-data.js +210 -0
  64. package/dist/routes/ui.js +64 -32
  65. package/dist/runtime/admission.js +43 -0
  66. package/dist/runtime/call-admission.js +67 -0
  67. package/dist/runtime/connector-scope.js +72 -0
  68. package/dist/runtime/run.js +174 -0
  69. package/dist/runtime/services.js +130 -0
  70. package/dist/runtime/storage.js +66 -0
  71. package/dist/server.js +111 -89
  72. package/dist/skills.js +1 -1
  73. package/dist/storage/file.js +39 -2
  74. package/dist/storage/memory.js +28 -16
  75. package/dist/timeout.js +17 -47
  76. package/dist/types.d.ts +64 -7
  77. package/dist/ui.d.ts +16 -2
  78. package/dist/ui.js +68 -176
  79. package/dist/url-safety.js +157 -0
  80. package/dist/version.d.ts +1 -1
  81. package/dist/version.js +1 -1
  82. package/documentation/architecture.md +368 -182
  83. package/documentation/auth.md +277 -176
  84. package/documentation/code-mode.md +479 -329
  85. package/documentation/meta-tools.md +494 -417
  86. package/ethos.md +44 -47
  87. package/examples/worker/AGENTS.md +2 -1
  88. package/examples/worker/README.md +62 -14
  89. package/examples/worker/src/d1-storage.ts +116 -0
  90. package/examples/worker/src/index.ts +18 -21
  91. package/examples/worker/wrangler.jsonc +16 -4
  92. package/package.json +11 -4
  93. package/templates/node/.env.example +3 -3
  94. package/templates/node/AGENTS.md +5 -4
  95. package/templates/node/README.md +2 -1
  96. package/templates/node/package.json +1 -1
  97. package/templates/node/src/index.ts +23 -22
  98. package/dist/auth/downstream-oauth.d.ts +0 -200
  99. package/dist/catalog-fingerprint.d.ts +0 -19
  100. package/dist/catalog-limits.d.ts +0 -9
  101. package/dist/catalog-service.d.ts +0 -263
  102. package/dist/catalog.d.ts +0 -87
  103. package/dist/concurrency.d.ts +0 -3
  104. package/dist/connectors/guarded-fetch.d.ts +0 -92
  105. package/dist/credential-rules.d.ts +0 -85
  106. package/dist/execute.d.ts +0 -154
  107. package/dist/executor-result.d.ts +0 -10
  108. package/dist/executors/quickjs-child.d.ts +0 -1
  109. package/dist/executors/quickjs-protocol.d.ts +0 -67
  110. package/dist/favicon.d.ts +0 -1
  111. package/dist/identity.d.ts +0 -4
  112. package/dist/invocation.d.ts +0 -74
  113. package/dist/mcp-result.d.ts +0 -8
  114. package/dist/meta-tools.d.ts +0 -129
  115. package/dist/operator-ui/generated.d.ts +0 -2
  116. package/dist/operator-ui/view.d.ts +0 -151
  117. package/dist/result-shapes.d.ts +0 -13
  118. package/dist/retired-toolkits.d.ts +0 -2
  119. package/dist/routes/activity.d.ts +0 -2
  120. package/dist/routes/credentials.d.ts +0 -2
  121. package/dist/routes/mcp.d.ts +0 -10
  122. package/dist/routes/oauth-management.d.ts +0 -2
  123. package/dist/routes/oauth.d.ts +0 -2
  124. package/dist/routes/ui.d.ts +0 -2
  125. package/dist/server.d.ts +0 -9
  126. package/dist/skills.d.ts +0 -44
  127. package/dist/timeout.d.ts +0 -21
  128. package/dist/tool-safety.d.ts +0 -9
  129. package/documentation/call-admission.md +0 -158
  130. package/documentation/cloudflare.md +0 -471
  131. package/documentation/connector-guides.md +0 -176
  132. package/documentation/connectors.md +0 -431
  133. package/documentation/linear.md +0 -193
  134. package/documentation/mixpanel.md +0 -160
  135. package/documentation/notion.md +0 -308
  136. package/documentation/operations.md +0 -359
  137. package/documentation/operator-ui.md +0 -135
  138. package/documentation/optional-modules-upgrade.md +0 -243
  139. package/documentation/provider-conventions.md +0 -729
  140. package/documentation/request-admission.md +0 -204
  141. package/documentation/revenuecat.md +0 -305
  142. package/documentation/storage-and-credentials.md +0 -254
  143. package/documentation/stripe.md +0 -262
  144. package/documentation/upgrading.md +0 -768
  145. package/documentation/vercel.md +0 -241
@@ -1,151 +0,0 @@
1
- import type { CatalogDriftReport } from "../types.js";
2
- import type { CredentialManagementCapability, UiData } from "./model.js";
3
- /**
4
- * Everything the operator app knows, and every rule for changing it, with no
5
- * DOM in sight. The components render this and nothing else, so the questions
6
- * that matter — what a page shows while it loads, what an identity change
7
- * erases, whether a value may become an href — are answered by functions a test
8
- * can call directly rather than by reading rendered markup back out of a
9
- * browser.
10
- */
11
- export type OperatorPage = "connections" | "activity";
12
- export declare const OPERATOR_PAGES: readonly OperatorPage[];
13
- export declare const PAGE_META: Readonly<Record<OperatorPage, {
14
- path: string;
15
- label: string;
16
- }>>;
17
- export declare function pageForPath(path: string): OperatorPage;
18
- export interface UiActivityActor {
19
- kind?: string;
20
- id?: string;
21
- namespace?: string;
22
- label?: string;
23
- }
24
- export interface UiActivityEvent {
25
- occurredAt: string;
26
- actor?: UiActivityActor;
27
- connectorId: string;
28
- toolName: string;
29
- address: string;
30
- source: string;
31
- outcome: string;
32
- durationMs: number;
33
- attempts: number;
34
- errorCode?: string;
35
- friction?: string;
36
- }
37
- /** A message with the tone that decides its live region: status or alert. */
38
- export interface Notice {
39
- message: string;
40
- tone: "info" | "error";
41
- }
42
- export declare function info(message: string): Notice;
43
- export declare function failure(message: string): Notice;
44
- /**
45
- * A remote collection's four states, named once so every page spells them the
46
- * same way. `idle` is "nobody has asked yet" and is what makes a re-entered
47
- * page fetch again after an identity change.
48
- */
49
- type LoadPhase = "idle" | "loading" | "ready" | "error";
50
- export interface OperatorState {
51
- page: OperatorPage;
52
- /**
53
- * Bumped by every identity change. Async work captures it before awaiting and
54
- * throws its result away if the operator is no longer the one who asked —
55
- * the fence that keeps one identity's data off another identity's screen.
56
- */
57
- generation: number;
58
- /** `loading` until the first /ui/data answer decides gated or ready. */
59
- session: "loading" | "gated" | "ready";
60
- gate: Notice | null;
61
- /** True while a signed-in operator's /ui/data is in flight. */
62
- refreshing: boolean;
63
- /**
64
- * Element id the next render should focus. A rebuilt page has no stable node
65
- * to hand focus to from an event handler, so the request travels through
66
- * state and the shell spends it once the new markup exists.
67
- */
68
- pendingFocus: string | null;
69
- data: UiData | null;
70
- connectorFilter: string;
71
- oauthNotice: Notice | null;
72
- /** Connector id whose OAuth mutation is in flight. */
73
- oauthBusy: string | null;
74
- credentialNotice: Notice | null;
75
- /** Connector id whose credential form is open. */
76
- credentialEditing: string | null;
77
- /** Connector id whose credential mutation is in flight. */
78
- credentialBusy: string | null;
79
- activityPhase: LoadPhase;
80
- activityNotice: Notice | null;
81
- activityEvents: UiActivityEvent[];
82
- activityCursor: string | null;
83
- activitySearch: string;
84
- }
85
- export declare function initialState(page: OperatorPage): OperatorState;
86
- /**
87
- * Drop to the gate and forget the previous operator. Bumping the generation is
88
- * what makes the drop stick: work already in flight for the old identity
89
- * resolves into a state that no longer accepts it.
90
- */
91
- export declare function resetIdentity(state: OperatorState, gate?: Notice | null): OperatorState;
92
- /**
93
- * Leaving a page closes what should not survive it: a one-time secret, a half
94
- * typed credential, and the notices that answered the page just left.
95
- */
96
- export declare function withPage(state: OperatorState, page: OperatorPage): OperatorState;
97
- export declare function credentialUnavailableCopy(capability?: CredentialManagementCapability): string;
98
- export declare function connectorStatusLabel(status: string): string;
99
- export declare function toolCountLabel(count: number): string;
100
- /**
101
- * Hosted-provider catalog drift, as an operator reads it
102
- * ([#343](https://github.com/zackbart/connecta/issues/343)).
103
- *
104
- * - `unavailable` — no refresh has been observed in this runtime, so there is
105
- * nothing to report. Deliberately not `clean`: "we have not looked" and "we
106
- * looked and it matches" are different answers, and only one of them is a
107
- * reason to stop worrying.
108
- * - `clean` — a refresh happened and every category counted zero.
109
- * - `warning` — a refresh happened and at least one category did not.
110
- */
111
- export type DriftState = "clean" | "warning" | "unavailable";
112
- export declare function driftTotal(drift?: CatalogDriftReport): number;
113
- export declare function driftState(drift?: CatalogDriftReport): DriftState;
114
- /** Every category with its count, so a clean report still shows its zeros. */
115
- export declare function driftCounts(drift?: CatalogDriftReport): Array<{
116
- key: string;
117
- label: string;
118
- count: number;
119
- }>;
120
- /** One line naming the state and when it was observed. Never what drifted. */
121
- export declare function driftSummary(drift?: CatalogDriftReport): string;
122
- /**
123
- * Only http/https may become a clickable href — the browser half of the gate
124
- * `src/ui.ts` applies before an authorizationUrl is ever serialized. A hostile
125
- * downstream that gets a `javascript:` URL past one of them still meets the
126
- * other, and the caller renders inert text instead of a link.
127
- */
128
- export declare function safeHttpHref(url: string | undefined): string | null;
129
- /** Locale timestamp, or "" for anything that is not a readable date. */
130
- export declare function formatDate(value?: string): string;
131
- export declare function actorLabel(actor?: UiActivityActor): string;
132
- /**
133
- * The stable id behind a friendly label, shown only when a label or namespace
134
- * could otherwise make two different people look like one.
135
- */
136
- export declare function actorStableId(actor?: UiActivityActor): string | null;
137
- export declare function filterActivity(events: UiActivityEvent[], query: string): UiActivityEvent[];
138
- /** Counts only. What the deployment ran, never what it sent or received. */
139
- export declare function activitySummary(events: UiActivityEvent[]): string;
140
- export declare function activityOutcomeClass(outcome: string): string;
141
- /** The one-line detail under an address: source, retries, and friction. */
142
- export declare function activityDetail(event: UiActivityEvent): string;
143
- export declare function credentialStateLabel(credential: {
144
- configured: boolean;
145
- fields?: unknown[];
146
- lastFour?: string;
147
- updatedAt?: string;
148
- }): string;
149
- /** Gate copy for each browser-auth shape, so the sign-in state is never a blank page. */
150
- export declare function gateCopy(kind: string, signedIn: boolean): string;
151
- export {};
@@ -1,13 +0,0 @@
1
- import type { JsonSchema, ToolDef } from "./types.js";
2
- /**
3
- * Passive, process-local output-shape learning. Provider declarations always
4
- * win; observations are an open, optional-field routing aid and never a
5
- * replacement contract.
6
- */
7
- export declare class ObservedOutputSchemas {
8
- private readonly entries;
9
- private cacheKey;
10
- private set;
11
- get(connectorId: string, definition: ToolDef): JsonSchema | undefined;
12
- observe(connectorId: string, definition: ToolDef, value: unknown): void;
13
- }
@@ -1,2 +0,0 @@
1
- /** Fail loudly when JavaScript callers pass the retired audience-scoping keys. */
2
- export declare function assertNoRetiredToolkitOptions(source: string, options: object): void;
@@ -1,2 +0,0 @@
1
- import { type RouteContext } from "./shared.js";
2
- export declare function routeActivity(context: RouteContext): Promise<Response | null>;
@@ -1,2 +0,0 @@
1
- import { type RouteContext } from "./shared.js";
2
- export declare function routeCredentials(context: RouteContext): Promise<Response | null>;
@@ -1,10 +0,0 @@
1
- import { type RouteContext, type ServerOptions } from "./shared.js";
2
- export declare const MCP_CORS_HEADERS: {
3
- "Access-Control-Allow-Origin": string;
4
- "Access-Control-Allow-Methods": string;
5
- "Access-Control-Allow-Headers": string;
6
- };
7
- export declare function createMcpRoute(opts: ServerOptions): {
8
- handle(context: RouteContext): Promise<Response | null>;
9
- rejectOrigin(request: Request): Response | null;
10
- };
@@ -1,2 +0,0 @@
1
- import { type RouteContext } from "./shared.js";
2
- export declare function routeOAuthManagement(context: RouteContext): Promise<Response | null>;
@@ -1,2 +0,0 @@
1
- import { type RouteContext } from "./shared.js";
2
- export declare function routeOAuthCallback(context: RouteContext): Promise<Response | null>;
@@ -1,2 +0,0 @@
1
- import { type RouteContext } from "./shared.js";
2
- export declare function routeUi(context: RouteContext): Promise<Response | null>;
package/dist/server.d.ts DELETED
@@ -1,9 +0,0 @@
1
- import { type RuntimeExecutionContext, type ServerOptions } from "./routes/shared.js";
2
- export type { ServerOptions } from "./routes/shared.js";
3
- /**
4
- * Build the Web-standard fetch handler.
5
- *
6
- * Route ordering is the contract: private mutation routes precede wildcard
7
- * OPTIONS, and the security wrapper is applied to every response, including 404s.
8
- */
9
- export declare function createFetchHandler(opts: ServerOptions): (request: Request, runtimeContext?: RuntimeExecutionContext) => Promise<Response>;
package/dist/skills.d.ts DELETED
@@ -1,44 +0,0 @@
1
- import type { Connector } from "./types.js";
2
- export declare const CONNECTA_INSTRUCTIONS = "Choose a route before discovery. A known-address read needs only call_tool. Unknown-address read-only work starts with execute_code to discover, call, and return the answer; use the same route for reduction, multiple or dependent calls, loops, joins, or branches. Keep discovery and calls together when schemas suffice; do not return catalog matches alone. Inspect unfamiliar result shapes with a small sample before proceeding. Only readOnlyHint: true tools run there. Keep catalog inspection and unannotated, write-capable, or destructive work top level: search_tools then call_destructive_tool when a call is needed. After auth_required use authorize_connector. After a truncated direct result use get_result. Guidance is on demand: fetch skills({ name: \"usage\" }) only when these instructions and the tool description are insufficient or a run needs repair.";
3
- /** Shared Connecta routing guidance, byte-identical across deployments. */
4
- export declare const USAGE_SKILL: string;
5
- /** The always-loaded MCP `instructions` string. */
6
- export declare function instructionsFor(): string;
7
- /** True when at least one of `connectors` carries a usage guide. */
8
- export declare function hasConnectorGuides(connectors: readonly Connector[]): boolean;
9
- /** The skill name that fetches `connector`'s guide. */
10
- export declare function connectorSkillName(connectorId: string): string;
11
- /** The connector's guide, or undefined when it declares none (or a blank one). */
12
- export declare function connectorGuide(connector: Connector): string | undefined;
13
- /** Discovery budget for one connector-guide summary, including an ellipsis. */
14
- export declare const GUIDE_SUMMARY_LENGTH = 120;
15
- /** Normalize authored and derived summaries under one construction contract. */
16
- export declare function normalizeGuideSummary(summary: string): string | undefined;
17
- /** Bounded, decision-useful discovery summary for a connector guide. */
18
- export declare function connectorGuideSummary(connector: Connector): string | undefined;
19
- /** Whether correct use always depends on conventions outside the tool schema. */
20
- export declare function connectorGuideRequired(connector: Connector): boolean;
21
- export interface SkillListing {
22
- name: string;
23
- description: string;
24
- }
25
- /**
26
- * Every fetchable skill: the built-in guides plus one entry per connector that
27
- * carries a usage guide. Derived from the connector list passed in — the single
28
- * place guide visibility is decided.
29
- */
30
- export declare function listSkills(connectors: readonly Connector[]): SkillListing[];
31
- export type SkillLookup = {
32
- found: true;
33
- content: string;
34
- } | {
35
- found: false;
36
- message: string;
37
- };
38
- /**
39
- * Resolve one skill name. Built-in names match exactly; connector guides are
40
- * reachable only through the `connector:` prefix. Every miss — unknown name,
41
- * unknown connector, connector without a guide — is an explicit error, never a
42
- * silent fallback to the generic guide.
43
- */
44
- export declare function resolveSkill(name: string, connectors: readonly Connector[]): SkillLookup;
package/dist/timeout.d.ts DELETED
@@ -1,21 +0,0 @@
1
- /**
2
- * Generous default bound for a single downstream probe/catalog call. High enough
3
- * to trip only on a pathological hang, not a realistically slow probe.
4
- */
5
- export declare const DEFAULT_PROBE_TIMEOUT_MS = 30000;
6
- /** A finite, positive integer number of milliseconds, or undefined. */
7
- export declare function normalizeTimeoutMs(value: number | undefined): number | undefined;
8
- /** Resolve after `ms`, or false when the caller aborts first. */
9
- export declare function sleep(ms: number, signal?: AbortSignal): Promise<boolean>;
10
- export interface DeadlineOptions {
11
- timeoutMs?: number;
12
- signal?: AbortSignal;
13
- timeoutError: Error;
14
- }
15
- export declare function withDeadline<T>(operation: (signal: AbortSignal) => Promise<T>, options: DeadlineOptions): Promise<T>;
16
- /**
17
- * Give one operation a caller-facing deadline and the matching cancellation
18
- * signal. The timeout rejects with the stable, labelled error while aborting
19
- * any in-flight work that honors the signal.
20
- */
21
- export declare function withAbortableTimeout<T>(operation: (signal: AbortSignal) => Promise<T>, ms: number, label: string): Promise<T>;
@@ -1,9 +0,0 @@
1
- import type { ToolDef } from "./types.js";
2
- /**
3
- * The one fail-closed classification shared by discovery and invocation.
4
- *
5
- * A tool is read-only only when it says so without also saying it is
6
- * destructive. Missing, false, and contradictory annotations all require the
7
- * approval-visible call path.
8
- */
9
- export declare function isExplicitlyReadOnly(definition: ToolDef): boolean;
@@ -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) |