@zackbart/connecta 0.24.1 → 0.24.3
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/CHANGELOG.md +169 -0
- package/dist/auth/bearer.js +2 -0
- package/dist/auth/clerk.d.ts +0 -5
- package/dist/auth/clerk.js +21 -8
- package/dist/auth/downstream-oauth.d.ts +12 -1
- package/dist/auth/downstream-oauth.js +147 -35
- package/dist/call-admission.d.ts +4 -0
- package/dist/call-admission.js +26 -0
- package/dist/catalog-drift.js +9 -4
- package/dist/catalog-service.d.ts +2 -0
- package/dist/catalog-service.js +25 -8
- package/dist/catalog.d.ts +2 -0
- package/dist/catalog.js +246 -121
- package/dist/connector-access.d.ts +32 -0
- package/dist/connector-access.js +79 -0
- package/dist/connectors/api.js +11 -1
- package/dist/connectors/guarded-fetch.d.ts +1 -1
- package/dist/connectors/guarded-fetch.js +27 -20
- package/dist/connectors/remote-mcp.js +84 -53
- package/dist/errors.d.ts +17 -0
- package/dist/errors.js +58 -0
- package/dist/execute.js +85 -23
- package/dist/executor-result.js +3 -1
- package/dist/executors/quickjs-child.js +5 -1
- package/dist/executors/quickjs-protocol.d.ts +4 -0
- package/dist/executors/quickjs-runtime.d.ts +1 -1
- package/dist/executors/quickjs-runtime.js +38 -21
- package/dist/executors/quickjs.js +68 -27
- package/dist/index.d.ts +37 -1
- package/dist/index.js +89 -3
- package/dist/invocation.js +134 -93
- package/dist/mcp-result.js +3 -2
- package/dist/meta-tools.js +118 -39
- package/dist/registry.d.ts +29 -1
- package/dist/registry.js +122 -15
- package/dist/routes/credentials.js +1 -0
- package/dist/routes/mcp.d.ts +4 -1
- package/dist/routes/mcp.js +112 -12
- package/dist/routes/oauth-management.js +1 -0
- package/dist/routes/oauth.js +4 -0
- package/dist/routes/shared.d.ts +7 -1
- package/dist/routes/shared.js +12 -13
- package/dist/routes/ui.js +2 -1
- package/dist/server.js +15 -3
- package/dist/skills.js +6 -5
- package/dist/storage/file.d.ts +6 -2
- package/dist/storage/file.js +312 -34
- package/dist/storage/memory.js +12 -1
- package/dist/validate.js +3 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +30 -9
- package/documentation/auth.md +110 -6
- package/documentation/call-admission.md +24 -8
- package/documentation/code-mode.md +34 -22
- package/documentation/connectors.md +47 -5
- package/documentation/meta-tools.md +74 -6
- package/documentation/operations.md +20 -19
- package/documentation/provider-conventions.md +7 -0
- package/documentation/request-admission.md +38 -4
- package/documentation/storage-and-credentials.md +54 -1
- package/documentation/upgrading.md +21 -5
- package/ethos.md +1 -1
- package/package.json +1 -1
- package/templates/node/package.json +1 -1
package/documentation/auth.md
CHANGED
|
@@ -6,6 +6,16 @@ Cloudflare Access from `/auth/cloudflare-access`. Providers may be combined;
|
|
|
6
6
|
static bearers are checked first, then other providers in configuration order.
|
|
7
7
|
Connecta no longer issues `cta_` tokens or serves token-management routes.
|
|
8
8
|
|
|
9
|
+
The bearer adapter challenges with `WWW-Authenticate: Bearer` and deliberately
|
|
10
|
+
omits `resource_metadata`. Its credential is configured out of band; it has no
|
|
11
|
+
OAuth authorization server or registration endpoint to advertise. Interactive
|
|
12
|
+
adapters or the edge own OAuth discovery. Every open deployment with at least
|
|
13
|
+
one connector warns at construction, including API connectors with static auth
|
|
14
|
+
headers. Credential and OAuth connectors add explicit wording about those grants.
|
|
15
|
+
|
|
16
|
+
MCP browser origins pass the [Origin check](./request-admission.md#origin-before-admission)
|
|
17
|
+
before admission or auth. This is independent of an identity's tool grants.
|
|
18
|
+
|
|
9
19
|
## Principals, visibility, and operators
|
|
10
20
|
|
|
11
21
|
The actor identifies the caller in activity. The subject owns transient results
|
|
@@ -13,10 +23,95 @@ such as `get_result` pages. The principal is the human owner of personal
|
|
|
13
23
|
connector auth. An interactive Clerk or Access user supplies all three. A
|
|
14
24
|
Cloudflare service identity has an actor and subject but no principal.
|
|
15
25
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
26
|
+
A subject or user id always selects a result-stash partition, even when the
|
|
27
|
+
provider omits `activityActorNamespace`. An explicit namespace remains the
|
|
28
|
+
partition namespace; without one, Connecta uses `connecta:auth:<provider kind>`.
|
|
29
|
+
Subject ids must be distinct within that namespace. This fallback grants no
|
|
30
|
+
personal-auth ownership and changes no activity attribution. Open deployments
|
|
31
|
+
and providers that return no identity share one result partition. A provider
|
|
32
|
+
that supplies only an explicit principal uses that principal as its subject.
|
|
33
|
+
|
|
34
|
+
`identity.connectorAccess` returns `"all"` or a list of grants. A grant is a
|
|
35
|
+
declared connector id, which opens every tool on it, or a `connector.tool`
|
|
36
|
+
address, which opens that tool alone. Grants are additive, so a bare id beside
|
|
37
|
+
addresses for the same connector means the whole connector. It governs
|
|
38
|
+
discovery and use, and defaults to all connectors.
|
|
39
|
+
|
|
40
|
+
Tool grants are enforced in the scoped registry view, below the catalog
|
|
41
|
+
service. Since 0.24.2, `search_tools`, `describe_tools`, both call tools, a
|
|
42
|
+
program's `connecta.search`, `connecta.describe`, and `connecta.call`, and the
|
|
43
|
+
connection UI read that filtered tool list. Connector-level discovery, guides,
|
|
44
|
+
and `authorize_connector` retain a connector when any tool on it is granted.
|
|
45
|
+
In particular, a `docs.read` grant permits the `docs` authorization handoff,
|
|
46
|
+
subject to the separate auth-management permissions below. Without any grant
|
|
47
|
+
on `docs`, `authorize_connector` returns the same "Unknown connector" refusal
|
|
48
|
+
as an absent connector. An ungranted tool fails exactly like one the connector
|
|
49
|
+
never had: `unknown_tool`, with no hint that it exists. That is the whole security
|
|
50
|
+
claim, and it lives in one place on purpose. There is no separate endpoint per
|
|
51
|
+
tool set; an identity that should see a narrower slice is a branch in this
|
|
52
|
+
resolver, and a bot that needs its own slice is its own bearer subject.
|
|
53
|
+
|
|
54
|
+
## Pools
|
|
55
|
+
|
|
56
|
+
A pool is a named slice of the deployment served at its own endpoint,
|
|
57
|
+
`/mcp/<pool>`, for the case where one identity needs different capability
|
|
58
|
+
sets on different clients: a support agent that sees three Notion tools and
|
|
59
|
+
Linear, a calendar bot that sees one tool, both over the same credentials and
|
|
60
|
+
catalog cache.
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
createConnecta({
|
|
64
|
+
pools: {
|
|
65
|
+
support: {
|
|
66
|
+
tools: ["linear", "notion.search_pages", "notion.fetch_page"],
|
|
67
|
+
grant: ({ principal }) => supportTeam.has(principal?.id ?? ""),
|
|
68
|
+
},
|
|
69
|
+
calendar_bot: {
|
|
70
|
+
tools: ["calendar.create_event"],
|
|
71
|
+
grant: ({ actor }) => actor.id === "calendar-bot",
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
identity: { connectorAccess },
|
|
75
|
+
connectors,
|
|
76
|
+
executor,
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The rules, each of which is a test:
|
|
81
|
+
|
|
82
|
+
- **A pool narrows; it never widens.** The view on `/mcp/<pool>` is the pool
|
|
83
|
+
intersected with the identity's own `connectorAccess`. Plain `/mcp` is
|
|
84
|
+
unchanged. The security boundary is still the resolver; the pool decides
|
|
85
|
+
which part of it a given client sees.
|
|
86
|
+
- **Grant defaults to deny.** A pool with no `grant` serves nobody. Only a
|
|
87
|
+
literal `true` admits; any other return, a throw, and an undeclared pool
|
|
88
|
+
name produce one 404 identical in status, body, and headers, so a
|
|
89
|
+
credential does not enumerate the other pools by response content. Timing
|
|
90
|
+
is explicitly not hidden: a declared name awaits its grant, while an
|
|
91
|
+
undeclared name returns without that lookup. We accept this pool-name
|
|
92
|
+
oracle because names grant no access, a fixed delay cannot hide unbounded
|
|
93
|
+
grant I/O, and invoking grants for unknown names would add avoidable work
|
|
94
|
+
while holding an admission permit. Keep grants pure and fast; do not treat
|
|
95
|
+
pool names as secrets. The operator log carries the refusal reason.
|
|
96
|
+
- **Structural mistakes throw at construction.** A malformed name, an
|
|
97
|
+
unknown connector, an empty pool, and a `connector.tool` address an
|
|
98
|
+
`api()` connector's static catalog lacks all refuse to boot. Remote
|
|
99
|
+
catalogs load lazily, so their addresses are checked at load and stay
|
|
100
|
+
unreachable until they match.
|
|
101
|
+
- **OAuth discovery follows the path.** On Clerk, the 401 challenge for
|
|
102
|
+
`/mcp/<pool>` names `/.well-known/oauth-protected-resource/mcp/<pool>`,
|
|
103
|
+
whose `resource` is the pool URL, so RFC 9728 clients see a match.
|
|
104
|
+
Cloudflare Managed OAuth is application-level and needs nothing.
|
|
105
|
+
|
|
106
|
+
A `connector.tool` address the live catalog does not contain is unreachable
|
|
107
|
+
and warned once while its address remains in a 1,024-entry FIFO. An evicted
|
|
108
|
+
address may warn again; caller-derived grant text cannot grow retained warning
|
|
109
|
+
state without bound. Remote catalogs load lazily, so construction
|
|
110
|
+
cannot check it, and a catalog that drifts later can never widen a grant
|
|
111
|
+
because there is no wildcard: every tool grant is an exact name.
|
|
112
|
+
|
|
113
|
+
Visibility alone grants no authentication-management permission. Two
|
|
114
|
+
independent resolvers return `"all"`, `"none"`, or declared connector ids:
|
|
20
115
|
|
|
21
116
|
- `credentialAdministration` allows an interactive human to manage shared
|
|
22
117
|
credentials and shared OAuth grants.
|
|
@@ -39,8 +134,12 @@ administrator role or token-management authority.
|
|
|
39
134
|
createConnecta({
|
|
40
135
|
auth: cloudflareAccessAuth(),
|
|
41
136
|
identity: {
|
|
42
|
-
connectorAccess: ({ principal }) =>
|
|
43
|
-
principal?.id === "owner-id"
|
|
137
|
+
connectorAccess: ({ principal, actor }) =>
|
|
138
|
+
principal?.id === "owner-id"
|
|
139
|
+
? "all"
|
|
140
|
+
: actor.id === "calendar-bot"
|
|
141
|
+
? ["calendar.create_event"]
|
|
142
|
+
: ["shared_docs", "personal_linear", "notion.search_pages"],
|
|
44
143
|
credentialAdministration: ({ principal }) =>
|
|
45
144
|
principal?.id === "owner-id" ? "all" : "none",
|
|
46
145
|
personalConnection: () => ["personal_linear"],
|
|
@@ -160,6 +259,11 @@ secret-free handoff to the connection UI. Without the UI, that recovery is
|
|
|
160
259
|
interactive MCP caller can still start downstream OAuth through
|
|
161
260
|
`authorize_connector` without the UI. Core owns the callback and verifies state
|
|
162
261
|
and principal ownership independently of the optional browser application.
|
|
262
|
+
A browser returning from downstream consent normally carries no MCP
|
|
263
|
+
Authorization header, so an interactive bearer provider's 401 does not reject
|
|
264
|
+
the callback. The verified state and its saved principal handoff select the
|
|
265
|
+
owner; a browser identity, when present, must match that owner and may manage
|
|
266
|
+
the connector. An interactive provider's explicit 403 still refuses the flow.
|
|
163
267
|
|
|
164
268
|
See [meta-tools](./meta-tools.md#authorization-recovery) and
|
|
165
269
|
[storage and credentials](./storage-and-credentials.md). The
|
|
@@ -103,15 +103,28 @@ checks, and authorization operations stay outside the budget: they are not the
|
|
|
103
103
|
calls a provider is rate-limiting, and charging discovery for them would make
|
|
104
104
|
a program's first search cost it capacity to act.
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
109
110
|
`diagnostics: true`, `admissionMs` reports the permit wait and `connectorMs`
|
|
110
111
|
the admitted attempt — which is the only way to tell "the provider is slow"
|
|
111
112
|
from "we are throttling ourselves".
|
|
112
113
|
|
|
113
114
|
## Enforcement scope
|
|
114
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
|
+
|
|
115
128
|
This is deliberately **per-runtime**. It completely contains fan-out inside one
|
|
116
129
|
request, including parallel `connecta.call` calls in one Worker isolate. A rolling
|
|
117
130
|
budget is exact inside one Node process or Worker isolate, and best-effort
|
|
@@ -124,13 +137,16 @@ the bound actually is rather than implying a global one.
|
|
|
124
137
|
|
|
125
138
|
## Observations
|
|
126
139
|
|
|
127
|
-
`/health` exposes payload-free
|
|
128
|
-
`admission.downstreamCalls.
|
|
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
|
|
129
143
|
active and queued gauges, cumulative admitted/queued/rejected/rate-limited/
|
|
130
144
|
cancelled counts, and queue-wait count, total, and maximum. The open endpoint
|
|
131
|
-
never exposes partition keys, tool arguments, or results — a partition key can
|
|
145
|
+
never exposes connector ids, principal ids, partition keys, tool arguments, or results — a partition key can
|
|
132
146
|
be a customer identifier, which is precisely why it stays out of an unauthenticated
|
|
133
|
-
payload.
|
|
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
|
|
134
150
|
typed error code.
|
|
135
151
|
|
|
136
152
|
## Tests that enforce this
|
|
@@ -138,5 +154,5 @@ typed error code.
|
|
|
138
154
|
| Invariant | Suite |
|
|
139
155
|
| --- | --- |
|
|
140
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) |
|
|
141
|
-
|
|
|
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) |
|
|
142
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) |
|
|
@@ -180,11 +180,11 @@ const page = await connecta.search({
|
|
|
180
180
|
});
|
|
181
181
|
```
|
|
182
182
|
|
|
183
|
-
**S1.** Returns one flat page: `{ tools, total, offset, limit, hasMore }`, plus `nextOffset` when more remains and `matchMode: "partial"` when no tool matched every term. Top-level `search_tools` is different: it returns `{ connectors: [{ id, tools }], total, offset, limit, hasMore }`. Complete matches normally precede partial matches, but a partial candidate whose complete normalized tool name occurs in the normalized raw query competes by score; conversational cleanup applies only to scoring terms. Other candidates covering at least two terms fill the page after every complete match; when no complete match exists, the existing any-term fallback remains. Each entry in `tools` carries `address`, `name`, the configured `connectorTitle` when present (normalized whitespace, at most 120 UTF-8 bytes), and — when requested — `description`, `inputSchema`, `outputSchema`, `annotations`, and the connector's `guide`. An output shape learned under `S9` also carries `outputSchemaSource: "observed"`; provider declarations carry no source marker. Tool rows expose neither lexical scores nor per-result coverage. An empty or whitespace-only query browses. Non-empty input with no ASCII lexical terms returns no tools and bounded no-match analysis; mixed input searches with its ASCII terms. Compact shapes omit property prose, put required fields first, and cap each shape at 1,024 UTF-8 bytes. Each enum node gets 256 of those bytes. About three near-cap enum nodes can therefore coexist while leaving the final quarter for surrounding syntax; the unchanged global fallback still applies above 1,024 bytes. A capped enum preserves whole values before `unknown` and an exact omitted-value count, while an empty enum renders as `never`. Either cap carries `inputSchemaTruncated` or `outputSchemaTruncated`; a shape-wide cap remains structurally valid with `unknown` types plus `/* truncated */`. Small enums remain complete. Use `connecta.describe`
|
|
183
|
+
**S1.** Returns one flat page: `{ tools, total, offset, limit, hasMore }`, plus `nextOffset` when more remains and `matchMode: "partial"` when no tool matched every term. Top-level `search_tools` is different: it returns `{ connectors: [{ id, tools }], total, offset, limit, hasMore }`. Complete matches normally precede partial matches, but a partial candidate whose complete normalized tool name occurs in the normalized raw query competes by score; conversational cleanup applies only to scoring terms. Other candidates covering at least two terms fill the page after every complete match; when no complete match exists, the existing any-term fallback remains. Each entry in `tools` carries `address`, `name`, the configured `connectorTitle` when present (normalized whitespace, at most 120 UTF-8 bytes), and — when requested — `description`, `inputSchema`, `outputSchema`, `annotations`, and the connector's `guide`. An output shape learned under `S9` also carries `outputSchemaSource: "observed"`; provider declarations carry no source marker. Tool rows expose neither lexical scores nor per-result coverage. An empty or whitespace-only query browses. Non-empty input with no ASCII lexical terms returns no tools and bounded no-match analysis; mixed input searches with its ASCII terms. Compact shapes omit property prose, put required fields first, and cap each shape at 1,024 UTF-8 bytes. Each render spends at most 2,000 visits across schema nodes, property and required names, literal values, any constraint-free retry, and its key-only fallback. Resolved `$ref` text is reused within that walk; exhausted work yields `unknown /* truncated */` and the corresponding truncation flag. Each enum node gets 256 of those bytes. About three near-cap enum nodes can therefore coexist while leaving the final quarter for surrounding syntax; the unchanged global fallback still applies above 1,024 bytes. A capped enum preserves whole values before `unknown` and an exact omitted-value count, while an empty enum renders as `never`. Either cap carries `inputSchemaTruncated` or `outputSchemaTruncated`; a shape-wide cap remains structurally valid with `unknown` types plus `/* truncated */`. Small enums remain complete. `prefixItems` renders as a tuple with the declared `items` rest, an `unknown[]` rest when open, or no rest when `items` is false. `dependentSchemas` and `if`/`then`/`else` keep the base shape plus `/* conditional */` and set the truncation flag. `$dynamicRef` resolves a same-named definition like `$ref`; unresolved dynamic references become `unknown` with the truncation flag. These forms share the byte and work bounds above. Use `connecta.describe({ address, format: "json" })` or JSON search for omitted exact constraints.
|
|
184
184
|
|
|
185
185
|
**S1a.** `connector` loads only the named catalog; omit it only when the integration is ambiguous, because an unscoped search fans out across every configured connector. `safety: "readOnly"` returns exactly the tools available through `connecta.call`; `"approvalRequired"` returns the complementary fail-closed class, including false, missing, and contradictory annotations. Omitted or `"all"` preserves the complete catalog. These filters grant no authority and change no admission decision.
|
|
186
186
|
|
|
187
|
-
**S2.** A requested object schema carries `inputKeys`, `requiredInputKeys`, and `outputKeys`: the same names the rendered schema shows, ready to check before building arguments. Match inputs, truncation, safety, and outputs, not lexical
|
|
187
|
+
**S2.** A requested object schema carries `inputKeys`, `requiredInputKeys`, and `outputKeys`: the same names the rendered schema shows, ready to check before building arguments. `requiredInputKeys` includes only declared properties. Match inputs, truncation, safety, and outputs, not lexical
|
|
188
188
|
rank; search distinct operations separately and use `outputKeys`, not guessed roots. A non-object schema — a union, an array, an
|
|
189
189
|
unresolvable `$ref` — carries no lists rather than empty ones, because absent
|
|
190
190
|
means "read the schema" where `[]` would claim the tool takes no fields. The
|
|
@@ -199,9 +199,8 @@ carries the same metadata whenever schemas are requested. Code-mode callers
|
|
|
199
199
|
can set `includeSchemaKeys: false` to buy the bytes back.
|
|
200
200
|
|
|
201
201
|
**S3.** Discovery is bounded and the bounds throw rather than silently shrink: a
|
|
202
|
-
`limit` outside 1–100 is `
|
|
203
|
-
exceeds 256,000 bytes is `result_too_large
|
|
204
|
-
ask for less. The thrown error carries the stable `code`, `retryable`, and
|
|
202
|
+
`limit` outside 1–100, an `offset` that is not a non-negative integer, or a supplied `query` that is not a string is `invalid_args`. Omitted `offset` starts at 0; omitted `query` browses. A page whose serialized form
|
|
203
|
+
exceeds 256,000 bytes is `result_too_large`; a program's page is measured as its serialized value, while top-level `search_tools` measures the complete tool result including both copies and JSON escaping. Each error carries a hint for correcting the request. The thrown error carries the stable `code`, `retryable`, and
|
|
205
204
|
`details` fields (`E1`).
|
|
206
205
|
|
|
207
206
|
### connecta.describe
|
|
@@ -222,7 +221,7 @@ carry a route-aware `nextAction`; a close miss may add three canonical `suggesti
|
|
|
222
221
|
Catalog failures add only `retryAfterMs` when known. One bad address never fails the whole call. Each failed entry clamps its
|
|
223
222
|
caller-authored `address` to 512 UTF-8 bytes with an `…` marker. Entry order
|
|
224
223
|
correlates a clipped address with its request; successes keep canonical addresses. More than 100
|
|
225
|
-
addresses is `invalid_args`; the same 256,000-byte ceiling applies. A success whose output shape came from `S9` carries `outputSchemaSource: "observed"` beside the rendered schema.
|
|
224
|
+
addresses is `invalid_args`; the same 256,000-byte ceiling applies. Compact describe keeps property prose within a separate 8,192-byte UTF-8 shape cap and shares search's 2,000-visit work budget. A capped shape sets `inputSchemaTruncated` or `outputSchemaTruncated`; use `format: "json"` for the exact schema. A success whose output shape came from `S9` carries `outputSchemaSource: "observed"` beside the rendered schema.
|
|
226
225
|
|
|
227
226
|
### connecta.call
|
|
228
227
|
|
|
@@ -286,8 +285,9 @@ clauses are [Emitted output](#emitted-output) (`M1`–`M10`).
|
|
|
286
285
|
| Program or execution failure (`E5`, `E6`, a bridge bound in `L6`) | error text | no |
|
|
287
286
|
|
|
288
287
|
Both executor bridges reduce a rejected host call to `new Error(message)`. Connecta restores the typed failure in a trusted prelude with a per-execution authenticated frame (`X11`), without turning the rejection into a returned value.
|
|
289
|
-
`message` remains
|
|
288
|
+
`message` remains human text, capped at 2,000 JSON-serialized characters including quotes and an `…` marker when clipped. `code` and `retryable` are the stable branch fields; `details` carries the host classification. The complete details object fits 3,700 serialized characters. If optional recovery metadata would exceed that bound, it is omitted whole, preserving `code`, `message`, `retryable`, and `retryAfterMs`; a clipped recovery address or argument would describe a different call. This covers `call`, `search`, `describe`, `emit`, and the host-call budget.
|
|
290
289
|
Program-authored errors stay untyped, and code must never parse error prose.
|
|
290
|
+
An `unavailable` classification may include optional `details.host` as an HTTP(S) origin of at most 253 UTF-8 bytes and `details.code` as a validated network errno, undici transport code, or `timeout` of at most 32 bytes; these diagnostics never enter activity records.
|
|
291
291
|
|
|
292
292
|
**E2.** The taxonomy: `retryable` is what connecta reports, `Y3` what a program may do.
|
|
293
293
|
|
|
@@ -301,7 +301,7 @@ Program-authored errors stay untyped, and code must never parse error prose.
|
|
|
301
301
|
| `not_found` | the downstream answered and the resource is not there — the one code that says skip this id rather than stop, raised only where the provider tells absence from a permission gap ([H11](./provider-conventions.md#h11--errors-are-mapped-to-what-the-caller-does-next)) | false |
|
|
302
302
|
| `input_required_unsupported` | a downstream asked for mid-call input | false |
|
|
303
303
|
| `rate_limited` | the downstream reported a rate limit | true |
|
|
304
|
-
| `unavailable` | the downstream is down or unreachable | true |
|
|
304
|
+
| `unavailable` | the downstream is down or unreachable; optional sanitized `details.host` and `details.code` describe the transport failure without paths, queries, credentials, or provider prose | true |
|
|
305
305
|
| `timeout` | the per-call deadline (`execute.hostCallTimeoutMs`, default 15 s) expired | true |
|
|
306
306
|
| `cancelled` | the run ended while this call was in flight (`E5`) | false |
|
|
307
307
|
| `connector_call_failed` | anything else the connector threw | per message |
|
|
@@ -330,16 +330,16 @@ guest: admission rejection (`executor_overloaded`, retryable, with
|
|
|
330
330
|
reported to the model as an error result. One seam: a host call still in flight
|
|
331
331
|
when the run is cancelled fails with `cancelled`, catchable on the way out but
|
|
332
332
|
never worth acting on (`Y3`). When shutdown tears down a program that had
|
|
333
|
-
already started, accepted blocks are reported as discarded under `M4`; a failure before execution started carries no discard fields.
|
|
333
|
+
already started, accepted blocks are reported as discarded under `M4`; a failure before execution started carries no discard fields. A returned `error` field is a failure even when empty; an empty string reports `executor_failed` with `Error: Execution failed without an error message.`
|
|
334
334
|
|
|
335
335
|
**E6.** An error the program raises itself — a `TypeError`, a call to a
|
|
336
336
|
`connecta` member that is not a provider function (including an inherited one
|
|
337
337
|
like `toString`), a `throw` of its own — ends the run with an error result
|
|
338
338
|
carrying that message. It is not typed, because it is not a connector failure.
|
|
339
339
|
One precedence rule: connecta recognizes an escaped tool failure by its message —
|
|
340
|
-
exactly first, by containment second — so a program that *wraps* a failure's
|
|
340
|
+
exactly first, by containment second for messages of at least eight characters — so a program that *wraps* a failure's
|
|
341
341
|
message in its own text still reports the underlying typed failure. Keeping the
|
|
342
|
-
type beats keeping the prose.
|
|
342
|
+
type beats keeping the prose. Matching retains only the most recent 64 failures per execution, including caught refusals; an older escaped message remains an untyped execution failure. An empty terminal error uses the fixed message in `E5`.
|
|
343
343
|
|
|
344
344
|
**E7.** `retryable` for `unknown_address`, `unknown_tool`, and `destructive_tool_requires_approval` is pinned false, never inferred from an address containing `503`, `429`, or `temporar`. The first two carry `nextAction: { function: "connecta.search", arguments: { query, connector?, includeSchemas: "compact" } }` — the same scoped discovery the top-level record names, keyed to the surface the caller actually has. A program cannot call `search_tools`, so it is never told to. The message, the derived `query`, and a failed describe entry's `address` clamp caller-authored text to 512 UTF-8 bytes with an `…` marker. Those values land in the text content and `structuredContent`, so an invented 50 KB address would otherwise produce a refusal orders of magnitude past the deployment's result cap. A clipped address still identifies the mistake by its position; a short one — the common case — is exact and untagged.
|
|
345
345
|
|
|
@@ -383,7 +383,7 @@ anything, so paging its result would reward the one behavior code mode exists to
|
|
|
383
383
|
remove — and stashing every unprojected return value would spend the result store
|
|
384
384
|
on data nobody asked for.
|
|
385
385
|
|
|
386
|
-
**R5.** `console.log`, `console.warn`, and `console.error` are captured in call order and returned as a single `logs` string, capped at 4,000 characters with a truncation marker. Logs survive failure
|
|
386
|
+
**R5.** `console.log`, `console.warn`, and `console.error` are captured in call order and returned as a single `logs` string, capped at 4,000 characters with a truncation marker. Logs survive program failure through either a returned error result or a thrown error carrying `logs: string[]`. QuickJS streams captured entries to its parent and preserves the received prefix on cancellation, shutdown, deadline termination, child crashes, and IPC failures (`X4`). How a non-string argument renders is not contract (`X4`).
|
|
387
387
|
|
|
388
388
|
**R6.** Nothing else is added to a normal program result. Passing `diagnostics: true` adds one request-local, payload-free `diagnostics` block; a program that emitted adds `emitted: N` and its blocks (`M2`). Omitted, `false`, and emit-free are byte-for-byte the ordinary response path. Diagnostics exist so catalog, connector, and executor costs are distinguishable without persisting payloads or charging normal responses ([#247](https://github.com/zackbart/connecta/issues/247)).
|
|
389
389
|
|
|
@@ -433,8 +433,8 @@ media, not base64 text.
|
|
|
433
433
|
trusted exactly as much as the return value. Preservation is re-emission of
|
|
434
434
|
the raw downstream block, so `S5`'s uncapped fallthrough is contract.
|
|
435
435
|
|
|
436
|
-
**M7.** `emit` spends no host-call budget (`L4`); `
|
|
437
|
-
bounds.
|
|
436
|
+
**M7.** `emit` alone spends no host-call budget (`L4`); `search`, `describe`,
|
|
437
|
+
and `call` share it. `M5`'s bounds are emission's only bounds.
|
|
438
438
|
|
|
439
439
|
**M8.** Emission asks nothing of an executor: `emit` is a provider function,
|
|
440
440
|
blocks cross the guest boundary once as an argument, and `ExecuteResult` is
|
|
@@ -491,7 +491,7 @@ because connecta enforces them above the sandbox:
|
|
|
491
491
|
|
|
492
492
|
| Bound | Value |
|
|
493
493
|
| --- | --- |
|
|
494
|
-
| Host calls per execution | 20 |
|
|
494
|
+
| Host calls per execution, shared by `search`, `describe`, and `call` | 20 by default, `execute.maxHostCalls` |
|
|
495
495
|
| Deadline per host call | 15 s, `execute.hostCallTimeoutMs` |
|
|
496
496
|
| Discovery page | ≤ 100 tools, ≤ 256,000 serialized bytes |
|
|
497
497
|
| `describe` addresses | ≤ 100 |
|
|
@@ -500,7 +500,7 @@ because connecta enforces them above the sandbox:
|
|
|
500
500
|
| Result | 24,000 serialized characters |
|
|
501
501
|
| Logs presented to the model | 4,000 characters |
|
|
502
502
|
|
|
503
|
-
Exhausting the host-call budget fails that call with non-retryable `budget_exceeded` (`E2`) and a message naming the budget. No connector is reached, and the budget does not refill inside one execution.
|
|
503
|
+
Every `call` attempt spends one host call on entry, before address resolution, catalog lookup, safety checks, validation, or dispatch. Unknown addresses, unknown tools, catalog failures, and other pre-dispatch refusals spend the same budget as successful calls; catching a refusal does not refund it. `search` and `describe` likewise spend on entry. Exhausting the host-call budget fails that call with non-retryable `budget_exceeded` (`E2`) and a message naming the budget. No connector is reached, and the budget does not refill inside one execution.
|
|
504
504
|
|
|
505
505
|
**L5.** The guest is memory-, stack-, and CPU-bounded, and a program that
|
|
506
506
|
exhausts a bound ends the run with an error instead of degrading the host. The
|
|
@@ -588,7 +588,14 @@ arguments and captures `log`, `info`, `warn`, `error`, and `debug`; the Dynamic
|
|
|
588
588
|
Worker renders arguments with `String()` (so an object logs as
|
|
589
589
|
`[object Object]`) and captures only `log`, `warn`, and `error`, prefixing the
|
|
590
590
|
latter two. Only the three captured everywhere are contract (`R5`); rendering is
|
|
591
|
-
not.
|
|
591
|
+
not. QuickJS streams each accepted entry within the existing IPC envelope
|
|
592
|
+
bound while retaining its per-entry and cumulative child caps. The parent
|
|
593
|
+
keeps at most 4,001 joined characters for failure recovery, one beyond the
|
|
594
|
+
presentation cap so truncation stays visible. On a normal reply, its complete
|
|
595
|
+
log array takes precedence over that prefix; the two copies are never joined.
|
|
596
|
+
On termination or IPC failure, the parent attaches its retained prefix to the
|
|
597
|
+
thrown error. Admission rejection before the program starts has no guest logs
|
|
598
|
+
to recover.
|
|
592
599
|
|
|
593
600
|
**X5. Leftover authority.** QuickJS blocks imports and has no `fetch`, `process`, timers, `crypto`, or `WebSocket`. Its Node child starts with an explicitly empty process environment rather than inheriting deployment variables or `NODE_OPTIONS`. A Dynamic Worker has those globals plus a non-contract set of runtime builtins through `import()` and `process.getBuiltinModule()`, including `node:path`, `node:crypto`, `node:net`, `node:tls`, `node:dns`, `node:module`, and `cloudflare:workers`. The upstream set can drift; this list is not an allowlist.
|
|
594
601
|
The supported Worker construction is exactly `new DynamicWorkerExecutor({ loader })`. Do not pass `bindings`, `modules`, or `globalOutbound`: each can grant ambient configuration, code, or egress. Under it, `process.env`, lexical `this.env`, and `cloudflare:workers.env` are empty; `node:fs`, `node:http`, and `node:https` are unavailable through either access route; external `fetch`, `WebSocket`, `node:net`, and `node:tls` fail with workerd's outbound-denial error; DNS lookup ends unresolved; and `fetch("data:...")` resolves locally.
|
|
@@ -625,7 +632,12 @@ from one tool call therefore fails on Node and may succeed on Workers — reduce
|
|
|
625
632
|
inside the program either way (`R1`).
|
|
626
633
|
|
|
627
634
|
**X11. Typed host rejection.** Both executors rebuild Connecta's authenticated host-failure frame as a thrown guest `Error` (`E1`). The per-run secret stays in the trusted prelude closure, and the prelude locks `globalThis.Error`, so guest code and connector prose cannot forge the host transport frame.
|
|
628
|
-
The
|
|
635
|
+
The host bounds details before framing (`E1`), including JSON escapes. QuickJS
|
|
636
|
+
refuses an oversized authenticated frame whole rather than slicing through its
|
|
637
|
+
JSON, and the prelude hides an authenticated frame whose JSON is malformed.
|
|
638
|
+
QuickJS keeps the raw bridge and its JSON decoder in a private closure so guest
|
|
639
|
+
code cannot intercept the frame before the prelude handles it. A mismatched
|
|
640
|
+
frame is ordinary untyped prose.
|
|
629
641
|
|
|
630
642
|
## Changes from earlier code mode
|
|
631
643
|
|
|
@@ -659,7 +671,7 @@ the upstream `Executor` shape assignable.
|
|
|
659
671
|
| `S5` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (`unwrapMcpResult`) |
|
|
660
672
|
| `S6` | `test/execute.test.ts` (fail-closed annotations, activity parity) |
|
|
661
673
|
| `S7` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (parallel calls and shared admission) |
|
|
662
|
-
| `S8`, `E1`, `X11` | both guest-contract executors (caught call, discovery, utility, budget, removed-function, and forgery cases; typed promise rejections) |
|
|
674
|
+
| `S8`, `E1`, `X11` | both guest-contract executors (caught call, discovery, utility, budget, removed-function, and forgery cases; typed promise rejections), `test/quickjs-executor.test.ts` (oversized messages, private transport, and forged frames) |
|
|
663
675
|
| `S9` | `test/result-shapes.test.ts` (value exclusion, bounds, merging, LRU and time expiry, runtime isolation, read-only admission, declared precedence, definition invalidation, unwrapped MCP results, discovery provenance, copy isolation, and failure isolation) |
|
|
664
676
|
| `E2`, `E8` | `test/guest-api-contract.test.ts` (code → `retryable`, caught, parallel, and uncaught validation recovery), `test/meta-tools.test.ts` (direct, destructive, provider fallback), `test/validate.test.ts` (bounded payload-free findings), `test/errors.test.ts` |
|
|
665
677
|
| `E3` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (`auth_required`) |
|
|
@@ -677,9 +689,9 @@ the upstream `Executor` shape assignable.
|
|
|
677
689
|
| `Y4` | `test/meta-tools-call.test.ts`, `test/call-admission.test.ts` (one attempt, retry hints, caller reissue) |
|
|
678
690
|
| `L1`, `L2` | `test/guest-api-contract.test.ts` (in-flight call fails `cancelled`), `test/execute.test.ts` (cancels outstanding host calls) |
|
|
679
691
|
| `L3`, `X1` | `test/guest-api-contract.test.ts` (short-deadline executors) |
|
|
680
|
-
| `L4`, `L8` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (budgets) |
|
|
692
|
+
| `L4`, `L8` | `test/guest-api-contract.test.ts`, `test/execute.test.ts` (shared discovery/call budgets) |
|
|
681
693
|
| `L5`, `X2` | `test/quickjs-executor.test.ts` (CPU, heap) |
|
|
682
|
-
| `L6`, `X10` | `test/quickjs-executor.test.ts` (bridge and IPC bounds for arguments and result; the address in the over-bound message) |
|
|
694
|
+
| `L6`, `X10` | `test/quickjs-executor.test.ts` (bridge and IPC bounds for arguments and result; the address in the over-bound message), `test/quickjs-child-stderr.test.ts` (outer reply serialization failure settles the call) |
|
|
683
695
|
| `L7` | `test/execute.test.ts`, `test/executor-admission.test.ts` |
|
|
684
696
|
| `V1`–`V4` | `test/guest-api-contract.test.ts` (dispatched calls, every refusal class including an address no connector owns, the friction each derives, no event for the execution itself), `test/activity.test.ts` (the shared code → friction table, and the identity clamp) |
|
|
685
697
|
| `M1` | `test/guest-api-contract.test.ts` (invalid emits throw catchably, accept nothing), `test/execute-emit.test.ts` (every rejected shape) |
|
|
@@ -690,7 +702,7 @@ the upstream `Executor` shape assignable.
|
|
|
690
702
|
| `M8` | two arms passing one case table, `test/codemode-compat.test.ts` |
|
|
691
703
|
| `M10` | `test/execute-emit.test.ts` (aggregate present, numbers only, absent when nothing emitted) |
|
|
692
704
|
| `X3` | `test/quickjs-executor.test.ts` (cancels a running child) |
|
|
693
|
-
| `X4` | `test/guest-api-contract.test.ts` (string logs only) |
|
|
705
|
+
| `X4` | `test/guest-api-contract.test.ts` (string logs only), `test/quickjs-executor.test.ts` (logs before cancellation), `test/quickjs-child-stderr.test.ts` (crash, shutdown, deadline, IPC failure, bounded parent retention), `test/quickjs-log-limits.test.ts` (unchanged successful logs) |
|
|
694
706
|
| `X6` | `test/quickjs-executor.test.ts` (never-settling await) |
|
|
695
707
|
| `X7` | `P3`'s tests; the Workers superset is deliberately unused |
|
|
696
708
|
|
|
@@ -188,6 +188,21 @@ downstream wrote them, and an unannotated or contradictory one stays
|
|
|
188
188
|
fail-closed onto `call_destructive_tool`. The contract binds the surfaces we
|
|
189
189
|
write, not the catalogs we relay.
|
|
190
190
|
|
|
191
|
+
An `unavailable` failure can carry `details.host` and `details.code` in its
|
|
192
|
+
classified error. `host` is an HTTP(S) origin, including a non-default port,
|
|
193
|
+
with no userinfo, path, query, or fragment and at most 253 UTF-8 bytes. `code`
|
|
194
|
+
is an allowlisted network errno, an undici transport code, or `timeout`, at
|
|
195
|
+
most 32 bytes. Invalid or oversized fields are omitted, never clipped.
|
|
196
|
+
`remoteMcp()` and the guarded transport know the failed fetch destination;
|
|
197
|
+
`api()` can classify a handler's structured runtime code but cannot recover
|
|
198
|
+
its destination from `ctx.baseUrl`, which names Connecta itself. A handler can
|
|
199
|
+
supply a known URL in a typed `ConnectorCallError` and the constructor reduces
|
|
200
|
+
it to an origin. AbortError and TimeoutError become diagnostic `timeout`;
|
|
201
|
+
workerd outbound denial supplies no errno, so a fetch boundary reports the
|
|
202
|
+
known origin alone. No provider prose supplies a code or retryability.
|
|
203
|
+
These diagnostics belong in tool failures and the bounded warning log, never
|
|
204
|
+
in payload-free activity records.
|
|
205
|
+
|
|
191
206
|
## The guarded fetch transport
|
|
192
207
|
|
|
193
208
|
Every hand-written HTTP surface re-derives the same safety machinery, and two
|
|
@@ -237,9 +252,13 @@ What it owns is mechanical and provider-independent:
|
|
|
237
252
|
as an absurd response is a fact about the API, not about HTTP. A declared
|
|
238
253
|
`Content-Length` past the ceiling fails before a byte is read, and a
|
|
239
254
|
streaming body is abandoned at the ceiling rather than buffered past it.
|
|
255
|
+
Without a body stream, text is measured in UTF-8 bytes before it is accepted;
|
|
256
|
+
JSON is parsed from that same bounded text. Such runtimes still buffer their
|
|
257
|
+
read internally, but cannot return an oversized body as a successful result.
|
|
240
258
|
- **Normalization.** An unreachable provider becomes a retryable `unavailable`
|
|
241
259
|
instead of whatever `TypeError` the runtime threw, and `ctx.signal` rides
|
|
242
|
-
every request.
|
|
260
|
+
every request. `Retry-After` hints accept delta-seconds and HTTP dates;
|
|
261
|
+
a date in the past means no further wait.
|
|
243
262
|
|
|
244
263
|
What it deliberately does not own is meaning. It never reads a status code and
|
|
245
264
|
never invents an authentication scheme: the provider's `authenticate` callback
|
|
@@ -288,9 +307,13 @@ Connecta deliberately sits between protocol generations
|
|
|
288
307
|
compatibility concession, so modern protocol support is still discovered.
|
|
289
308
|
- **Legacy sessions:** Connecta's own endpoint creates no protocol session, but
|
|
290
309
|
a stateful legacy downstream can still issue `Mcp-Session-Id`. Closing a
|
|
291
|
-
request scope
|
|
292
|
-
|
|
293
|
-
|
|
310
|
+
request scope, rotating credentials, retiring an OAuth generation, and
|
|
311
|
+
abandoning a connect all send the legacy DELETE before closing the transport.
|
|
312
|
+
The DELETE gets at most one second and failures are logged. Rotation and
|
|
313
|
+
abandoned connects start cleanup without waiting; the connector context has
|
|
314
|
+
no deferred-work hook. Scope teardown gives its bounded tail back to core,
|
|
315
|
+
which can attach the runtime's deferred channel. SDK v2 `Client.close()` does
|
|
316
|
+
not send DELETE on Connecta's behalf.
|
|
294
317
|
- **Modern cache hints:** `tools/list` is deployment-fixed and returns a
|
|
295
318
|
one-hour private cache hint. Downstream hints do not alter Connecta's existing
|
|
296
319
|
five-minute fingerprinted catalog cache; that remains gated in
|
|
@@ -352,7 +375,11 @@ For remote MCP tools, that path checks the catalog's advertised `inputSchema`
|
|
|
352
375
|
before provider dispatch. Supported mismatches become bounded, payload-free
|
|
353
376
|
`invalid_args` findings; a schema the local validator cannot evaluate passes
|
|
354
377
|
through unchanged. Connecta does not parse provider error prose to invent a
|
|
355
|
-
validation classification.
|
|
378
|
+
validation classification. A downstream JSON-RPC `-32602` is an explicit
|
|
379
|
+
`invalid_args` refusal, with a bounded server message and no retry. HTTP 4xx
|
|
380
|
+
refusals preserve a bounded JSON `message`, including `error.message`, and are
|
|
381
|
+
non-retryable; 429 becomes `rate_limited` and 408 becomes `timeout`. The SDK
|
|
382
|
+
still owns OAuth challenges and step-up authorization.
|
|
356
383
|
|
|
357
384
|
## Authentication
|
|
358
385
|
|
|
@@ -363,6 +390,21 @@ server issuer discovered and validated by the SDK; see
|
|
|
363
390
|
The callback route validates `state` before passing the complete callback query
|
|
364
391
|
to the SDK so RFC 9207 `iss` validation is not lost.
|
|
365
392
|
|
|
393
|
+
Within one runtime, overlapping refreshes share one completion gate per
|
|
394
|
+
credential partition and generation. A valid token response is a consumed
|
|
395
|
+
refresh token, so the coordinator keeps that response's tokens on the flight.
|
|
396
|
+
If the owner then fails before the SDK saves them — cancelled, redirected to
|
|
397
|
+
authorization, or invalidated — the host persists the rotation itself, holds
|
|
398
|
+
contenders behind the mutation marker until that write lands, and hands them
|
|
399
|
+
the saved rotation; a retired token is never redeemed twice. A write already
|
|
400
|
+
running in `saveTokens` clears the marker on its own success or failure, and a
|
|
401
|
+
late duplicate save from a detached owner is harmless. Nothing can leave a
|
|
402
|
+
permanent 503 gate ([#526](https://github.com/zackbart/connecta/issues/526)).
|
|
403
|
+
Refresh token response validation
|
|
404
|
+
reads at most 65,536 bytes, and credential reads stop after 64 revision races
|
|
405
|
+
with `temporarily_unavailable` so churn cannot spin indefinitely. Request
|
|
406
|
+
cancellation is checked before each attempt.
|
|
407
|
+
|
|
366
408
|
A remote MCP connector that authenticates with a static key has two ways to
|
|
367
409
|
receive one. `{ type: "headers", headers }` bakes the literal value into the
|
|
368
410
|
deployment file, which suits a secret the runtime already holds.
|
|
@@ -121,15 +121,25 @@ bytes. Within that unchanged total, each enum node and each constraint
|
|
|
121
121
|
annotation may spend at most 256 UTF-8 bytes. Numeric bounds, string length
|
|
122
122
|
bounds, patterns, and formats render beside their type. A constraint that does
|
|
123
123
|
not fit is dropped whole. If constraints push the full shape over 1,024 bytes,
|
|
124
|
-
search retries the shape without them. Compact describe keeps
|
|
125
|
-
constraints
|
|
124
|
+
search retries the shape without them. Compact describe keeps declared
|
|
125
|
+
constraints and property prose within its own 8,192-byte shape cap, sharing
|
|
126
|
+
search's 2,000-visit rendering budget; a capped shape sets
|
|
127
|
+
`inputSchemaTruncated` or `outputSchemaTruncated`, and `format: "json"` or
|
|
128
|
+
JSON search returns the exact schema. A large enum keeps the longest whole-value prefix that fits, then
|
|
126
129
|
adds `unknown` and a comment with the exact omitted-value count. An empty enum
|
|
127
130
|
renders as the valid `never` type. A capped object becomes a valid
|
|
128
131
|
required-first shape with `unknown` types; other shapes become
|
|
129
132
|
`unknown /* truncated */`. Any cap marks the match with
|
|
130
133
|
`inputSchemaTruncated` or `outputSchemaTruncated`; repeat the search with
|
|
131
134
|
`includeSchemas: "json"` or use the existing describe path when exact
|
|
132
|
-
constraints matter.
|
|
135
|
+
constraints matter. `prefixItems` renders as a tuple, with the `items` type
|
|
136
|
+
as its rest, an `unknown[]` rest when open, and no rest for `items: false`.
|
|
137
|
+
`dependentSchemas` and `if`/`then`/`else` preserve the base shape and append
|
|
138
|
+
`/* conditional */`, setting the truncation flag so the caller reads the exact
|
|
139
|
+
JSON schema. `$dynamicRef` resolves a same-named definition like `$ref`; an
|
|
140
|
+
unresolved dynamic reference renders as `unknown` with the truncation flag.
|
|
141
|
+
These shapes share the same byte and work budgets. Small enums and both exact
|
|
142
|
+
paths remain complete.
|
|
133
143
|
|
|
134
144
|
## Connector guide selection
|
|
135
145
|
|
|
@@ -206,12 +216,42 @@ removing or summarizing the text copy is deferred until host-forwarding
|
|
|
206
216
|
measurements demonstrate that supported clients do not need it.
|
|
207
217
|
|
|
208
218
|
Plain-text guidance and errors remain text-only. A downstream MCP tool's native
|
|
209
|
-
content blocks
|
|
210
|
-
|
|
219
|
+
content blocks pass through when `call_tool` uses MCP result mode. When no
|
|
220
|
+
text block exists and `structuredContent` is present, Connecta appends a text
|
|
221
|
+
block containing its compact JSON, then applies the same content size guard.
|
|
222
|
+
This preserves structured-only results, including `null`, arrays, and scalars.
|
|
223
|
+
An existing text mirror stays unchanged; Connecta does not add another copy. Newly stashed JSON and
|
|
211
224
|
downstream content envelopes use compact serialization, so `get_result` byte
|
|
212
225
|
offsets and totals refer to that exact compact text.
|
|
213
226
|
|
|
214
|
-
|
|
227
|
+
The direct-call stash keeps results for 15 minutes. `results.maxStashBytes`
|
|
228
|
+
defaults to 8 MiB and `results.maxStashEntries` to 64 per `createConnecta`
|
|
229
|
+
runtime, shared across all subjects and pools. Both accept non-negative safe
|
|
230
|
+
integers; zero disables stashing. The byte budget counts the stored ASCII
|
|
231
|
+
paging envelope, including base64 overhead, rather than only the result text.
|
|
232
|
+
Capacity is reserved before each storage write, so concurrent requests cannot
|
|
233
|
+
oversubscribe it. A full stash refuses new entries. A later stash attempt
|
|
234
|
+
deletes expired entries before reusing their capacity; a failed deletion keeps
|
|
235
|
+
the charge. These bounds cover writes by this runtime, not other processes,
|
|
236
|
+
Worker isolates, or entries left by a previous runtime.
|
|
237
|
+
|
|
238
|
+
Results belong to the authenticated subject whenever auth supplies a subject
|
|
239
|
+
or user id, independently of activity configuration. The provider's namespace
|
|
240
|
+
is used when present; otherwise the namespace is `connecta:auth:<provider kind>`.
|
|
241
|
+
Keep subject ids distinct within that namespace. An explicit principal is the
|
|
242
|
+
fallback subject when neither id is supplied. Open deployments and auth
|
|
243
|
+
providers that supply no identity share one partition.
|
|
244
|
+
|
|
245
|
+
New entries store UTF-8 bytes in a base64 envelope. After the KV read,
|
|
246
|
+
`get_result` decodes only the requested byte range and a few boundary bytes;
|
|
247
|
+
it does not encode the full text on every page. Storage still reads one full
|
|
248
|
+
value. Pre-upgrade raw-text entries remain readable during their TTL using
|
|
249
|
+
one full encoding per page. Offsets and `totalBytes` always describe the
|
|
250
|
+
original UTF-8 text, not the envelope. A supplied offset inside a character
|
|
251
|
+
moves back to its start; page ends also align to character boundaries, and a
|
|
252
|
+
page smaller than one character widens just enough to make progress.
|
|
253
|
+
|
|
254
|
+
A successfully stashed `call_tool` truncation notice carries both the historical `resultId` and an
|
|
215
255
|
exact `nextAction: { tool: "get_result", arguments: { id, offset: 0 } }`. The
|
|
216
256
|
handle is therefore
|
|
217
257
|
directly actionable without copying an identifier out of prose. Program results
|
|
@@ -219,6 +259,28 @@ and oversized discovery responses carry no such route — paging a program's
|
|
|
219
259
|
return value is a refused shape, because a program can shrink anything before
|
|
220
260
|
it returns.
|
|
221
261
|
|
|
262
|
+
A refused stash or failed stash write cannot undo a downstream success. Both direct call tools
|
|
263
|
+
return a truncated preview where usable, with a paging-unavailable notice and
|
|
264
|
+
no `resultId` or paging action. Activity records success; the operator logger
|
|
265
|
+
receives a fixed warning with the connector and tool, without storage error
|
|
266
|
+
prose. For read-only work, reduce the result inside `execute_code`; repeating
|
|
267
|
+
an approved write is not a way to recover its output. Other result-processing
|
|
268
|
+
failures use a fixed `result_processing_failed` message and are never retryable.
|
|
269
|
+
|
|
270
|
+
Downstream MCP `isError` text is bounded at its source to 512 UTF-8 bytes plus
|
|
271
|
+
an `…` marker. Error framing may shorten it further to fit the call's result
|
|
272
|
+
cap, counting JSON escaping and both copies in value mode.
|
|
273
|
+
|
|
274
|
+
The top-level discovery ceiling is 256,000 UTF-8 bytes for the serialized tool
|
|
275
|
+
result, including text, `structuredContent`, and JSON escaping. Measuring only
|
|
276
|
+
one copy would advertise half the bytes the adapter actually returns.
|
|
277
|
+
|
|
278
|
+
A per-call `timeoutMs` covers catalog resolution, admission, and connector
|
|
279
|
+
execution with one deadline. The admission queue's own timeout may expire
|
|
280
|
+
sooner, but it cannot extend the call deadline. Result processing happens after
|
|
281
|
+
that deadline ends, because a completed downstream call must not turn into a
|
|
282
|
+
retryable timeout while Connecta prepares its response.
|
|
283
|
+
|
|
222
284
|
## Lexical discovery
|
|
223
285
|
|
|
224
286
|
`search_tools` tokenizes tool names and descriptions at punctuation and
|
|
@@ -373,6 +435,12 @@ recovery query, each of which lands in both the text content and
|
|
|
373
435
|
the address is the thing being corrected, a clipped one still identifies the
|
|
374
436
|
mistake, and a short one — every real one — comes back exact and untagged.
|
|
375
437
|
|
|
438
|
+
Unknown `get_result.id`, `authorize_connector.connector`, and `skills.name`
|
|
439
|
+
echoes use the same 512-byte clamp. `search_tools.connector` instead rejects
|
|
440
|
+
values over 512 UTF-8 bytes with `invalid_args` before catalog lookup, so a
|
|
441
|
+
clipped scope can never select a different connector. A failed result-storage
|
|
442
|
+
read returns typed `unavailable` without exposing backend error text.
|
|
443
|
+
|
|
376
444
|
`call_destructive_tool` accepts an optional
|
|
377
445
|
`reason` of at most 500 characters for the host's human approval view. It is
|
|
378
446
|
outer-call context only: Connecta neither treats it as authority nor passes it
|