@zackbart/connecta 0.38.0 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +91 -0
- package/README.md +3 -2
- package/bin/connecta.mjs +4 -8
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.js +6 -4
- package/dist/agent-routes.js +231 -0
- package/dist/auth/downstream-oauth/consent.js +519 -0
- package/dist/auth/downstream-oauth/grant-store.js +439 -0
- package/dist/auth/downstream-oauth/refresh-coordinator.js +562 -0
- package/dist/auth/downstream-oauth/refresh-records.js +447 -0
- package/dist/auth/downstream-oauth/sdk-hooks.js +242 -0
- package/dist/auth/downstream-oauth.js +66 -2382
- package/dist/auth/native.js +37 -0
- package/dist/auth/oauth-grant.js +13 -0
- package/dist/auth/static-oauth.js +153 -131
- package/dist/auth-elicitation.js +7 -1
- package/dist/authorization-handoff.js +11 -5
- package/dist/catalog-cache.js +219 -59
- package/dist/catalog-policy.js +18 -0
- package/dist/catalog-service.js +284 -47
- package/dist/catalog.js +18 -2
- package/dist/config.d.ts +16 -11
- package/dist/config.js +9 -6
- package/dist/connector-caller.d.ts +1 -1
- package/dist/connector-health.js +59 -0
- package/dist/connector-status.js +117 -0
- package/dist/connectors/api-connector.d.ts +7 -2
- package/dist/connectors/api-connector.js +7 -4
- package/dist/connectors/negotiation-cache.js +22 -4
- package/dist/connectors/remote-mcp/call.js +164 -0
- package/dist/connectors/remote-mcp/catalog.js +278 -0
- package/dist/connectors/remote-mcp/config.js +263 -0
- package/dist/connectors/remote-mcp/connection.js +682 -0
- package/dist/connectors/remote-mcp/errors.js +488 -0
- package/dist/connectors/remote-mcp/oauth.js +473 -0
- package/dist/connectors/remote-mcp/resources.js +473 -0
- package/dist/connectors/remote-mcp/runtime.js +1 -0
- package/dist/connectors/remote-mcp/transport.js +555 -0
- package/dist/connectors/remote-mcp.js +62 -2863
- package/dist/credential-contract.d.ts +23 -0
- package/dist/credential-reads.js +17 -0
- package/dist/credential-verification.js +53 -0
- package/dist/credentials.d.ts +7 -0
- package/dist/credentials.js +60 -25
- package/dist/describe-config.d.ts +12 -2
- package/dist/describe-config.js +2 -0
- package/dist/discovery-health.js +199 -0
- package/dist/downstream-input.js +2 -0
- package/dist/errors.d.ts +55 -17
- package/dist/errors.js +95 -41
- package/dist/execute.js +152 -114
- package/dist/executor-result.js +3 -1
- package/dist/guest-functions.js +83 -0
- package/dist/guest-runtime.js +60 -19
- package/dist/guest-types.d.ts +19 -5
- package/dist/http/argument-problems.js +35 -0
- package/dist/http/errors.js +5 -4
- package/dist/http/fields.js +192 -13
- package/dist/http/hatch-response.js +73 -0
- package/dist/http/hatch.js +88 -23
- package/dist/http/oauth.js +3 -2
- package/dist/http/provider.js +32 -4
- package/dist/http/safety.js +8 -0
- package/dist/http/tools.js +44 -23
- package/dist/index.d.ts +1 -1
- package/dist/index.js +9 -2
- package/dist/invocation.js +26 -8
- package/dist/membership.d.ts +13 -5
- package/dist/membership.js +34 -2
- package/dist/meta-tools.js +54 -31
- package/dist/native-recovery.js +4 -16
- package/dist/native-tools.js +5 -2
- package/dist/oauth-sealing.js +10 -0
- package/dist/operation-vocabulary.js +135 -13
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +1 -0
- package/dist/operator-ui/contract.d.ts +10 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/model.d.ts +22 -1
- package/dist/provider-call-facts.js +6 -2
- package/dist/providers/_shared/rest/operation-index.js +48 -50
- package/dist/providers/_shared/rest/value-safety.js +80 -0
- package/dist/providers/breeze/api.js +7 -1
- package/dist/providers/breeze/skill.generated.js +1 -1
- package/dist/providers/breeze/tools.js +4 -3
- package/dist/providers/ccb/api.js +2 -1
- package/dist/providers/ccb/index.js +5 -0
- package/dist/providers/ccb/skill.generated.js +1 -1
- package/dist/providers/cloudflare/api.js +8 -1
- package/dist/providers/cloudflare/skill.generated.js +1 -1
- package/dist/providers/docs/lib/schemas.js +24 -22
- package/dist/providers/docs/safety.js +2 -0
- package/dist/providers/docs/skill.generated.js +1 -1
- package/dist/providers/docs/tools.js +2 -4
- package/dist/providers/drive/api.js +1 -0
- package/dist/providers/drive/lib/contracts.js +8 -5
- package/dist/providers/drive/skill.generated.js +1 -1
- package/dist/providers/drive/tools.js +11 -16
- package/dist/providers/forms/api.js +1 -0
- package/dist/providers/forms/skill.generated.js +1 -1
- package/dist/providers/forms/tools.js +10 -7
- package/dist/providers/github/api.js +1 -0
- package/dist/providers/github/lib/discovery.js +1 -1
- package/dist/providers/github/skill.generated.js +1 -1
- package/dist/providers/github/tools.js +61 -17
- package/dist/providers/gmail/lib/schemas.js +60 -50
- package/dist/providers/gmail/skill.generated.js +1 -1
- package/dist/providers/gmail/tools.js +7 -5
- package/dist/providers/granola/index.js +2 -0
- package/dist/providers/granola/skill.generated.js +1 -1
- package/dist/providers/overflow/api.js +7 -1
- package/dist/providers/overflow/skill.generated.js +1 -1
- package/dist/providers/planning-center/api.js +3 -1
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/planning-center/skill.generated.js +1 -1
- package/dist/providers/quickbooks/safety.js +80 -39
- package/dist/providers/quickbooks/skill.generated.js +1 -1
- package/dist/providers/sheets/api.js +1 -0
- package/dist/providers/sheets/skill.generated.js +1 -1
- package/dist/providers/sheets/tools.js +13 -14
- package/dist/providers/slides/skill.generated.js +1 -1
- package/dist/providers/slides/tools.js +15 -13
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/tithely/api.js +3 -1
- package/dist/providers/tithely/skill.generated.js +1 -1
- package/dist/providers/vercel/api.js +1 -1
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/providers/vercel/tools.js +4 -3
- package/dist/registry.d.ts +10 -4
- package/dist/registry.js +41 -13
- package/dist/routes/credentials.js +129 -60
- package/dist/routes/dispatch.js +5 -1
- package/dist/routes/mcp.js +30 -14
- package/dist/routes/operator.js +2 -2
- package/dist/routes/shared.d.ts +6 -1
- package/dist/routes/shared.js +7 -1
- package/dist/routes/ui-config.js +2 -2
- package/dist/routes/ui-data.js +126 -20
- package/dist/routes/ui.js +16 -0
- package/dist/runtime/call.js +7 -2
- package/dist/runtime/native-storage.js +12 -0
- package/dist/runtime/request-reads.js +207 -0
- package/dist/runtime/request-scope.js +16 -0
- package/dist/runtime/storage.js +12 -0
- package/dist/skills.js +37 -20
- package/dist/storage/keys.js +53 -3
- package/dist/storage/memory.js +8 -2
- package/dist/storage/sql-native.js +53 -1
- package/dist/storage/sql.js +12 -2
- package/dist/storage-digest.js +32 -0
- package/dist/timeout.js +8 -0
- package/dist/types.d.ts +33 -0
- package/dist/usage-guide.js +16 -35
- package/dist/validate.d.ts +3 -0
- package/dist/validate.js +220 -158
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/write-outcome.js +0 -3
- package/documentation/README.md +13 -13
- package/documentation/architecture.md +385 -20
- package/documentation/auth.md +233 -64
- package/documentation/code-mode.md +122 -39
- package/documentation/deploying.md +6 -3
- package/documentation/integrating.md +16 -2
- package/documentation/meta-tools.md +240 -49
- package/documentation/migration-0.33.md +5 -1
- package/documentation/migration-0.34.md +3 -1
- package/documentation/operator-tests.md +1 -1
- package/documentation/operator-ui.md +105 -12
- package/documentation/pool-membership-upgrade.md +44 -0
- package/documentation/providers.md +105 -0
- package/examples/worker/AGENTS.md +10 -10
- package/examples/worker/README.md +4 -3
- package/examples/worker/src/connecta.config.ts +1 -0
- package/guides/connecting-agents.md +2 -1
- package/guides/how-it-works.md +4 -2
- package/package.json +1 -1
- package/templates/node/AGENTS.md +13 -12
- package/templates/node/package.json +1 -1
- package/templates/node/src/connecta.config.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,97 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this package are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.40.0 — 2026-11-02
|
|
6
|
+
|
|
7
|
+
This release makes warm Connecta calls on Workers cheaper in storage and closes a credential gap on OAuth remote MCP connectors. Deployments need no configuration change or migration, unless they use the Eve helper.
|
|
8
|
+
|
|
9
|
+
**Eve helper:** `@zackbart/connecta-eve` now requires exactly Eve 1.0.0-rc.1. Eve 0.90 apps must upgrade Eve together with the helper and, when installed, upgrade `@vercel/connect` to `^2.7.0`. The core package and non-Eve deployments are not affected by this change.
|
|
10
|
+
|
|
11
|
+
**Security:** an OAuth remote MCP connector no longer sends a passive handshake, catalog request or tool call without a credential if its tokens are removed mid-request. It returns OAuth recovery before sending instead. Before this, a server accepting anonymous requests could receive that request without the connector's credential.
|
|
12
|
+
|
|
13
|
+
**Native pools:** native pools now always list `authorize_connector`, so an agent on a pool without `execute_code` can get the operator a handoff for a connector that needs authorization. On those pools the repaired connector's tools appear only after the host refreshes its tool list, usually by reconnecting; pools with `execute_code` can reach them from a program straight away. Anything that compares a native pool's tool list exactly will see one more tool. Meta endpoints keep the same six tools.
|
|
14
|
+
|
|
15
|
+
**Storage work per call:** warm remote MCP catalog resolution now takes four D1 round trips instead of six on modern servers and five on legacy ones. Warm agent admission takes two reads instead of four. Warm OAuth calls read the grant 9 times instead of 11 on modern servers and 12 instead of 13 on legacy ones. A warm isolate reuses a stored catalog it has already validated, after the same freshness and partition checks. The catalog and admission round trips were measured with injected latency on local workerd, and the OAuth counts come from request-budget tests. None of this has been measured in production.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Read agent token admission state in one fresh D1 or SQLite statement, reducing warm admission and last-seen observation from four reads to two while preserving next-request rotation and revocation enforcement.
|
|
20
|
+
- **Breaking:** `@zackbart/connecta-eve` now requires exactly Eve 1.0.0-rc.1; Eve 0.90 apps must upgrade Eve together with the helper and, when installed, upgrade `@vercel/connect` to `^2.7.0`. Align installed-consumer probes and upgrade guidance with the tested release candidate. Later candidates and stable 1.0 require a compatibility check.
|
|
21
|
+
- Reduce grant reads on warm OAuth remote MCP calls while retaining fresh catalog acceptance, disconnect, token rotation and dispatch checks. Keep the separate invocation token read so same-epoch invalidation refuses writes with OAuth recovery before dispatch, and expiry changes are observed before early renewal.
|
|
22
|
+
- Reuse validated stored catalogs within a warm isolate after checking stored chunks, partition identity, expiry and the fresh generation fence. Retained facts are immutable and bounded; credential redaction still runs per request.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- Reduce warm remote MCP catalog storage round trips by batching locating reads and private/public manifest probes while preserving fresh generation and OAuth credential acceptance checks.
|
|
27
|
+
- Keep authorization recovery available on native pools without code by listing authorize_connector with the granted connector ids, even when a connector's catalog cannot load.
|
|
28
|
+
|
|
29
|
+
### Security
|
|
30
|
+
|
|
31
|
+
- Require a credential on passive OAuth remote MCP sends, including handshakes, catalog requests and tool dispatches, regardless of provider replacement. Return OAuth recovery before sending when the SDK cannot obtain one, and count a verified unsent write as failed without reconciliation. Explicit authorization flows retain their challenges; writes that may already have left remain unknown.
|
|
32
|
+
|
|
33
|
+
## 0.39.0 — 2026-10-30
|
|
34
|
+
|
|
35
|
+
This release makes Connecta faster and leaner for agents and operators on Cloudflare Workers. A warm search over remote MCP connectors with cached catalogs no longer reconnects to them: an all-legacy warm search makes no downstream requests. Repeated storage reads are memoized per request, and catalog chunks are read in batches. OAuth tokens refresh before they expire instead of failing the first write after expiry. Legacy negotiation verdicts last six hours, with bounded recovery when a server changes protocol era. A connector that recently hung or failed slowly is reported in `catalogErrors` and skipped by unscoped search for about a minute, and unscoped discovery has a total budget. With one hung connector, three consecutive searches went from 30 s each to 5 s, 11 ms and 8 ms.
|
|
36
|
+
|
|
37
|
+
The operator UI shows stored connector status at once, then refreshes it with a live read from the browser. Detail reads have an 8 s deadline, load eight at a time, and park a connector that doesn't respond instead of retrying it repeatedly. A credential can be tested before it is saved, and failures are reported as rejected, scope, unreachable, timeout or failed. A credential connector's status shows when it was last verified.
|
|
38
|
+
|
|
39
|
+
Agents get smaller, clearer responses. The `execute_code` description declares the guest API's shapes. `invalid_args` returns bounded validation findings and a schema-generated example when available; API hatches point to operation details instead. Errors never echo submitted strings or object keys; non-sensitive numeric bounds may include the submitted number. Each `fields` description says where its paths start. `*_api_details` returns the operation's response fields, and catalog search can return a fully determined API read as a ready call. Search rows are smaller, program searches default to TypeScript signatures, and tests cap the size of the always-loaded surface. Meta-tool names in agent text follow the serving surface, so native pools read correctly. Remote cache partitions use a deployment key derived by `encryptedCredentialVault()`, or supplied by a custom vault's `storageDigestKey()`. Without that method, partitions remain exact but cached entries are reused only within one process or isolate.
|
|
40
|
+
|
|
41
|
+
Breaking changes:
|
|
42
|
+
- Pools must list only people or only agents. Split mixed pools before upgrading ([Split mixed pools](./documentation/pool-membership-upgrade.md)).
|
|
43
|
+
- Search rows move `connectorTitle`, `guide` and `guideSummary` into a page-level `connectors` map.
|
|
44
|
+
- Programs that read JSON schemas from a default `connecta.search` must pass `includeSchemas: "json"`.
|
|
45
|
+
- `invalid_args` drops `repair` and `issues[].code` in favour of `validation`.
|
|
46
|
+
- The published `GuestApi.emit` type no longer accepts a paged handle.
|
|
47
|
+
- `@zackbart/connecta-eve` requires Eve `>=0.90.0 <0.91.0`; Eve apps upgrade together and remove their undici override.
|
|
48
|
+
|
|
49
|
+
Plain `/mcp` now routes a person to their single pool. Each remote connector refills its catalog cache once after upgrade. Before upgrading, split mixed pools, update clients that consume the changed response shapes or guest types, and upgrade Eve apps as described above. Other deployments need no configuration changes.
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
|
|
53
|
+
- Credentials can be tested before they are saved. `PUT /ui/credentials/<id>` with `verify: "test"` tests the candidate in memory under `discovery.probeTimeoutMs` and stores it only if the test passes; `verify: "skip"` (the default) stores it unverified, so an outage never blocks a replacement. Tests answer a typed failure class (`rejected`, `scope`, `unreachable`, `timeout`, `failed`) instead of a bare `ok`, `CredentialTestResult` gains an optional `failure`, and the operator page shows a stored last-verified record for credential connectors, bound to the revision each vault save now seals, so a concurrent replacement is never shown as verified. Vaults gain an optional `snapshot()` and per-save `revision` metadata. `remoteMcp()` credential tests now test the value they are given rather than refusing an unsaved one.
|
|
54
|
+
- `eval:agent --target worker` runs the agent tasks and graders against the production Worker shape (local D1, the Dynamic Worker executor and Access) under `wrangler dev`, refuses the tasks it cannot serve, records the target in every result, and pairs node with worker results only through `eval:compare --cross-target`.
|
|
55
|
+
- The agent eval harness reports efficiency medians, response bytes per tool, guide fetches and pass@k/pass^k reliability in `eval:compare`, the `eval:agent` end-of-batch summary and the HTML report.
|
|
56
|
+
- `eval:agent` and `eval:smoke` take opt-in `--latency-ms`, `--hung-connector`, `--unauthorized-connector` and, on workerd, `--d1-latency-ms`. The settings are recorded in every result, and `eval:compare` refuses to pair runs whose injection differs.
|
|
57
|
+
- Standard `*_api_details` answers now carry a capped `response` section: the operation's declared success-response paths, listed in the coordinates the read and write tools' `fields` argument takes, with the anchor stated, so programs can write `fields` filters without a sampling read. It lists at most 100 paths and 4,096 characters of path JSON, with an explicit `omitted` count. It promises no readability: value safety may still withhold or redact any value at read time. A best-effort `withheld` hint names the listed paths its rules always withhold.
|
|
58
|
+
- Search returns fully determined API reads as `readyCalls` on the first page: `{ address: "<id>.<x>_api_read", args: { path }, classification: "read", summary }`, ready to pass unchanged to `connecta.call` or `call_tool`. Only a reviewed GET served by the hatch's read tool with no path template, required parameter or required body qualifies, and every object word of the query must belong to a whole resource name, a path segment or the operationId, stated in order (summaries and parts of compound segments never count); an exact name, or a complete authored tool whose name adds nothing to the query, suppresses ready calls; anything needing an id, method choice or argument still routes through `_api_search` and `_api_details`. Ready calls pass the same endpoint, connector, safety, hidden-tool and grant checks as other discovery, are computed offline, and offer only the top-ranked operation per connector, at most three per page. Listing Granola notes, Vercel deployments or Stripe customers now takes two host calls instead of four.
|
|
59
|
+
|
|
60
|
+
### Changed
|
|
61
|
+
|
|
62
|
+
- The `execute_code` connector inventory names every configured connector instead of truncating with `+N more` at 256 bytes. Its cap is now 1,024 bytes; titles appear only where they say more than the id, and past the cap titles, then optional guide pointers, give way before whole entries do. Guide pointers read as the call to make: `read first: skills({name:"connector:<id>"})` for a required guide, `guide: skills({name:"connector:<id>"})` otherwise. `skills({ name })` and `connecta.skill(name)` serve a connector's guide by its bare id; `usage` and `investigate` stay reserved for the built-in skills. Program errors name the engine error once (`Program TypeError: …`, not `Program TypeError: TypeError: …`), and a failed program that read a result field such as `data` or `tools` off an unawaited `connecta.*` call, detected at runtime, gets the hint `connecta.* returns a Promise; await it`.
|
|
63
|
+
- The always-loaded agent surface is smaller and capped in CI. `search_tools`'s output schema defines its typed failure and authorization handoff once under `$defs` instead of once per field (4,642 to 3,622 characters), with every handoff field still typed. The full routing rule is stated once, in the server instructions; `call_tool` and `search_tools` describe only their own scope, and the `usage` skill points to the instructions instead of repeating a routing table. The built-in `investigate` skill is now domain-neutral guidance for investigations spanning several services, and the `execute_code` description no longer advertises it; `skills` still lists it.
|
|
64
|
+
- Search and catalog reads serve a remote MCP connector's complete, unexpired cached catalog without connecting. A warm search over cached legacy (2025, stateful-session) connectors no longer pays an `initialize`, `notifications/initialized`, standalone SSE `GET` and session `DELETE` per connector. A miss, expiry, explicit `ttlMs: 0`, invalidation, or credential, token or OAuth epoch change still connects. The read never seals, binds or refreshes an OAuth grant and never claims a cache generation. A cached catalog does not validate the downstream credential: an expired or revoked credential surfaces on the next call, resource read or credential test, not in search.
|
|
65
|
+
- Repeated storage reads within one request are shared where staleness inside that request is safe, and catalog chunks are read together. The shared reads are the protocol-version verdict, catalog generation reads that only locate an entry, and the credential reads behind status, drift and the operator UI card. OAuth tokens and epochs, refresh and compare-and-set reads, disconnect checks, the catalog generation fence and the credential a call sends still read storage every time; any write in the same isolate clears the shared value at once, and runtimes without weak references share nothing. On D1, a warm OAuth remote MCP call drops from 21 to 18 queries, an expired-token refresh from 51 to 48 statements, a cold-isolate catalog load from 8 to 5, and an operator UI connector detail from 12 to 8.
|
|
66
|
+
- Unscoped search now remembers a connector whose discovery timed out or failed slowly and skips it for up to 60 seconds, reporting it in `catalogErrors` with `retryAfterMs`. Each connector gets at most 5 seconds and the whole fan-out ends within a 9-second budget. Records are typed facts partitioned by owner and configuration, lapse on a credential change, and clear on recovery. Scoped search, describe and calls still reach the connector. Records are kept per owner, admitted endpoint and value-free described configuration, and health storage starts only within the discovery budget and is always awaited. A healthy connector whose catalog always takes more than 5 seconds can stay out of unscoped search until a scoped search or another longer-deadline operation warms its complete catalog.
|
|
67
|
+
- **Breaking:** `@zackbart/connecta-eve` now requires Eve 0.90 (`>=0.90.0 <0.91.0`) and is tested against Eve 0.90.0; Eve 0.86 apps must upgrade Eve to 0.90.0 together with the helper, including Eve's own 0.87–0.90 breaking changes to app code. Eve 0.90.0 pins undici 8.12.3, so remove the app's `eve` undici override (npm `"eve": { "undici": "8.12.3" }`, pnpm `"eve@0.86.0>undici": "8.12.3"`) after upgrading.
|
|
68
|
+
- Every `fields` argument now states where its paths start, with one valid example, in its schema description: `the result` for named reads, `each item in items` for named lists and searches, and `the response body in data` for `*_api_read` and `*_api_write`. Anchors are unchanged. Below the first segment, named tools refuse an undeclared property only inside an object their schema closes with `additionalProperties: false`, with `invalid_args` naming the entry position, the segment and the declared properties; first-segment checks are unchanged. A declared path the response lacks is never refused. Hatch reads and writes still check only the first segment before dispatch, and now return `warnings` naming each `fields` entry that matched nothing in a nonempty response, with the properties the operation declares where matching stopped and never the response's own keys. Warnings are bounded to 10 of at most 512 characters and 4,096 characters together, reviewed vendor warnings included, followed by an omitted count.
|
|
69
|
+
- **Breaking:** The `execute_code` description now declares compact shapes for the guest API's arguments and results instead of naming undeclared types, including the exact authored block variants `emit` accepts and the native blocks it forwards from `result.content`. It explains in plain sentences that search and describe return objects, that a call result over 1 MiB returns a paged handle, and that every guest function except `emit` spends a host call. The published `GuestApi` types `describe`'s `address` as `string` and `addresses` as `string[]`, and its `emit` no longer accepts a paged handle, which the host always refused.
|
|
70
|
+
|
|
71
|
+
Migration: TypeScript programs that pass a `connecta.call` result straight to `connecta.emit` must first narrow it, because a call result may be a paged handle: check `result.format !== "paged"` before emitting, and page and reduce a paged result with `connecta.result` instead. Runtime behaviour is unchanged.
|
|
72
|
+
- Guest functions are listed once in an internal registry. It now generates the guest runtime's `connecta` namespace, the execute_code budget admission for each function, and the compact guest declaration in the execute_code description. The guest runtime source, the execute_code description, the usage skill and `tools/list` are unchanged byte for byte, and so is `GuestApi`.
|
|
73
|
+
- Remote MCP catalog-cache and negotiation-verdict partitions, and connector-health records, are now deployment-keyed. Their inputs enter stored keys through HMAC-SHA-256 under a key that is never stored. Those inputs are each connector's cache configuration, including every configured URL in full and its header values, plus operator-managed and request-resolved credentials, custom connector credential values, OAuth token responses and the described configuration. `encryptedCredentialVault()` derives that key from its credential key and exposes it through a new optional host-only vault method, `storageDigestKey()`. Without a vault, or with a custom vault that lacks the method, each process or isolate uses its own random key, so cached remote catalogs, verdicts and health records are reused within that process or isolate only. Partitioning by owner, configuration, credential, token and epoch is unchanged. After the upgrade, each remote connector refills its cache once with one handshake and catalog listing. Earlier entries are not read and expire on their own TTLs.
|
|
74
|
+
- A remote MCP connector keeps a legacy negotiation verdict for six hours instead of five minutes. A legacy server therefore no longer pays a `server/discover` probe and a verdict write every five minutes; that was 11 probes in an hour of once-a-minute calls. Era changes are still noticed. A server that stops answering the legacy handshake is renegotiated in the same connect. A server that adds the modern era but still answers the legacy handshake is adopted when the verdict expires. A modern verdict still lasts five minutes. A downgraded server's first refusal now deletes it, so the next call renegotiates instead of failing until the verdict expires. A refusal here means an HTTP 400 whose body is empty or is not a recognized modern error; a modern server's `HeaderMismatch`, `MissingRequiredClientCapability` or `UnsupportedProtocolVersion` keeps the verdict. A remote MCP write answered with a 4xx that carries no JSON-RPC error is now reported as an unknown outcome rather than a known refusal.
|
|
75
|
+
- **Breaking:** Every `invalid_args` now repairs the call in one turn. `validation` is the single recovery envelope: up to eight `{ path, expected, received? }` findings that state the actual bound, allowed values, accepted keys or selector shape (`/limit: expected at least 5, received 4`), plus one valid `example` synthesized from the published schema. A refusal never contains a submitted string or object key: paths name only declared properties and indices (`<key>` otherwise), and schema literals at `writeOnly` positions are never rendered. API hatches withhold examples and point to `*_api_details`; `conditionalRequirements` no longer renders `if`/`then`/`else` conditions. Maintained providers' own checks report every problem at once, starting with GitHub `search_scoped` and `list_scopes` and every `fields` path. Responses no longer carry `repair` or `validation.issues[].code`; custom connectors may still pass both to `ConnectorCallError`, which folds `repair` into `validation`.
|
|
76
|
+
- Contributors: a maintained provider keeps its eval fake, tasks and deployment wiring in its own unshipped `src/providers/<name>/eval/` folder, and `providers:generate` registers it with the eval harness and appends a `documentation/providers.md` setup skeleton for a provider without one. Granola moved to this layout; adding a provider now needs no shared eval, Vitest or task-list edit. No published API or package content changes.
|
|
77
|
+
- Contributor tests now hold downstream-request, D1 round-trip and SQL-statement ceilings for the common request shapes (warm and cold-isolate remote MCP calls with header auth and OAuth, refresh after a 401 and before expiry, an `api()` credential call, an unscoped warm search, a five-call program and operator UI connector detail reads) on a production-shaped fixture with a sealed vault, Cloudflare Access and a D1 activity sink. Runtime behavior is unchanged.
|
|
78
|
+
- Plain `/mcp` now admits a non-maintainer person listed in exactly one pool and serves them that pool exactly as `/mcp/<pool>` does: its tools, `readOnly`, `surface`, `executeCode` and `description`. Maintainers keep the whole catalog; zero or several memberships keep the same 404, logged as `not_maintainer` or the new `ambiguous_pool`, and construction warns about people in several pools, also listed in `describeConfig().peopleInSeveralPools`. Result handles and auth or downstream-input continuations are bound to both the requested path and the admitted pool, so handles minted before this release no longer page. The member page shows `/mcp` as a single-pool member's address.
|
|
79
|
+
- Connecta-authored agent text and maintained providers name routes with the names of the surface serving them, resolved when the text is built, instead of spelling meta-tool names. The six-tool surface is byte-identical. Native pools no longer rewrite a caller's unknown tool name, programs on native pools see native recovery wording in failures they catch, and native pools serve provider notes, schema descriptions and authorization errors in complete native phrases with that pool's paging. Custom connectors can build such text from the new optional `ConnectorContext.routes` (`AgentRouteNames`).
|
|
80
|
+
- **Breaking:** Search pages state connector facts once: `connectorTitle`, `guide` and `guideSummary` moved off every tool row into a page-level `connectors` map keyed by connector id (`connectors[id].title`, `.guide`, `.guideSummary`); rows keep `guideRequired` and `guideRequiredReasons`. In-program `connecta.search` now defaults to `includeSchemas: "typescript"` (one `signature` per row, about half the bytes of JSON); pass `includeSchemas: "json"` for JSON Schema. Top-level `search_tools` still omits schemas by default. A query led by `list`, `get`, `find`, `search`, `show`, `read`, `fetch` or `count` ranks read rows above write rows within each relevance tier without hiding writes. Maintained providers may declare `searchSynonyms`, groups of interchangeable lowercase words search matches lexically for that provider only; Church Community Builder declares `person`/`individual` and `household`/`family`, and Granola `meeting`/`note`.
|
|
81
|
+
- **Breaking:** Require every pool to list only people or only agents. Mixed membership now throws ConfigError at construction, including personal pools with or without an owner. Split shared team pools into people-only and agent-only pools sharing a tool list; personal agent pools list only the agent. See documentation/pool-membership-upgrade.md.
|
|
82
|
+
- An unchanged remote catalog refresh no longer rewrites its drift baseline. Zero-TTL listings, the SDK server default, previously rewrote the hash-only baseline (one chunk and one compare-and-set manifest) on every call. A refresh now reads the baseline first and skips the write when the digest matches a baseline written within the last hour. Changed catalogs still write their baseline and report drift. A zero TTL is still never reused, and its listing still deletes any previously cached entry.
|
|
83
|
+
- Split `remoteMcp()` and the downstream OAuth provider into per-seam internal modules, defining each native OAuth method once. Behavior and published declarations are unchanged.
|
|
84
|
+
- The operator page shows each connector's last observed status at once, labelled with its age, then checks it live in a separate request. A live detail read records one status observation per connector and viewer (state, problem code, tool count, time; 120-second TTL) beside discovery health. `GET /ui/connectors/:id?observation=stored` answers from it without probing, with permissions, credential cards and auth links still computed fresh. A stored timeout stays parked until Refresh.
|
|
85
|
+
- The operator page now reads up to eight connector details at once instead of four, so a page of up to eight connectors loads in one round. With 500 ms of downstream latency, a 10-connector page settles in about 2.4 s warm and 3.6 s cold instead of 3.0 s and 5.0 s. Each tab still reads every connector once per load, and detail reads do not count against `admission.requests`.
|
|
86
|
+
- Operator-page connector details stop waiting on a downstream after 8 seconds, or after `discovery.probeTimeoutMs` when that is shorter, instead of the full 30-second probe deadline. A connector that does not answer in time now reports the typed problem `connector_timeout` and renders as **Not responding** with a fix prompt and Refresh, still showing its credential card. The page no longer retries a timed-out connector on its own or on focus; Refresh reads it again, and a consent return from OAuth keeps polling through a timeout. Agent discovery keeps the full `discovery.probeTimeoutMs`.
|
|
87
|
+
|
|
88
|
+
### Fixed
|
|
89
|
+
|
|
90
|
+
- A remote MCP connector's credential test always sends a live `tools/list` to the downstream. Against a modern server whose catalog was cached, it previously could report success from the cache without any request.
|
|
91
|
+
- Eval fake vendors share one refcounted, credential-routed transport that fails closed, so `eval:agent --concurrency` trials of different provider families no longer drop each other's routing and reach real vendor hosts.
|
|
92
|
+
- The eval graders now accept two equivalent correct answers they used to reject. Drive revision timestamps (`google-drive-4`) may be keyed by revision id instead of listed in revision order. The Stripe rejected-parameter task accepts any customer page that reaches the five newest matches, as long as the answer reduces it to exactly those ids. `google-drive-4` is no longer treated as an expected capability gap.
|
|
93
|
+
- Hatch details tests expect the position-only `fields` refusal wording that never echoes the submitted path.
|
|
94
|
+
- Downstream OAuth tokens are refreshed before they expire, not only after a 401. Grants now store `expiresAt` with their tokens, counted from when the token request was sent (an `expires_in` of `0` expires at issuance), and a call whose token is within 60 seconds of it renews first through the existing refresh coordinator, so a `remoteMcp()` or `api()` write no longer meets an avoidable 401 and an unknown outcome. The renewal runs in the invocation's authorization check, so a failed one is a plain `downstream_oauth_required` with nothing sent. Two isolates that find the same token due spend one refresh token. Grants stored without `expiresAt` keep refreshing after a 401 until their next refresh stores one; reading a grant never adds it, and status, listing and discovery never refresh. Waiting on another isolate's refresh now polls with a 50 ms to 2 s backoff and reads its lease, grant and spent record in one batched read.
|
|
95
|
+
|
|
5
96
|
## 0.38.0 — 2026-10-28
|
|
6
97
|
|
|
7
98
|
0.38.0 is a minor release that retires the old storage and OAuth layout migrations, adds a configurable program return cap, makes in-program skill calls more forgiving, and moves the Eve helper to Eve 0.86.
|
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ should call.
|
|
|
38
38
|
|
|
39
39
|
| Boundary | What Connecta does |
|
|
40
40
|
| ---------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
|
41
|
-
| Access | A pool limits the tools its members can reach.
|
|
41
|
+
| Access | A pool limits the tools its members can reach; it lists only people or only agents. |
|
|
42
42
|
| Writes | For tools marked as writes, once a change is sent, Connecta won't automatically send it again. |
|
|
43
43
|
| Secrets | Known secrets are redacted from agent output. Enable the credential vault to encrypt stored credentials and OAuth tokens. |
|
|
44
44
|
| Read-only access | Read-only endpoints hide writes and refuse attempts to call them. |
|
|
@@ -69,7 +69,8 @@ connections and access rules still need configuring.
|
|
|
69
69
|
the server with the access rules you choose.
|
|
70
70
|
3. **Connect your AI app.** This app is the MCP client: it uses Connecta's
|
|
71
71
|
tools on your agent's behalf. Give it the pool address and authentication
|
|
72
|
-
method supplied by your operator.
|
|
72
|
+
method supplied by your operator. A person in a single pool can use the
|
|
73
|
+
plain `/mcp` address; their sign-in selects the pool. [Connecting agents](./guides/connecting-agents.md)
|
|
73
74
|
explains what to ask for; the [deployment guide](./documentation/deploying.md)
|
|
74
75
|
has runnable steps.
|
|
75
76
|
|
package/bin/connecta.mjs
CHANGED
|
@@ -258,18 +258,14 @@ async function doctor() {
|
|
|
258
258
|
`Native catalogs unavailable: ${surface.catalogErrors.join(", ")}. Inspect connector status and authentication.`,
|
|
259
259
|
);
|
|
260
260
|
const expected = native
|
|
261
|
-
? ["skills", executeCode ? "execute_code" : "read_result"].sort()
|
|
261
|
+
? ["authorize_connector", "skills", executeCode ? "execute_code" : "read_result"].sort()
|
|
262
262
|
: [...META_TOOL_NAMES].sort();
|
|
263
263
|
if (
|
|
264
264
|
native
|
|
265
265
|
? expected.some((name) => !actual.includes(name)) ||
|
|
266
|
-
[
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
"call_destructive_tool",
|
|
270
|
-
"authorize_connector",
|
|
271
|
-
executeCode ? "read_result" : "execute_code",
|
|
272
|
-
].some((name) => actual.includes(name))
|
|
266
|
+
["search_tools", "call_tool", "call_destructive_tool", executeCode ? "read_result" : "execute_code"].some(
|
|
267
|
+
(name) => actual.includes(name),
|
|
268
|
+
)
|
|
273
269
|
: JSON.stringify(actual) !== JSON.stringify(expected)
|
|
274
270
|
) {
|
|
275
271
|
throw new Error(
|
package/bin/version.mjs
CHANGED
package/dist/access-tokens.js
CHANGED
|
@@ -8,7 +8,7 @@ import { accessTokenKeys } from "./storage/keys.js";
|
|
|
8
8
|
import { AGENT_NAMESPACE, agentSubjectId, isAgentName, trustAgentProvider } from "./agent-identity.js";
|
|
9
9
|
import { keepErrorName } from "./redaction-identity.js";
|
|
10
10
|
import { agentConfig } from "./agent-config.js";
|
|
11
|
-
import { authCall, authCatch, authFlow, authInvoke, authMethod, authOwnMethod, authStorage, registerAuthMethod, } from "./auth/native.js";
|
|
11
|
+
import { authAccessToken, authCall, authCatch, authFlow, authInvoke, authMethod, authOwnMethod, authStorage, registerAuthMethod, } from "./auth/native.js";
|
|
12
12
|
import { runEdge } from "./runtime/run.js";
|
|
13
13
|
const TOKEN_PREFIX = "cta_";
|
|
14
14
|
const TOKEN_BYTES = 32;
|
|
@@ -802,7 +802,9 @@ export class AccessTokenManager {
|
|
|
802
802
|
signal.throwIfAborted();
|
|
803
803
|
const hash = (yield authCall(hashToken, [token]));
|
|
804
804
|
signal.throwIfAborted();
|
|
805
|
-
const
|
|
805
|
+
const snapshot = (yield authAccessToken(this.storage, hash));
|
|
806
|
+
const read = (key) => snapshot ? (snapshot.get(key) ?? null) : authStorage(this.storage, "get", [key]);
|
|
807
|
+
const lookupRaw = (yield authCall(read, [lookupKey(hash)]));
|
|
806
808
|
signal.throwIfAborted();
|
|
807
809
|
if (!lookupRaw)
|
|
808
810
|
return unauthorized();
|
|
@@ -810,7 +812,7 @@ export class AccessTokenManager {
|
|
|
810
812
|
if (!lookup)
|
|
811
813
|
return unauthorized();
|
|
812
814
|
const recordRaw = ID_RE.test(lookup.id)
|
|
813
|
-
? (yield
|
|
815
|
+
? (yield authCall(read, [recordKey(lookup.id)]))
|
|
814
816
|
: null;
|
|
815
817
|
const record = recordRaw ? parseRecord(recordRaw) : null;
|
|
816
818
|
if (record && record.id !== lookup.id)
|
|
@@ -827,7 +829,7 @@ export class AccessTokenManager {
|
|
|
827
829
|
// code, refuse exactly as an unknown token does.
|
|
828
830
|
if (record.principal || record.agent === undefined || !this.agents.has(record.agent))
|
|
829
831
|
return unauthorized();
|
|
830
|
-
const slotRaw = (yield
|
|
832
|
+
const slotRaw = (yield authCall(read, [slotKey(record.agent)]));
|
|
831
833
|
const slot = slotRaw === null ? null : parseSlot(slotRaw, record.agent);
|
|
832
834
|
signal.throwIfAborted();
|
|
833
835
|
if (slot?.state !== "issued" || slot.tokenId !== record.id)
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route wording: how Connecta-authored text names a route on the surface that
|
|
3
|
+
* serves it. Text is built with that surface's wording when Connecta writes
|
|
4
|
+
* it, never rendered afterwards: nothing scans or rewrites replies, program
|
|
5
|
+
* values, downstream results, guides or caller data.
|
|
6
|
+
*
|
|
7
|
+
* Two tables per surface:
|
|
8
|
+
* - `AgentRouteNames` (public, `ConnectorContext.routes`) names the tools. Use
|
|
9
|
+
* a name only in a sentence written for a surface that lists that tool.
|
|
10
|
+
* - `RoutePhrases` holds complete phrases for sentences every surface shares,
|
|
11
|
+
* such as "for a direct call_tool only" against "when calling this tool
|
|
12
|
+
* directly". A message chooses whole phrases; it never splices a name into a
|
|
13
|
+
* sentence written for another surface, and it branches when the surface
|
|
14
|
+
* runs no programs (`program` is undefined).
|
|
15
|
+
*
|
|
16
|
+
* Where the wording comes from:
|
|
17
|
+
* - Connector and provider code reads `routePhrases(ctx)`, or `Routes` from a
|
|
18
|
+
* call's Effect services. The registry stamps every context with the
|
|
19
|
+
* deployment's names, and a call carries the names of the request's surface.
|
|
20
|
+
* - Request handlers (meta-tools, programs, native pools, authorization
|
|
21
|
+
* handoffs) receive the request's names from the MCP route.
|
|
22
|
+
* - Instructions, tool descriptions and built-in guides are built per surface,
|
|
23
|
+
* or, when only one surface serves them, with the meta names.
|
|
24
|
+
* - Static provider schema descriptions declare `routeDescription`. The meta
|
|
25
|
+
* text is the declared default; each request's catalog serves `api()`
|
|
26
|
+
* catalogs with the description built for its own surface.
|
|
27
|
+
*/
|
|
28
|
+
import { Context } from "effect";
|
|
29
|
+
const NAMES = {
|
|
30
|
+
meta: Object.freeze({
|
|
31
|
+
search: "search_tools",
|
|
32
|
+
read: "call_tool",
|
|
33
|
+
write: "call_destructive_tool",
|
|
34
|
+
program: "execute_code",
|
|
35
|
+
authorize: "authorize_connector",
|
|
36
|
+
skills: "skills",
|
|
37
|
+
}),
|
|
38
|
+
code: Object.freeze({
|
|
39
|
+
search: "connecta.search inside execute_code",
|
|
40
|
+
read: "connecta.call inside execute_code",
|
|
41
|
+
write: "connecta.call inside execute_code",
|
|
42
|
+
program: "execute_code",
|
|
43
|
+
authorize: "authorize_connector",
|
|
44
|
+
skills: "skills",
|
|
45
|
+
}),
|
|
46
|
+
native: Object.freeze({
|
|
47
|
+
search: "your host's tool search",
|
|
48
|
+
read: "the original native tool",
|
|
49
|
+
write: "the original native tool",
|
|
50
|
+
program: "your host's tool search",
|
|
51
|
+
authorize: "the returned authorization handoff",
|
|
52
|
+
skills: "skills",
|
|
53
|
+
}),
|
|
54
|
+
"native-code": Object.freeze({
|
|
55
|
+
search: "your host's tool search",
|
|
56
|
+
read: "the original native tool",
|
|
57
|
+
write: "the original native tool",
|
|
58
|
+
program: "execute_code",
|
|
59
|
+
authorize: "the returned authorization handoff",
|
|
60
|
+
skills: "skills",
|
|
61
|
+
}),
|
|
62
|
+
};
|
|
63
|
+
/** The six-tool surface's names: the default wherever no other surface applies. */
|
|
64
|
+
export const META_ROUTES = NAMES.meta;
|
|
65
|
+
export function routeNames(surface) {
|
|
66
|
+
return NAMES[surface];
|
|
67
|
+
}
|
|
68
|
+
/** The names for the surface a connector operation serves; meta for contexts Connecta did not build. */
|
|
69
|
+
export function routesOf(ctx) {
|
|
70
|
+
return ctx?.routes ?? META_ROUTES;
|
|
71
|
+
}
|
|
72
|
+
// Meta and code text authorize with authorize_connector, which both list. A
|
|
73
|
+
// native pool lists no authorization tool: its errors carry the handoff in
|
|
74
|
+
// nextAction, so the agent passes that on.
|
|
75
|
+
const metaAuthorize = {
|
|
76
|
+
authorizeAndOpen: (connector) => `call authorize_connector({ connector: "${connector}" }) and open the returned URL`,
|
|
77
|
+
authorizeAndFollow: (connector) => `call authorize_connector({ connector: "${connector}" }) and follow the operator handoff it returns`,
|
|
78
|
+
authorizeFor: (purpose) => `Call authorize_connector for ${purpose}`,
|
|
79
|
+
authorizeCall: (connector) => connector === undefined ? "Call authorize_connector." : `Call authorize_connector({ connector: "${connector}" }).`,
|
|
80
|
+
authorizeAgain: "call authorize_connector again",
|
|
81
|
+
};
|
|
82
|
+
const nativeAuthorize = {
|
|
83
|
+
authorizeAndOpen: () => "give this error's nextAction to the user or operator",
|
|
84
|
+
authorizeAndFollow: () => "give this error's nextAction to the user or operator",
|
|
85
|
+
authorizeFor: () => "Give this error's nextAction to the user or operator",
|
|
86
|
+
authorizeCall: () => "Give this error's nextAction to the user or operator.",
|
|
87
|
+
authorizeAgain: "retry the original native call",
|
|
88
|
+
};
|
|
89
|
+
const nativeDirect = {
|
|
90
|
+
directOnly: "when calling this tool directly",
|
|
91
|
+
forDirect: "when calling this tool directly",
|
|
92
|
+
onlyDirectRead: "only when calling this tool directly",
|
|
93
|
+
writeNeedsApproval: "Invoke it through its own native tool so the MCP host can request approval.",
|
|
94
|
+
};
|
|
95
|
+
const PHRASES = {
|
|
96
|
+
meta: Object.freeze({
|
|
97
|
+
program: "execute_code",
|
|
98
|
+
directOnly: "for a direct call_tool only",
|
|
99
|
+
forDirect: "for a direct call_tool",
|
|
100
|
+
onlyDirectRead: "only for a direct call_tool read",
|
|
101
|
+
callDirectlyAndPage: "call directly (call_tool, connecta.result to page)",
|
|
102
|
+
writeNeedsApproval: "Invoke it through call_destructive_tool so the MCP host can request approval.",
|
|
103
|
+
...metaAuthorize,
|
|
104
|
+
}),
|
|
105
|
+
// The eval code arm keeps the text its comparator protocol recorded.
|
|
106
|
+
code: Object.freeze({
|
|
107
|
+
program: "execute_code",
|
|
108
|
+
directOnly: "for a direct connecta.call inside execute_code only",
|
|
109
|
+
forDirect: "for a direct connecta.call inside execute_code",
|
|
110
|
+
onlyDirectRead: "only for a direct connecta.call inside execute_code read",
|
|
111
|
+
callDirectlyAndPage: "call directly (connecta.call inside execute_code, connecta.result to page)",
|
|
112
|
+
writeNeedsApproval: "Invoke it through connecta.call inside execute_code so the MCP host can request approval.",
|
|
113
|
+
...metaAuthorize,
|
|
114
|
+
}),
|
|
115
|
+
native: Object.freeze({
|
|
116
|
+
program: undefined,
|
|
117
|
+
callDirectlyAndPage: "call this tool directly and page with read_result",
|
|
118
|
+
...nativeDirect,
|
|
119
|
+
...nativeAuthorize,
|
|
120
|
+
}),
|
|
121
|
+
"native-code": Object.freeze({
|
|
122
|
+
program: "execute_code",
|
|
123
|
+
callDirectlyAndPage: "call this tool directly and page with connecta.result in execute_code",
|
|
124
|
+
...nativeDirect,
|
|
125
|
+
...nativeAuthorize,
|
|
126
|
+
}),
|
|
127
|
+
};
|
|
128
|
+
const SURFACES = new Map(Object.keys(NAMES).map((surface) => [NAMES[surface], surface]));
|
|
129
|
+
/** The surface a names table belongs to; meta for a table Connecta did not build. */
|
|
130
|
+
function routeSurface(routes) {
|
|
131
|
+
return (routes && SURFACES.get(routes)) ?? "meta";
|
|
132
|
+
}
|
|
133
|
+
/** The phrases for the surface an operation serves, from its names or its context. */
|
|
134
|
+
export function routePhrases(source) {
|
|
135
|
+
const routes = source && "read" in source ? source : routesOf(source);
|
|
136
|
+
return PHRASES[routeSurface(routes)];
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* The same names inside a call's Effect services, for provider code that has
|
|
140
|
+
* no context in hand (an `authenticate` program, a vendor error table).
|
|
141
|
+
*/
|
|
142
|
+
export const Routes = Context.Reference("connecta/call/Routes", {
|
|
143
|
+
defaultValue: () => META_ROUTES,
|
|
144
|
+
});
|
|
145
|
+
const descriptions = new Map();
|
|
146
|
+
/**
|
|
147
|
+
* A static schema description that names a route. Returns the meta text, which
|
|
148
|
+
* the schema declares; a request on another surface lists the text built from
|
|
149
|
+
* its own phrases. Declare it once, at module scope.
|
|
150
|
+
*/
|
|
151
|
+
export function routeDescription(build) {
|
|
152
|
+
const meta = build(PHRASES.meta);
|
|
153
|
+
descriptions.set(meta, build);
|
|
154
|
+
return meta;
|
|
155
|
+
}
|
|
156
|
+
const declaringConnectors = new WeakSet();
|
|
157
|
+
/** Mark a connector whose catalog Connecta authors (`api()`), so its declared descriptions follow the surface. */
|
|
158
|
+
export function declaresRouteDescriptions(connector) {
|
|
159
|
+
declaringConnectors.add(connector);
|
|
160
|
+
}
|
|
161
|
+
// Where an authored tool's declared descriptions sit, per connector and tool
|
|
162
|
+
// name: api() definitions are fixed at construction, while the registry
|
|
163
|
+
// publishes a fresh copy of each tool on every read.
|
|
164
|
+
const descriptionPaths = new WeakMap();
|
|
165
|
+
const rendered = new Map();
|
|
166
|
+
function at(value, path) {
|
|
167
|
+
let current = value;
|
|
168
|
+
for (const key of path)
|
|
169
|
+
current = current?.[key];
|
|
170
|
+
return current;
|
|
171
|
+
}
|
|
172
|
+
function declaredPaths(tool) {
|
|
173
|
+
const found = [];
|
|
174
|
+
const visit = (value, path) => {
|
|
175
|
+
if (Array.isArray(value))
|
|
176
|
+
value.forEach((item, index) => visit(item, [...path, index]));
|
|
177
|
+
else if (value !== null && typeof value === "object")
|
|
178
|
+
for (const [key, item] of Object.entries(value)) {
|
|
179
|
+
if (key === "description" && typeof item === "string" && descriptions.has(item))
|
|
180
|
+
found.push([...path, key]);
|
|
181
|
+
else
|
|
182
|
+
visit(item, [...path, key]);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
for (const key of ["inputSchema", "outputSchema"])
|
|
186
|
+
visit(tool[key], [key]);
|
|
187
|
+
return found;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Give a request's own copy of one tool the declared descriptions of the
|
|
191
|
+
* surface it serves, in place, so the copy keeps everything else it carries,
|
|
192
|
+
* its source classification included. Only tools of connectors Connecta
|
|
193
|
+
* authors change; downstream catalogs and the meta surface are left as given.
|
|
194
|
+
*/
|
|
195
|
+
export function routeToolInPlace(connector, tool, routes) {
|
|
196
|
+
const surface = routeSurface(routes);
|
|
197
|
+
if (surface === "meta" || descriptions.size === 0 || !connector || !declaringConnectors.has(connector))
|
|
198
|
+
return;
|
|
199
|
+
let byName = descriptionPaths.get(connector);
|
|
200
|
+
if (!byName)
|
|
201
|
+
descriptionPaths.set(connector, (byName = new Map()));
|
|
202
|
+
let paths = byName.get(tool.name);
|
|
203
|
+
// Recompute when a path no longer holds declared meta text.
|
|
204
|
+
if (!paths || !paths.every((path) => descriptions.has(at(tool, path)))) {
|
|
205
|
+
paths = declaredPaths(tool);
|
|
206
|
+
byName.set(tool.name, paths);
|
|
207
|
+
}
|
|
208
|
+
for (const path of paths) {
|
|
209
|
+
const meta = at(tool, path);
|
|
210
|
+
let bySurface = rendered.get(meta);
|
|
211
|
+
if (!bySurface)
|
|
212
|
+
rendered.set(meta, (bySurface = new Map()));
|
|
213
|
+
let text = bySurface.get(surface);
|
|
214
|
+
if (text === undefined)
|
|
215
|
+
bySurface.set(surface, (text = descriptions.get(meta)(PHRASES[surface])));
|
|
216
|
+
at(tool, path.slice(0, -1))["description"] = text;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const programRouteConfigs = new WeakMap();
|
|
220
|
+
/**
|
|
221
|
+
* Eval only: serve this configuration's text with program-form route names.
|
|
222
|
+
* No package entry exports it and no configuration key selects it; the eval
|
|
223
|
+
* code arm marks the configuration object it passes to createConnecta.
|
|
224
|
+
*/
|
|
225
|
+
export function renderRoutesAsPrograms(config) {
|
|
226
|
+
programRouteConfigs.set(config, true);
|
|
227
|
+
}
|
|
228
|
+
/** The names every reply of a deployment built from `config` uses outside native pools. */
|
|
229
|
+
export function deploymentRoutes(config) {
|
|
230
|
+
return programRouteConfigs.has(config) ? NAMES.code : NAMES.meta;
|
|
231
|
+
}
|