@zackbart/connecta 0.28.1 → 0.30.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 +855 -5
- package/README.md +125 -116
- package/bin/connecta.mjs +121 -139
- package/bin/meta-tool-names.d.mts +2 -0
- package/bin/meta-tool-names.mjs +9 -0
- package/bin/version.mjs +3 -0
- package/dist/access-tokens.js +53 -63
- package/dist/activity-request.js +8 -0
- package/dist/activity.d.ts +76 -42
- package/dist/activity.js +110 -42
- package/dist/argument-redaction.js +230 -0
- package/dist/auth/clerk-sdk/client.d.ts +65 -0
- package/dist/auth/clerk-sdk/client.js +5 -0
- package/dist/auth/clerk-sdk/generated.js +1845 -0
- package/dist/auth/clerk-transport.js +19 -0
- package/dist/auth/clerk.d.ts +17 -7
- package/dist/auth/clerk.js +189 -107
- package/dist/auth/cloudflare-access.js +25 -26
- package/dist/auth/downstream-client-metadata.d.ts +29 -0
- package/dist/auth/downstream-client-metadata.js +133 -0
- package/dist/auth/downstream-oauth.js +1584 -1580
- package/dist/auth/downstream-revocation.js +106 -0
- package/dist/auth/oauth-token-response.js +231 -0
- package/dist/auth/oauth-v2-migration.js +116 -0
- package/dist/auth/static-oauth.js +93 -71
- package/dist/auth-elicitation.js +288 -0
- package/dist/authorization-handoff.js +111 -0
- package/dist/branding.d.ts +8 -16
- package/dist/branding.js +26 -59
- package/dist/byte-read-response.js +39 -0
- package/dist/call-admission.js +12 -16
- package/dist/catalog-cache.js +739 -0
- package/dist/catalog-drift.d.ts +50 -15
- package/dist/catalog-drift.js +365 -128
- package/dist/catalog-fingerprint.js +2 -4
- package/dist/catalog-freshness.js +13 -0
- package/dist/catalog-limits.js +2 -2
- package/dist/catalog-service.js +409 -225
- package/dist/catalog.js +43 -84
- package/dist/concurrency.js +3 -4
- package/dist/config-defaults.d.ts +39 -0
- package/dist/config-defaults.js +46 -0
- package/dist/config-schema.d.ts +181 -0
- package/dist/config-schema.js +435 -0
- package/dist/config-value-sources.js +102 -0
- package/dist/config.d.ts +633 -0
- package/dist/config.js +508 -0
- package/dist/connector-access.d.ts +1 -0
- package/dist/connector-access.js +13 -15
- package/dist/connector-caller.d.ts +8 -0
- package/dist/connector-caller.js +15 -6
- package/dist/connector-scope.js +1 -1
- package/dist/connectors/api-connector.d.ts +59 -7
- package/dist/connectors/api-connector.js +106 -19
- package/dist/connectors/api.js +4 -1
- package/dist/connectors/guarded-fetch.d.ts +144 -0
- package/dist/connectors/guarded-fetch.js +87 -38
- package/dist/connectors/negotiation-cache.js +46 -0
- package/dist/connectors/option-shapes.js +27 -0
- package/dist/connectors/remote-mcp.d.ts +30 -4
- package/dist/connectors/remote-mcp.js +1774 -320
- package/dist/connectors/resource-uri.js +335 -0
- package/dist/credential-contract.d.ts +10 -0
- package/dist/credential-rules.js +1 -3
- package/dist/credential-url.js +108 -0
- package/dist/credentials.d.ts +6 -1
- package/dist/credentials.js +27 -18
- package/dist/d1.d.ts +130 -0
- package/dist/d1.js +290 -0
- package/dist/describe-config.d.ts +165 -0
- package/dist/describe-config.js +315 -0
- package/dist/described.js +106 -0
- package/dist/downstream-input-context.js +128 -0
- package/dist/downstream-input.js +325 -0
- package/dist/errors.d.ts +132 -31
- package/dist/errors.js +175 -87
- package/dist/execute.js +798 -318
- package/dist/executor-admission.d.ts +1 -0
- package/dist/executor-admission.js +11 -15
- package/dist/executor-contract.d.ts +20 -0
- package/dist/executor-contract.js +48 -0
- package/dist/executor-result.js +30 -13
- package/dist/executors/quickjs-child.js +15 -9
- package/dist/executors/quickjs-protocol.js +5 -6
- package/dist/executors/quickjs-runtime.js +152 -59
- package/dist/executors/quickjs.js +49 -39
- package/dist/favicon.js +1 -2
- package/dist/fix-prompt.js +1 -3
- package/dist/guest-runtime.js +238 -0
- package/dist/guest-types.d.ts +218 -0
- package/dist/guest-types.js +1 -0
- package/dist/html-security.js +24 -0
- package/dist/identity.js +1 -3
- package/dist/inbound-credential.js +52 -0
- package/dist/index.d.ts +24 -256
- package/dist/index.js +65 -478
- package/dist/invocation-auth.js +69 -0
- package/dist/invocation.js +262 -79
- package/dist/json-schema.d.ts +1 -1
- package/dist/mcp-client-context.js +35 -0
- package/dist/mcp-result.js +11 -7
- package/dist/meta-output.js +61 -0
- package/dist/meta-tool-names.d.ts +2 -0
- package/dist/meta-tool-names.js +3 -0
- package/dist/meta-tools.js +419 -272
- package/dist/module-contracts.d.ts +8 -23
- package/dist/node.d.ts +0 -2
- package/dist/node.js +2 -2
- package/dist/oauth-callback-outcome.js +1 -3
- package/dist/oauth-handoff.js +194 -0
- package/dist/oauth-sealing.js +20 -0
- package/dist/operator-record.d.ts +137 -0
- package/dist/operator-record.js +420 -0
- package/dist/operator-ui/assets.js +31 -0
- package/dist/operator-ui/contract.d.ts +72 -0
- package/dist/operator-ui/contract.js +1 -0
- package/dist/operator-ui/fix-prompts.d.ts +18 -0
- package/dist/operator-ui/generated.js +5 -4
- package/dist/operator-ui/model.d.ts +12 -87
- package/dist/operator-ui/model.js +2 -9
- package/dist/operator-ui/setup-commands.js +1 -3
- package/dist/operator-ui/view.d.ts +323 -0
- package/dist/operator-ui/view.js +62 -189
- package/dist/page-styles.js +2 -3
- package/dist/program-result.js +5 -0
- package/dist/program-source.js +14 -2
- package/dist/{exempt-writes.js → program-writes.js} +60 -19
- package/dist/provider.d.ts +142 -0
- package/dist/provider.js +187 -0
- package/dist/providers/_shared/google/result-size.js +57 -0
- package/dist/providers/_shared/google/service-account.d.ts +58 -0
- package/dist/providers/_shared/google/service-account.js +500 -0
- package/dist/providers/_shared/google/workspace.d.ts +276 -0
- package/dist/providers/_shared/google/workspace.js +518 -0
- package/dist/providers/_shared/request-token.js +83 -0
- package/dist/providers/{basecamp.d.ts → basecamp/index.d.ts} +6 -28
- package/dist/providers/basecamp/index.js +997 -0
- package/dist/providers/basecamp/skill.generated.js +9 -0
- package/dist/providers/{breeze.d.ts → breeze/index.d.ts} +2 -2
- package/dist/providers/{breeze.js → breeze/index.js} +184 -140
- package/dist/providers/breeze/skill.generated.js +9 -0
- package/dist/providers/{ccb.d.ts → ccb/index.d.ts} +2 -2
- package/dist/providers/{ccb.js → ccb/index.js} +271 -116
- package/dist/providers/ccb/skill.generated.js +9 -0
- package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
- package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
- package/dist/providers/cloudflare/skill.generated.js +10 -0
- package/dist/providers/docs/index.d.ts +54 -0
- package/dist/providers/docs/index.js +1237 -0
- package/dist/providers/docs/skill.generated.js +9 -0
- package/dist/providers/drive/index.d.ts +53 -0
- package/dist/providers/drive/index.js +1523 -0
- package/dist/providers/drive/skill.generated.js +9 -0
- package/dist/providers/forms/index.d.ts +54 -0
- package/dist/providers/forms/index.js +1217 -0
- package/dist/providers/forms/skill.generated.js +11 -0
- package/dist/providers/github/auth.d.ts +32 -0
- package/dist/providers/github/auth.js +361 -0
- package/dist/providers/github/hosted.js +218 -0
- package/dist/providers/github/index.d.ts +13 -0
- package/dist/providers/github/index.js +140 -0
- package/dist/providers/github/key.js +100 -0
- package/dist/providers/github/rest.js +321 -0
- package/dist/providers/github/scope.d.ts +29 -0
- package/dist/providers/github/scope.js +124 -0
- package/dist/providers/github/skill.generated.js +8 -0
- package/dist/providers/gmail/index.d.ts +51 -0
- package/dist/providers/gmail/index.js +1436 -0
- package/dist/providers/gmail/skill.generated.js +9 -0
- package/dist/providers/infisical/index.d.ts +7 -0
- package/dist/providers/infisical/index.js +713 -0
- package/dist/providers/infisical/skill.generated.js +8 -0
- package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
- package/dist/providers/linear/index.js +352 -0
- package/dist/providers/linear/skill.generated.js +8 -0
- package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
- package/dist/providers/mixpanel/index.js +416 -0
- package/dist/providers/mixpanel/skill.generated.js +9 -0
- package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
- package/dist/providers/{notion.js → notion/index.js} +226 -239
- package/dist/providers/notion/skill.generated.js +10 -0
- package/dist/providers/{overflow.d.ts → overflow/index.d.ts} +2 -2
- package/dist/providers/{overflow.js → overflow/index.js} +74 -111
- package/dist/providers/overflow/skill.generated.js +8 -0
- package/dist/providers/{planning-center.d.ts → planning-center/index.d.ts} +2 -2
- package/dist/providers/{planning-center.js → planning-center/index.js} +264 -104
- package/dist/providers/planning-center/skill.generated.js +10 -0
- package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
- package/dist/providers/revenuecat/index.js +557 -0
- package/dist/providers/revenuecat/skill.generated.js +11 -0
- package/dist/providers/sheets/index.d.ts +54 -0
- package/dist/providers/sheets/index.js +1237 -0
- package/dist/providers/sheets/skill.generated.js +10 -0
- package/dist/providers/slides/index.d.ts +53 -0
- package/dist/providers/slides/index.js +2645 -0
- package/dist/providers/slides/skill.generated.js +9 -0
- package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
- package/dist/providers/stripe/index.js +266 -0
- package/dist/providers/stripe/skill.generated.js +11 -0
- package/dist/providers/{tithely.d.ts → tithely/index.d.ts} +2 -2
- package/dist/providers/{tithely.js → tithely/index.js} +60 -91
- package/dist/providers/tithely/skill.generated.js +8 -0
- package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
- package/dist/providers/vercel/index.js +1038 -0
- package/dist/providers/vercel/mcp-ownership.js +212 -0
- package/dist/providers/vercel/skill.generated.js +11 -0
- package/dist/registry.d.ts +110 -171
- package/dist/registry.js +588 -927
- package/dist/request-state.js +21 -0
- package/dist/resource-read.js +103 -0
- package/dist/result-shapes.js +17 -49
- package/dist/retired-toolkits.js +1 -1
- package/dist/routes/access-tokens.js +3 -6
- package/dist/routes/activity-context.js +21 -0
- package/dist/routes/activity-disclosure.js +40 -0
- package/dist/routes/activity.js +59 -21
- package/dist/routes/actor-labels.js +3 -50
- package/dist/routes/connect.js +221 -0
- package/dist/routes/credentials.js +15 -27
- package/dist/routes/mcp.js +365 -130
- package/dist/routes/oauth-client-metadata.js +15 -0
- package/dist/routes/oauth-management.js +45 -107
- package/dist/routes/oauth.js +110 -57
- package/dist/routes/operator.js +3 -3
- package/dist/routes/shared.d.ts +25 -64
- package/dist/routes/shared.js +160 -80
- package/dist/routes/skills.js +30 -0
- package/dist/routes/ui-config.js +225 -0
- package/dist/routes/ui-data.js +40 -35
- package/dist/routes/ui.js +66 -56
- package/dist/runtime/admission.js +4 -2
- package/dist/runtime/call-admission.js +3 -1
- package/dist/runtime/connector-scope.js +1 -4
- package/dist/runtime/run.js +10 -39
- package/dist/runtime/services.js +12 -117
- package/dist/runtime/shared-read.js +1 -3
- package/dist/runtime/storage.js +8 -1
- package/dist/sent-secrets.d.ts +93 -0
- package/dist/sent-secrets.js +1013 -0
- package/dist/server.js +28 -71
- package/dist/skills.js +416 -174
- package/dist/sqlite.d.ts +48 -0
- package/dist/sqlite.js +181 -0
- package/dist/storage/keys.js +443 -0
- package/dist/storage/memory.js +16 -4
- package/dist/storage/sql.d.ts +66 -0
- package/dist/storage/sql.js +543 -0
- package/dist/timeout.js +4 -2
- package/dist/tool-name.js +17 -0
- package/dist/tool-safety.d.ts +9 -35
- package/dist/tool-safety.js +20 -40
- package/dist/types.d.ts +336 -56
- package/dist/types.js +0 -1
- package/dist/ui.d.ts +7 -19
- package/dist/ui.js +76 -90
- package/dist/url-safety.js +3 -6
- package/dist/usage-guide.js +146 -0
- package/dist/validate.d.ts +7 -0
- package/dist/validate.js +309 -47
- package/dist/version.d.ts +1 -7
- package/dist/version.js +3 -7
- package/dist/worker.d.ts +11 -0
- package/dist/worker.js +435 -0
- package/documentation/README.md +63 -0
- package/documentation/architecture.md +696 -231
- package/documentation/auth.md +1069 -546
- package/documentation/code-mode.md +662 -356
- package/documentation/deploying.md +146 -0
- package/documentation/integrating.md +159 -0
- package/documentation/meta-tools.md +570 -216
- package/documentation/operating.md +135 -0
- package/documentation/operator-tests.md +97 -0
- package/documentation/operator-ui.md +132 -0
- package/documentation/provider-migration-0.29.md +65 -0
- package/examples/worker/AGENTS.md +34 -12
- package/examples/worker/README.md +293 -255
- package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
- package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
- package/examples/worker/src/connecta.config.ts +151 -0
- package/examples/worker/src/index.ts +14 -197
- package/examples/worker/wrangler.jsonc +15 -35
- package/package.json +126 -54
- package/templates/node/.dockerignore +1 -2
- package/templates/node/.env.example +20 -21
- package/templates/node/AGENTS.md +21 -17
- package/templates/node/Dockerfile +3 -5
- package/templates/node/README.md +140 -58
- package/templates/node/docker-compose.yml +11 -12
- package/templates/node/package.json +4 -2
- package/templates/node/src/connecta.config.ts +119 -0
- package/templates/node/src/index.ts +9 -154
- package/templates/node/src/provision-token.ts +17 -0
- package/AGENTS.md +0 -170
- package/dist/artifacts/connector.d.ts +0 -58
- package/dist/artifacts/connector.js +0 -730
- package/dist/artifacts/document.js +0 -86
- package/dist/artifacts/guide.js +0 -140
- package/dist/artifacts/html-scan.js +0 -329
- package/dist/artifacts/json.js +0 -78
- package/dist/artifacts/kv-store.d.ts +0 -20
- package/dist/artifacts/kv-store.js +0 -140
- package/dist/artifacts/markdown.js +0 -478
- package/dist/artifacts/module.d.ts +0 -37
- package/dist/artifacts/module.js +0 -66
- package/dist/artifacts/operations.d.ts +0 -197
- package/dist/artifacts/operations.js +0 -1002
- package/dist/artifacts/refresh.d.ts +0 -55
- package/dist/artifacts/refresh.js +0 -236
- package/dist/artifacts/routes.js +0 -206
- package/dist/artifacts/types.d.ts +0 -214
- package/dist/artifacts/types.js +0 -22
- package/dist/artifacts/validate.d.ts +0 -62
- package/dist/artifacts/validate.js +0 -651
- package/dist/artifacts.d.ts +0 -6
- package/dist/artifacts.js +0 -9
- package/dist/auth/bearer.d.ts +0 -11
- package/dist/auth/bearer.js +0 -50
- package/dist/providers/basecamp.js +0 -445
- package/dist/providers/linear.js +0 -217
- package/dist/providers/mixpanel.js +0 -247
- package/dist/providers/revenuecat.js +0 -317
- package/dist/providers/stripe.js +0 -253
- package/dist/providers/vercel.js +0 -1253
- package/dist/storage/file.d.ts +0 -15
- package/dist/storage/file.js +0 -455
- package/ethos.md +0 -93
- package/examples/worker/src/cloudflare-kv.ts +0 -43
- package/examples/worker/src/d1-activity-row.ts +0 -155
- package/examples/worker/src/d1-activity.ts +0 -147
- package/examples/worker/src/d1-storage.ts +0 -138
- package/examples/worker/src/r2-artifact-blobs.ts +0 -29
- package/templates/node/src/file-activity.ts +0 -182
|
@@ -4,72 +4,107 @@ One Web-standard `fetch(request) => Promise<Response>` handler, a long-lived
|
|
|
4
4
|
registry behind it, and a strict rule about what may be imported. Everything
|
|
5
5
|
else in this repository is a detail of those three things.
|
|
6
6
|
|
|
7
|
-
Read [
|
|
8
|
-
|
|
7
|
+
Read [PRINCIPLES.md](https://github.com/zackbart/connecta/blob/main/PRINCIPLES.md)
|
|
8
|
+
first; this guide explains the shape and where each subsystem lives. The surface itself belongs to
|
|
9
9
|
[meta-tools](./meta-tools.md), [code mode](./code-mode.md), and
|
|
10
10
|
[inbound auth](./auth.md).
|
|
11
11
|
|
|
12
|
+
## Task routes
|
|
13
|
+
|
|
14
|
+
[Agent index](./README.md) routes endpoint use, deployment, integration, and
|
|
15
|
+
repository maintenance. Read this guide when changing the core or checking an
|
|
16
|
+
implementation boundary. Use these sections for the affected subsystem.
|
|
17
|
+
|
|
18
|
+
| Change or question | Sections |
|
|
19
|
+
| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
20
|
+
| Request handling, transport, cleanup | [Two lifetimes](#the-two-lifetimes), [request lifecycle](#request-lifecycle), [admission](#admission-in-two-places) |
|
|
21
|
+
| Discovery, invocation, classification | [Layers below meta-tools](#layers-below-the-meta-tools), [providers and classification](#providers-and-reviewed-classification) |
|
|
22
|
+
| Storage, credentials, catalogs, paging | [Storage and connectors](#storage-credentials-and-connectors), [errors and records](#errors-and-records) |
|
|
23
|
+
| Config validation and optional features | [Configuration](#configuration), [modules](#optional-deployment-modules), [operator contract](./operator-ui.md#operator-data-contract) |
|
|
24
|
+
| Imports, executors, or Effect | [Purity](#import-graph-purity), [Effect](#effect-inside), [sharp edges](#sharp-edges) |
|
|
25
|
+
| Find code and deployment evidence | [Source map](#where-else-to-look), [sharp edges](#sharp-edges), [repository policy](https://github.com/zackbart/connecta/blob/main/AGENTS.md) |
|
|
26
|
+
|
|
27
|
+
Source paths and test names in each section identify its implementation and
|
|
28
|
+
regression evidence. Recorded production probes and bundle measurements are
|
|
29
|
+
historical observations tied to their stated date or version, not current
|
|
30
|
+
benchmarks or proof that planned replacements have shipped.
|
|
31
|
+
|
|
12
32
|
## The two lifetimes
|
|
13
33
|
|
|
14
34
|
Almost every bug in this codebase is a lifetime mistake, so the split is worth
|
|
15
35
|
stating before anything else.
|
|
16
36
|
|
|
17
37
|
**Per isolate, built once.** `createConnecta(config)` returns
|
|
18
|
-
`{ fetch, registry, close }` (`src/index.ts`). The `Registry` owns the connector
|
|
19
|
-
set, address resolution, catalog
|
|
38
|
+
`{ fetch, registry, describeConfig, close }` (`src/index.ts`). The `Registry` owns the connector
|
|
39
|
+
set, address resolution, request-local catalog reads, observed output schemas, connector
|
|
20
40
|
health, and the per-connector call limiters. It is built once and lives as long
|
|
21
41
|
as the isolate — on Workers a lazy module-scope singleton, which is why both
|
|
22
42
|
deployment shapes construct it outside the request handler.
|
|
23
43
|
|
|
24
|
-
An OAuth connector
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
44
|
+
An OAuth connector, `remoteMcp()` or `api()` with a static grant, owns a
|
|
45
|
+
runtime-local refresh completion gate in `src/auth/downstream-oauth.ts`.
|
|
46
|
+
Joined scopes share the outcome while keeping their own request I/O and
|
|
47
|
+
cancellation. A shared-storage CAS lease prevents independent isolates from
|
|
48
|
+
redeeming the same refresh token concurrently. The holder commits an accepted
|
|
49
|
+
rotation to the grant by CAS before releasing waiters, even when the owner
|
|
50
|
+
cancels after dispatch. The HTTP exchange uses its own 20-second deadline,
|
|
51
|
+
detached from caller cancellation, and the runtime passes its completion and
|
|
52
|
+
commit to Workers `waitUntil`. Contenders read the committed tokens or typed
|
|
53
|
+
verdict. An unsent claim can be taken over after expiry. Dispatch uses a lease CAS
|
|
54
|
+
transition and a CAS of the token fingerprint into permanent spent storage
|
|
55
|
+
before sending. Fingerprint records have no TTL and carry outstanding,
|
|
56
|
+
ambiguous, or resolved state across Restart and Disconnect. Outstanding and
|
|
57
|
+
ambiguous fingerprints block every epoch. A resolved fingerprint can be sent
|
|
58
|
+
once in a later epoch only when its code exchange observed the resolved
|
|
59
|
+
storage write before completing. Holder liveness survives reset until
|
|
60
|
+
completion or expiry. Credential-bearing
|
|
61
|
+
token requests use manual fetch and bypass resource redirect handling. Every
|
|
62
|
+
3xx requires re-consent after a refresh and refuses a code exchange. The HTTP
|
|
63
|
+
deadline is 20 seconds. A sent request
|
|
64
|
+
whose 120-second storage-owned liveness record expired without a commit is
|
|
65
|
+
never retried and requires re-consent. Valid rotations retry their grant commit
|
|
66
|
+
up to 32 times without another HTTP request; exhausted commits and all dispatched
|
|
67
|
+
provider failures require re-consent. [Auth](./auth.md#refresh-failures) describes the lease and failure
|
|
68
|
+
contracts.
|
|
37
69
|
|
|
38
70
|
**Per request, and no longer.** The MCP server, its transport, downstream MCP
|
|
39
71
|
clients, abort signals, and the connector scope a probe opens all belong to the
|
|
40
|
-
request that created them.
|
|
41
|
-
|
|
42
|
-
is a cross-request capability leak, and a promise awaited after the response is
|
|
72
|
+
request that created them. INV-7 requires this lifetime: a client retained
|
|
73
|
+
across requests on Workers is a cross-request capability leak, and a promise awaited after the response is
|
|
43
74
|
work the runtime may already have torn down. Deferred work has one sanctioned
|
|
44
75
|
channel, `ctx.waitUntil`, threaded through `fetch(request, env, ctx)` — activity
|
|
45
|
-
writes
|
|
46
|
-
|
|
76
|
+
writes and dispatched OAuth grant commits use it. Catalog listing and SQL cache
|
|
77
|
+
publication finish in the requesting scope. A fresh
|
|
47
78
|
`McpServer` per request is what makes the deployment stateless: no sessions, no
|
|
48
79
|
server push, no stream resumability, scope resolved rather than remembered.
|
|
49
80
|
|
|
50
81
|
## Request lifecycle
|
|
51
82
|
|
|
52
83
|
`src/server.ts` is the composition root: MCP origin check, scheme upgrade, route
|
|
53
|
-
table, security headers. Route
|
|
84
|
+
table, security headers. Route _order_ is the contract — several routes would
|
|
54
85
|
behave differently if they were reachable in another order — so read the table
|
|
55
86
|
top to bottom.
|
|
56
87
|
|
|
57
|
-
| Order | Route
|
|
58
|
-
|
|
|
59
|
-
| 0
|
|
60
|
-
| 0
|
|
61
|
-
| 0
|
|
62
|
-
| 1
|
|
63
|
-
|
|
|
64
|
-
|
|
|
65
|
-
|
|
|
66
|
-
|
|
|
67
|
-
|
|
|
68
|
-
|
|
|
69
|
-
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
88
|
+
| Order | Route | Notes |
|
|
89
|
+
| ----- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
90
|
+
| 0 | MCP Origin check | A disallowed `Origin` on `/mcp*` is a fixed 403 before redirects, admission, auth, or preflight — costing no permit and no auth lookup. Originless requests are admitted. |
|
|
91
|
+
| 0 | HTTPS upgrade | 308 to an HTTPS `publicUrl`, with path and query _assigned_ onto it rather than resolved against it, so a `//host` pathname cannot replace the origin. `/health` is exempt: a loopback probe must not need public DNS. |
|
|
92
|
+
| 0 | Cloudflare Access (Worker, when enabled) | Edge admission ahead of this table; an admitted invocation carries trusted identity in `ctx.access`. |
|
|
93
|
+
| 1 | `/oauth/client-metadata/<connectorId>` | Public GET-only CIMD for configured self-hosted OAuth connectors; fixed `publicUrl`, no auth or storage. |
|
|
94
|
+
| 1 | Mounted UI routes | Before wildcard OPTIONS, so mutation routes refuse preflight rather than inheriting MCP CORS. No UI module, no routes. |
|
|
95
|
+
| 1 | `/connect/<connectorId>`, `/oauth/callback/<connectorId>` | GET-only browser OAuth routes, before wildcard OPTIONS. Both verify the initiating user and management permission, independent of the UI. |
|
|
96
|
+
| 2 | MCP preflight | Allowed `OPTIONS` on `/mcp*`: 204 without admission or auth. |
|
|
97
|
+
| 2 | Other `OPTIONS` | Auth metadata first, otherwise compatibility CORS preflight. |
|
|
98
|
+
| 3 | `/.well-known/*` | Auth metadata, or 404. |
|
|
99
|
+
| 4 | `/health` | Open and payload-free: health, executor, admission, and deployment metadata. |
|
|
100
|
+
| 6 | `/mcp`, `/mcp/<pool>` | Admission, then auth, then a request-local MCP server. Body-confirmed modern listens skip admission and are refused after auth and SDK validation. An undeclared pool, a refusing grant, and a throwing grant are one identical 404; see [pools](./auth.md#pools). |
|
|
101
|
+
| 7 | Other paths | 404. Custom HTTP routes belong to the deployment. |
|
|
102
|
+
|
|
103
|
+
Every response leaves through `withSecurityHeaders`. HTML uses one common
|
|
104
|
+
security-header helper, including status and error pages. Operator shells
|
|
105
|
+
allow same-origin scripts and deny framing. Clerk pages also admit the validated
|
|
106
|
+
loader origin, the exact Cloudflare CAPTCHA script and frame host, Clerk images,
|
|
107
|
+
and first-party or blob workers. Local Clerk sign-in adds a nonce for its bootstrap.
|
|
73
108
|
`test/server-route-contracts.test.ts` pins the ordering and the exact refusal
|
|
74
109
|
bodies; it exists because the ordering is invisible in any one file and a
|
|
75
110
|
reordering reads like a harmless refactor.
|
|
@@ -77,41 +112,112 @@ reordering reads like a harmless refactor.
|
|
|
77
112
|
An admitted non-preflight `/mcp` request then takes five steps in
|
|
78
113
|
`src/routes/mcp.ts`:
|
|
79
114
|
|
|
80
|
-
1. **Admit.**
|
|
115
|
+
1. **Admit.** Modern `subscriptions/listen` requests take no permit: their
|
|
116
|
+
mirrored method and parsed body must both name the unsupported method.
|
|
117
|
+
They still pass auth, pool grants, and SDK validation, then receive HTTP
|
|
118
|
+
404 with JSON-RPC `-32601`. Their body classification and auth share the
|
|
119
|
+
configured request lifetime, starting before classification. Other requests
|
|
120
|
+
take one permit from the deployment-wide pool, taken before auth so an
|
|
81
121
|
unauthenticated flood costs a permit rather than a Clerk lookup, and held
|
|
82
|
-
until the response
|
|
122
|
+
until the response _body_ completes, the caller leaves, or its configured
|
|
83
123
|
lifetime ends, not until the handler returns.
|
|
84
|
-
2. **Authorize.**
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
124
|
+
2. **Authorize.** Machine tokens precede interactive providers. A provider's
|
|
125
|
+
synchronous credential recognition selects the sole verifier for an explicit
|
|
126
|
+
credential; a refusal never falls back to another identity. Human routes
|
|
127
|
+
reject machine credentials without verification. With no recognized
|
|
128
|
+
credential, configured providers are tried in order. A 401 takes its
|
|
129
|
+
challenge from the actual protected-resource metadata owner. No providers
|
|
130
|
+
means open development and warns at construction.
|
|
88
131
|
3. **Narrow to the pool.** On `/mcp/<pool>`, look the name up, run its grant
|
|
89
132
|
against the identity, then `intersectAccess` the pool with the identity's own
|
|
90
133
|
access. A pool can never widen a view; anything else is a 404 naming no pool.
|
|
91
134
|
4. **Derive the registry view.** One `registry.scoped(...)` call with those
|
|
92
135
|
connector ids, exact `connector.tool` addresses when the identity declares a
|
|
93
136
|
narrower slice, and the subject and principal keys. Personal connectors use
|
|
94
|
-
the principal partition
|
|
95
|
-
|
|
137
|
+
the principal partition and result paging the subject partition with principal,
|
|
138
|
+
endpoint/pool, origin, connector, and tool bindings. Paging rechecks current
|
|
139
|
+
grants, pool membership, and trust; no caller parameter selects a partition (`test/identity-scope.test.ts`).
|
|
96
140
|
5. **Serve.** Refuse `?toolkit=` with a 404 — the toolkits are gone
|
|
97
141
|
([#178](https://github.com/zackbart/connecta/issues/178)) but their URLs were
|
|
98
142
|
handed out, and retiring a scoping boundary into fail-open is worse than any
|
|
99
|
-
404 — then register the
|
|
143
|
+
404 — then register the six meta-tools on a fresh `McpServer`
|
|
100
144
|
(`test/server.test.ts`, `test/code-first-surface.test.ts`).
|
|
101
145
|
|
|
146
|
+
### Modern request metadata and transport refusals
|
|
147
|
+
|
|
148
|
+
The SDK lifts each modern request's client declarations from `_meta` into
|
|
149
|
+
`ctx.mcpReq.envelope`. Meta-tool handlers bind `clientCapabilities` and
|
|
150
|
+
`clientInfo` to their request-local context before dispatch. These declarations
|
|
151
|
+
never select an identity, grant, connector view, or pool. Direct and program
|
|
152
|
+
activity records retain only the self-declared `clientName` and `clientVersion`,
|
|
153
|
+
validated by one ASCII grammar at the record builder, SQL write/read boundaries,
|
|
154
|
+
and authenticated activity read route. Names match
|
|
155
|
+
`[A-Za-z0-9][A-Za-z0-9 ._@/+-]{0,63}`; versions match
|
|
156
|
+
`[A-Za-z0-9][A-Za-z0-9._+-]{0,31}`, each as a whole string. Names allow up to
|
|
157
|
+
64 characters, versions up to 32. `__proto__`, `constructor`, and `prototype`
|
|
158
|
+
are reserved. Invalid values are absent, never truncated or escaped. Capabilities and other client metadata never enter activity.
|
|
159
|
+
Legacy requests without an envelope leave client facts absent.
|
|
160
|
+
Every emitted activity event also carries `packageVersion`, generated from
|
|
161
|
+
`package.json` before build and independent of the display-only `serverInfo`.
|
|
162
|
+
Doctor uses and reports that same generated version. SQL stores validate
|
|
163
|
+
package versions against a bounded release-version grammar on write and read;
|
|
164
|
+
the nullable `package_version` migration leaves old rows unknown rather than
|
|
165
|
+
assigning today's version to history. `/ui/api/activity` serves these facts
|
|
166
|
+
read-only with the same authorization and validation as `/ui/activity`.
|
|
167
|
+
Catalog changes emit discrete, payload-free activity events after accepted
|
|
168
|
+
refreshes. Reviewed drift observations remain in authenticated connector status
|
|
169
|
+
and the maintainer-run provider check; public health counts are removed.
|
|
170
|
+
|
|
171
|
+
`server/discover` advertises `io.modelcontextprotocol/skills` and `resources: {}`
|
|
172
|
+
with private one-hour discovery cache hints. It includes the configured identity, icons, title,
|
|
173
|
+
and website in `io.modelcontextprotocol/serverInfo` result metadata. The Skills
|
|
174
|
+
extension and its skill-only resources share `src/skills.ts` with the meta-tool
|
|
175
|
+
and guest API. Both direct
|
|
176
|
+
call tools declare `address` as `x-mcp-header: "Address"`, which the SDK mirrors
|
|
177
|
+
and validates as `Mcp-Param-Address` on modern requests.
|
|
178
|
+
|
|
179
|
+
Connecta's own admission, deadline, pool, and access refusals use JSON-RPC error
|
|
180
|
+
bodies with `Cache-Control: no-store` and the HTTP statuses below. They omit the
|
|
181
|
+
unknown RPC id because the route refuses before decoding the RPC body. An aborted
|
|
182
|
+
request releases its resources without cancelling a newly created deadline
|
|
183
|
+
error body. Auth adapters continue to own their challenges and refusal bodies.
|
|
184
|
+
Admission and Connecta-owned refusal bodies share the MCP exchange's
|
|
185
|
+
request-scoped sent-credential set and pass through `redactAgentOutput` before
|
|
186
|
+
serialization. The SDK's `server/discover` response and registered meta-tool
|
|
187
|
+
results use that same set; connector contexts retain only an opaque scope.
|
|
188
|
+
|
|
189
|
+
| Refusal | HTTP status | Application code | Previous code |
|
|
190
|
+
| ----------------------------------- | ----------- | ---------------- | ------------- |
|
|
191
|
+
| Request capacity exhausted | 503 | `-33001` | `-31001` |
|
|
192
|
+
| Server shutting down | 503 | `-33002` | `-31002` |
|
|
193
|
+
| Request lifetime exceeded | 504 | `-33003` | none |
|
|
194
|
+
| Pool missing or refused | 404 | `-33004` | none |
|
|
195
|
+
| Origin or admitted access forbidden | 403 | `-33005` | none |
|
|
196
|
+
|
|
197
|
+
These allocations are outside JSON-RPC's reserved `-32768..-32000` range and
|
|
198
|
+
MCP's `-32020..-32099` range. The prior admission codes were also outside those
|
|
199
|
+
ranges; the migration changes the application allocation, not a reserved-code
|
|
200
|
+
violation. See the [2026-07-28 error-code policy](https://modelcontextprotocol.io/specification/2026-07-28/basic#error-codes).
|
|
201
|
+
|
|
202
|
+
`connecta doctor` uses SDK auto negotiation, checks the exported
|
|
203
|
+
`META_TOOL_NAMES` set, and reports the negotiated revision. It closes its client
|
|
204
|
+
on success or failure and refuses credential-bearing redirects during discovery,
|
|
205
|
+
legacy initialization, and tool calls.
|
|
206
|
+
|
|
102
207
|
## Layers below the meta-tools
|
|
103
208
|
|
|
104
209
|
The meta-tool handlers are thin. The work sits in six modules the registry owns
|
|
105
210
|
or hands out, and a change usually belongs in exactly one of them:
|
|
106
211
|
|
|
107
|
-
| Module
|
|
108
|
-
|
|
|
109
|
-
| `src/registry.ts`
|
|
110
|
-
| `src/catalog-
|
|
111
|
-
| `src/
|
|
112
|
-
| `src/
|
|
113
|
-
| `src/
|
|
114
|
-
| `src/
|
|
212
|
+
| Module | Owns |
|
|
213
|
+
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
214
|
+
| `src/registry.ts` | The connector set, identity-scoped views, personal storage partitions, address resolution, request-local complete-catalog reads, connector health, per-connector call limiters, and drift. Construction-time refusals live here. |
|
|
215
|
+
| `src/catalog-cache.ts` | SQL-backed SDK tools/resource/template listing cache, intake redaction, TTL bounds, host auth partitions, invalidation generations, and completed-refresh observations. |
|
|
216
|
+
| `src/catalog-service.ts` | Request-local listing, search, and describe. Caches catalogs inside one request and fans discovery probes out under deadlines. |
|
|
217
|
+
| `src/invocation.ts` | One tool call: argument validation, call admission, one-attempt timeout, provider retry hints, result unwrapping, size capping, and the activity record. |
|
|
218
|
+
| `src/catalog.ts` | Ranking, description summarizing, and the compact and TypeScript schema renderers discovery shows. |
|
|
219
|
+
| `src/result-shapes.ts` | Bounded runtime-only inference and merging for output shapes learned from successful read-only calls whose providers declared none. |
|
|
220
|
+
| `src/program-writes.ts` | Trusted-pool write accounting: write budgets, dispatch draining, and known or unknown outcomes. |
|
|
115
221
|
|
|
116
222
|
`src/meta-tools.ts` and `src/execute.ts` are two front doors onto the same two
|
|
117
223
|
services, `CatalogService` and `InvocationService`. That is the point: a
|
|
@@ -121,11 +227,18 @@ same credential resolution, and the same fail-closed read-only check.
|
|
|
121
227
|
`test/execute.test.ts` asserts the parity directly, because a sandbox path that
|
|
122
228
|
quietly diverges is how generated code would mint a capability.
|
|
123
229
|
|
|
230
|
+
Program result paging shares `meta-tools.ts`'s direct-call stash envelope and
|
|
231
|
+
reader. `program-result.ts` owns executor-independent byte limits;
|
|
232
|
+
`execute.ts` issues bounded transfer cursors, and `guest-runtime.ts` reconstructs
|
|
233
|
+
values through provider preludes. Cursors hold only an ID and offset for the
|
|
234
|
+
current run. Result bytes, expiry, authority and capacity charges stay in the
|
|
235
|
+
existing storage and ledger; no executor-specific store or storage key is added.
|
|
236
|
+
|
|
124
237
|
## Admission, in two places
|
|
125
238
|
|
|
126
239
|
Request admission (`src/executor-admission.ts`, applied in `src/routes/mcp.ts`)
|
|
127
240
|
bounds the MCP envelope: one deployment-wide FIFO pool, plus a deliberately
|
|
128
|
-
smaller code pool a program takes a
|
|
241
|
+
smaller code pool a program takes a _second_ permit from, so one request cannot
|
|
129
242
|
trade ordinary capacity for unbounded sandboxes. `admission.code` is only a
|
|
130
243
|
fallback — an executor implementing `acquire()` owns a bounded pool already, its
|
|
131
244
|
settings win, and connecta warns the fallback was ignored. Invalid bounds throw
|
|
@@ -166,16 +279,16 @@ queue. Health checks and subsequent MCP requests confirmed the same isolate ID.
|
|
|
166
279
|
The [recorded observations](https://github.com/zackbart/connecta/blob/a0ac904513fac735582b4bc9c817a48300134328/scripts/probes/worker-disconnect-2026-09-25.json)
|
|
167
280
|
contain all twelve cases, including served flags and client-end timing.
|
|
168
281
|
|
|
169
|
-
| Response and flag
|
|
170
|
-
|
|
|
171
|
-
| Heartbeats, no flag
|
|
172
|
-
| Idle with a pending timer, no flag
|
|
173
|
-
| Heartbeats, request signal enabled
|
|
174
|
-
| Idle with a pending timer, request signal enabled
|
|
175
|
-
| One chunk, then no timer or I/O, either configuration | neither
|
|
282
|
+
| Response and flag | Raw stream abort / cancel | Connecta source abort / cancel | Admission after response; next request |
|
|
283
|
+
| ----------------------------------------------------- | ------------------------- | ------------------------------ | -------------------------------------- |
|
|
284
|
+
| Heartbeats, no flag | neither | neither | active 1; 503 |
|
|
285
|
+
| Idle with a pending timer, no flag | neither | neither | active 1; 503 |
|
|
286
|
+
| Heartbeats, request signal enabled | abort only | both | active 0; 200 |
|
|
287
|
+
| Idle with a pending timer, request signal enabled | abort only | both | active 0; 200 |
|
|
288
|
+
| One chunk, then no timer or I/O, either configuration | neither | neither | active 1; 503 |
|
|
176
289
|
|
|
177
290
|
The last row is a different failure. The client observed the response end
|
|
178
|
-
|
|
291
|
+
_before_ its planned disconnect, although the source never closed itself.
|
|
179
292
|
The runtime ended the response without running JavaScript cleanup. This does
|
|
180
293
|
not show a missing abort on a still-live request. The other cases remained open
|
|
181
294
|
until the client aborted. Admission was sampled 1.5 seconds later, followed by
|
|
@@ -217,26 +330,193 @@ exposed by the deployment; these findings are tied to the date and flags above.
|
|
|
217
330
|
|
|
218
331
|
## Storage, credentials, and connectors
|
|
219
332
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
333
|
+
A deployment has one store. On Workers it is one D1 database
|
|
334
|
+
(`d1Storage(env.CONNECTA_DB)` from `@zackbart/connecta/d1`); on Node it is one
|
|
335
|
+
SQLite file (`sqliteStorage(path)` from `@zackbart/connecta/sqlite`, over the
|
|
336
|
+
built-in `node:sqlite`). Both are the same SQL key-value store
|
|
337
|
+
(`src/storage/sql.ts`) over a three-method driver, so every statement runs
|
|
338
|
+
unchanged on each, and the same module carries the activity table beside it.
|
|
339
|
+
Each store creates its tables on first use. `memoryStorage()` is the default
|
|
340
|
+
and the test double. Workers KV and the 0.28 JSON file store are gone: KV is
|
|
341
|
+
eventually consistent and cannot compare-and-set, and the file store rewrote
|
|
342
|
+
its whole file on every write. Each has a one-shot copy into its replacement,
|
|
343
|
+
`copyKvToD1` on `/d1` and `connecta migrate-state` on Node. Both copy live
|
|
344
|
+
entries verbatim, because physical keys did not change, and both keep any key
|
|
345
|
+
the target already holds. `copyKvToD1` reports counts per key family
|
|
346
|
+
(`familyOfKey` in `src/storage/keys.ts`), never a key. It resumes through a
|
|
347
|
+
source-bound, atomically claimed token, because Workers KV's own list cursor
|
|
348
|
+
can spell a key. Copy under maintenance after KV stabilizes, verify hashes
|
|
349
|
+
and expiries, and mark cutover before reopening traffic; the permanent D1
|
|
350
|
+
marker refuses stale copies afterward.
|
|
351
|
+
|
|
352
|
+
`KVStorage` is `get`/`set`/`delete`/`list(prefix)`/`compareAndSet`, all
|
|
353
|
+
required, and `createConnecta` refuses storage missing one (INV-11).
|
|
354
|
+
Adapters must also declare `capabilities: { absoluteExpiry: true }`.
|
|
355
|
+
Construction rejects missing or false capability declarations with an error
|
|
356
|
+
naming `capabilities.absoluteExpiry`. The memory, SQLite, and D1 adapters carry
|
|
357
|
+
this marker. Custom adapters may declare it only after implementing absolute
|
|
358
|
+
expiry on both writes; a legacy `ttlSeconds`-only adapter is rejected by the
|
|
359
|
+
published types and at runtime.
|
|
360
|
+
`compareAndSet(key, expected, next, options?)` is an atomic claim: `null` means
|
|
361
|
+
absent (expired counts) on the way in and delete on the way out, and a
|
|
362
|
+
successful write takes the same expiry options as `set`. Relative `ttlSeconds`
|
|
363
|
+
is created and checked by the database clock inside each statement, so isolate
|
|
364
|
+
clock skew cannot expire a live holder's record. Absolute `expiresAtMs` is a
|
|
365
|
+
safe-integer epoch-millisecond deadline stored verbatim and checked against the
|
|
366
|
+
storage clock. Supply at most one option. Custom adapters must implement both;
|
|
367
|
+
a delayed absolute-expiry write must not restart a relative TTL at commit time. Each SQL claim
|
|
368
|
+
is one statement, which SQLite executes atomically and D1 serializes on its
|
|
369
|
+
primary. Everything that must claim a key exactly once relies on it with no
|
|
370
|
+
read-then-write fallback: access-token issuance and
|
|
371
|
+
capacity, OAuth handoff ownership, single-use connect links, every downstream
|
|
372
|
+
OAuth grant write and consent claim, and the result stash.
|
|
373
|
+
|
|
374
|
+
Every key is built in `src/storage/keys.ts`, which lists each family with its
|
|
375
|
+
scope, version, codec, and TTL policy; `test/storage-keys.node.test.ts` fails when
|
|
376
|
+
two families overlap, another source file spells a prefix (constant concatenation
|
|
377
|
+
folded), or a storage call passes a key holding literal text. Core hands
|
|
378
|
+
subsystems namespaced views: `conn:<id>:` per connector, `principal:<key>:` per
|
|
379
|
+
personal registry, `results:` and `subject:<key>:` for result paging.
|
|
380
|
+
|
|
381
|
+
Keys and list prefixes must not contain U+0000 (NUL). D1, SQLite, and memory
|
|
382
|
+
storage reject them with `TypeError` before accessing storage: Node 22's
|
|
383
|
+
`node:sqlite` truncates TEXT results at NUL. Builders reject NUL in unencoded
|
|
384
|
+
components.
|
|
385
|
+
State-file import validates all keys before writing. The `connecta_kv` table
|
|
386
|
+
keeps its existing TEXT keys, including compatibility with the 0.28 schema.
|
|
387
|
+
Storage writes no log lines. A refused import names the file and an entry's
|
|
388
|
+
position, never a key, a value, or the JSON parser's account, which quotes
|
|
389
|
+
the file (INV-6). Database failures report only a fixed import step and an
|
|
390
|
+
entry's position, including errors raised by target-database triggers.
|
|
391
|
+
|
|
392
|
+
Remote MCP catalogs use the SDK `ResponseCacheStore` over this same SQL KV.
|
|
393
|
+
The `response-cache:v1:` family binds connector id, a hash of its configured
|
|
394
|
+
endpoint/auth/protocol settings and TTL policy, an invalidation generation,
|
|
395
|
+
and a hash of the listing method and host-computed auth partition. Later pages
|
|
396
|
+
can only narrow the aggregate TTL and sharing within that partition. Request
|
|
397
|
+
tokens and personal OAuth or credential auth always use private entries,
|
|
398
|
+
including when the downstream declares `public`. Private partitions bind the
|
|
399
|
+
admitted principal, subject, identity, pool, endpoint, credential digest, and
|
|
400
|
+
OAuth epoch. Operator-shared credentials and unauthenticated connectors may
|
|
401
|
+
share public-hinted listings across principals within the same admitted pool
|
|
402
|
+
and endpoint. Private hints retain principal isolation even with shared auth.
|
|
403
|
+
The adapter refuses the SDK's empty shared slot for private auth modes. OAuth
|
|
404
|
+
listings resolve the current credential digest even within one epoch; a
|
|
405
|
+
credential change during a walk prevents publication under the old digest.
|
|
406
|
+
Each remote connector instance owns its request's cache lifetime separately,
|
|
407
|
+
so closing a temporary client does not end another instance with the same id.
|
|
408
|
+
Only complete, intake-redacted tools/list, resources/list and
|
|
409
|
+
resources/templates/list successes are stored. Resource payloads are not cached. SDK default
|
|
410
|
+
in-memory caching is replaced, so raw catalogs never enter it.
|
|
411
|
+
|
|
412
|
+
`discovery.catalogTtlSeconds` (300 by default) is the fallback for legacy
|
|
413
|
+
listings without `ttlMs`. Positive hints are bounded by
|
|
414
|
+
`catalogMinTtlSeconds` (0) and `catalogMaxTtlSeconds` (86400, also the SDK
|
|
415
|
+
ceiling); an explicit zero is never reused. Both bounds are nonnegative finite
|
|
416
|
+
seconds (fractions round down to milliseconds), with min <= max <= 86400. `persistCatalog` and `staleCatalogSeconds`
|
|
417
|
+
have been removed. SQL storage is always used for remote catalogs; dynamic
|
|
418
|
+
custom connectors are listed anew in each request; authenticated callers may
|
|
419
|
+
retain a complete private fallback, and static catalogs remain
|
|
420
|
+
in configuration. After a transient first-page tool listing failure, ordinary calls
|
|
421
|
+
may use a complete catalog for up to five minutes after its expiry in the same
|
|
422
|
+
auth partition. Partial walks, permission and auth failures do not use this
|
|
423
|
+
fallback. Resources require unexpired inventories. There is no deferred refresh.
|
|
424
|
+
Host-only provenance carries each catalog's original fetch time, TTL and expiry,
|
|
425
|
+
and whether it was a fallback, through classification and request-local copies.
|
|
426
|
+
Zero TTL and stale fallbacks cannot authorize post-entry auth recovery.
|
|
427
|
+
|
|
428
|
+
A manifest is published by CAS only after every UTF-8 chunk is written. It
|
|
429
|
+
records the original fetch time, absolute expiry, revision, fingerprint, and
|
|
430
|
+
monotonic stamp. Readers verify all chunks and fingerprint before accepting it.
|
|
431
|
+
Freshness expires at the bounded TTL; tool facts remain for the five-minute
|
|
432
|
+
fallback window, and generations expire after 48 hours; invalidation
|
|
433
|
+
rotates the generation, and old chunks expire without becoming reachable
|
|
434
|
+
again. Remote clients pin that generation before listing, including SDK cache
|
|
435
|
+
refreshes. Each new listing pins the current generation, so a live client can
|
|
436
|
+
publish a new catalog after invalidation without letting an older listing
|
|
437
|
+
adopt its generation. A public transport observer starts a generation rotation
|
|
438
|
+
before the SDK processes tool or resource list-changed notifications; its lifetime is
|
|
439
|
+
the connection's, including after a successful resource read or listing ends.
|
|
440
|
+
Opposite-scope cleanup only deletes its entry. SDK listings serialize per client
|
|
441
|
+
because the cache store has no per-call options. Their operation binding lasts
|
|
442
|
+
through the SDK's cleanup, and each cache method captures that binding before
|
|
443
|
+
awaiting. Cache I/O checks cancellation and scope teardown at every await
|
|
444
|
+
boundary. Cancelled storage waits release the listing without starting more
|
|
445
|
+
I/O when an already-dispatched storage operation settles. Teardown waits a
|
|
446
|
+
bounded time for generation rotations already started. Catalog age remains the
|
|
447
|
+
original fetch age across process restarts and cache hits.
|
|
448
|
+
`test/catalog-cache.test.ts` runs against real SQLite and D1.
|
|
449
|
+
|
|
450
|
+
`observeCompletedCatalogRefresh` in `src/catalog-cache.ts` is the accepted
|
|
451
|
+
refresh publication hook. The SDK tools/list wrapper calls it once after a
|
|
452
|
+
complete, unfenced wire refresh, including zero-TTL results. Custom dynamic
|
|
453
|
+
connectors publish their complete intake through the same hook. Cache hits,
|
|
454
|
+
rejected or abandoned walks, and identical catalogs emit no change event.
|
|
455
|
+
`Registry.recordCatalogDrift` compares the previous and next tool fingerprints
|
|
456
|
+
from this hook, preserving the requesting actor, request id, pool and deferred
|
|
457
|
+
activity write. The first complete catalog is a baseline.
|
|
458
|
+
|
|
459
|
+
Hash-only baselines publish bounded chunks before a manifest. They store the
|
|
460
|
+
catalog digest and hashes of each tool name and fact, so added, removed and changed counts require no persisted tool names or
|
|
461
|
+
schemas. They use the exact host auth/config cache partition, survive catalog
|
|
462
|
+
expiry and invalidation, and expire after 48 hours. There is no connection-wide
|
|
463
|
+
fallback baseline across partitions. Private catalog events use the admitted
|
|
464
|
+
principal actor basis even for shared-auth connectors; activity disclosure
|
|
465
|
+
requires that same owner and the recorded pool's grants. Ownerless private
|
|
466
|
+
observations cannot be disclosed. Public-hinted shared-auth catalogs retain
|
|
467
|
+
shared history within their admitted pool and endpoint.
|
|
468
|
+
|
|
469
|
+
Result paging stores each oversized result for 15 minutes, chunked so a page
|
|
470
|
+
reads only what it covers. Its bounds (`results.maxStashBytes`,
|
|
471
|
+
`results.maxStashEntries`) are the deployment's, not an isolate's: every charge
|
|
472
|
+
is a row in one ledger record, booked by compare-and-set before any chunk is
|
|
473
|
+
written, so isolates and processes sharing the store see one count. Every
|
|
474
|
+
reservation starts with a finite deadline of booking time plus the 30-second
|
|
475
|
+
write budget plus the chunk TTL, normally 15 minutes. Pending writes retain
|
|
476
|
+
that charge until its deadline. Booking retries recognize their own reservation
|
|
477
|
+
ID and reuse its stored deadline, including a final read after an ambiguous
|
|
478
|
+
last CAS. A lost swap or transient storage error backs off and re-reads, up to
|
|
479
|
+
32 attempts. A full ledger or unconfirmed exhausted booking returns no result id.
|
|
480
|
+
|
|
481
|
+
Caller time only nominates charges for pruning. Before discarding one, the
|
|
482
|
+
ledger writes a probe with that charge's absolute deadline and reads it back.
|
|
483
|
+
Only an absent probe proves storage has passed the deadline; a live probe keeps
|
|
484
|
+
the charge, and a storage error cannot free capacity. Each deadline has its own
|
|
485
|
+
key in `stashExpiryKeys`, so concurrent probes cannot overwrite a different
|
|
486
|
+
deadline's proof. Probes expire at the deadline they check and need no cleanup.
|
|
487
|
+
This uses the same absolute-expiry contract on memory, SQLite and D1 without a
|
|
488
|
+
storage API or schema change. An ahead caller clock cannot admit a second stash
|
|
489
|
+
while the first remains readable.
|
|
490
|
+
|
|
491
|
+
The whole chunk-write phase has a 30-second timeout measured from booking, with
|
|
492
|
+
a clock check before and after each write. Trailing chunks precede the header;
|
|
493
|
+
failure or timeout returns no result id and stops the write loop. Chunks use an
|
|
494
|
+
absolute expiry at booking time plus their TTL, so slow writes shorten the
|
|
495
|
+
remaining paging window. Storage promises cannot guarantee cancellation of an
|
|
496
|
+
already-dispatched database operation, and a rejected client response does not
|
|
497
|
+
prove that operation cannot commit later. Even a commit after timeout or process
|
|
498
|
+
interruption keeps that absolute expiry, rather than starting another TTL.
|
|
499
|
+
|
|
500
|
+
Settlement and release only shorten or remove a reservation. Successful writes
|
|
501
|
+
attempt to shorten the charge to the fixed chunk expiry. Cleanup releases it
|
|
502
|
+
only after all deletions succeed and every dispatched write returned confirmed
|
|
503
|
+
success. Failed cleanup or exhausted release attempts finite settlement when
|
|
504
|
+
no write is unconfirmed. A rejected or timed-out write keeps its original
|
|
505
|
+
reservation because it may still commit after deletion. Each completion path
|
|
506
|
+
has one 15-second budget covering deletion, release, fallback settlement, and
|
|
507
|
+
their storage waits and retries. Successful-write settlement has the same
|
|
508
|
+
15-second budget. A stalled operation stops being awaited when the budget
|
|
509
|
+
expires; its late response starts no further I/O, and no background cleanup
|
|
510
|
+
fiber is started. The caller returns its write outcome even if these
|
|
511
|
+
optimizations cannot finish. Ledger updates retry within the same 32-attempt
|
|
512
|
+
budget. If they fail, time out, crash, or exhaust,
|
|
513
|
+
capacity may be over-held until the booked deadline, at most 15 minutes and
|
|
514
|
+
30 seconds for the normal TTL. The ledger row itself has an absolute expiry at
|
|
515
|
+
its latest live reservation deadline. There are no completion receipts or
|
|
516
|
+
separate recovery rows; expiry probes carry no completion state. Expired data reads as absent and normal storage sweeps
|
|
517
|
+
reclaim the physical rows. A full stash returns the successful call's preview
|
|
518
|
+
and a paging-unavailable notice rather than a result id. Shared failure cases
|
|
519
|
+
live in `test/stash-charge-contract.ts`, exercised by the memory and SQL suites.
|
|
240
520
|
|
|
241
521
|
`src/credentials.ts` is the AES-GCM vault behind the root-exported
|
|
242
522
|
`CredentialVault` contract, selected through the `vault` slot. It binds connector
|
|
@@ -255,23 +535,259 @@ produce instances that take the same catalog, read-only, credential, storage,
|
|
|
255
535
|
invocation, result-size, and activity paths. Every one is deployment
|
|
256
536
|
configuration, never runtime registration. `authScope: "shared" | "personal"`
|
|
257
537
|
partitions connecta-owned context — state, credentials, OAuth, catalogs, observed
|
|
258
|
-
shapes — by principal, and
|
|
538
|
+
shapes — by principal, and _only_ connecta-owned context: a secret a custom
|
|
259
539
|
handler closes over is shared JavaScript state, and `remoteMcp()` refuses the
|
|
260
540
|
literal-headers-plus-personal version of that mistake. Visibility
|
|
261
541
|
(`identity.connectorAccess`) is a separate rule; hiding a connector does not
|
|
262
542
|
change who owns its auth.
|
|
263
543
|
|
|
544
|
+
### Errors and records
|
|
545
|
+
|
|
546
|
+
A failure has two audiences with two rules. The agent that made a call may read
|
|
547
|
+
the downstream's own answer to it: a JSON-RPC error message, an HTTP 4xx
|
|
548
|
+
refusal, `isError` content, or the words a handler put in a
|
|
549
|
+
`ConnectorCallError`. Each upstream request owns a memory-only sent-secrets
|
|
550
|
+
set. Every connector context links its local set into that request, including
|
|
551
|
+
listing, discovery, registry refresh, OAuth refresh, and provider handlers.
|
|
552
|
+
Direct meta-tool invocations and program runs get fresh request identities;
|
|
553
|
+
all operations within one HTTP request share its identity. Credential-slot reads, static
|
|
554
|
+
auth headers, raw confidential OAuth client IDs and secrets, and outbound
|
|
555
|
+
bearer tokens join the set, including tokens rotated during a call. Token and
|
|
556
|
+
revocation requests register client credentials before dispatch, decoding OAuth
|
|
557
|
+
Basic form components so a raw echo is covered too. Public client IDs remain
|
|
558
|
+
visible. Before any diagnostic is truncated or returned, the
|
|
559
|
+
agent boundary replaces these values and their auth prefixes, mixed JSON
|
|
560
|
+
escapes, URL encodings, and base64/base64url forms with `[redacted]`. Final
|
|
561
|
+
transports register sensitive headers and query values after assembling the
|
|
562
|
+
request; custom API handlers use `ctx.fetch`. Values shorter than eight
|
|
563
|
+
characters do not enter the matcher, including Basic usernames, passwords
|
|
564
|
+
and explicitly secret fields such as `client_secret`. Configured short
|
|
565
|
+
credentials produce one payload-free warning per connector; use secrets of
|
|
566
|
+
at least eight characters. Encoded and JSON-escaped echoes of registered
|
|
567
|
+
credentials follow the same redaction rules. Connecta's own messages
|
|
568
|
+
never quote a credential.
|
|
569
|
+
Core uncertainty and retry envelopes share one schema-filtered argument snapshot,
|
|
570
|
+
captured before dispatch and byte budgeting. `writeOnly: true` omits values of
|
|
571
|
+
every length, including nested properties, array items and prefixes, local
|
|
572
|
+
`$ref`/`$defs`, and `allOf`. `oneOf`/`anyOf` must agree on sensitivity. Unresolved
|
|
573
|
+
references, sensitive dynamic-property schemas or other unsupported applicators,
|
|
574
|
+
and traversal limits withhold the entire echo. A private array element withholds
|
|
575
|
+
its containing array to preserve indices. `argsRedacted: true` marks a partial
|
|
576
|
+
or withheld echo as reconciliation context; retries require the original arguments.
|
|
577
|
+
OAuth discovery and consent URLs are refused as a typed, non-retryable failure
|
|
578
|
+
when they contain a known credential, before persistence or reuse. This guard
|
|
579
|
+
checks the request's sent credentials and the current grant's credentials,
|
|
580
|
+
including confidential client secrets. The consent URL's expected `client_id`
|
|
581
|
+
parameter remains public, while echoes elsewhere are refused. URLs are never
|
|
582
|
+
rewritten. OAuth failure
|
|
583
|
+
origins are recorded only for the configured MCP or issuer origin, or the
|
|
584
|
+
origin of discovery metadata whose issuer matches its authorization server.
|
|
585
|
+
An origin matching a sent credential is omitted.
|
|
586
|
+
The matcher is cached until its set changes; the empty set has a fast path.
|
|
587
|
+
`redactAgentOutput` is the agent-facing choke point. Every meta-tool exit,
|
|
588
|
+
including errors, and every host-to-guest value or rejection passes through
|
|
589
|
+
it. Both MCP transports also pass their serialized response through it, covering
|
|
590
|
+
JSON-RPC errors, HTTP diagnostics, structured content, and paging responses.
|
|
591
|
+
Per-call redaction remains before diagnostic truncation, paging, and emits. Paging stores only request-redacted text before encoding
|
|
592
|
+
chunks, so a later request needs no original credentials. Programs retain the
|
|
593
|
+
request set only for the run so later outputs cannot reconstruct an echo.
|
|
594
|
+
Discovery registers sent credentials under the same rules, including
|
|
595
|
+
`server/discover`, legacy initialization, and every `tools/list` page on a
|
|
596
|
+
reused transport. Remote MCP sanitizes every list page before SDK aggregation, automatic cache
|
|
597
|
+
writes, derived indexes, or request-local definitions. Invalid `x-mcp-header`
|
|
598
|
+
declarations refuse the entire listing before the SDK can log downstream names
|
|
599
|
+
or silently exclude a tool. Registry intake also sanitizes custom, API and
|
|
600
|
+
provider listings before observation or request-local storage. Failed listings
|
|
601
|
+
are redacted in their own request and are never cached. Successful catalog caches and result
|
|
602
|
+
stashes retain only intake-redacted values; there is no tool-response cache.
|
|
603
|
+
Every nested string and object key passes through the redactor,
|
|
604
|
+
including titles, descriptions, schemas and annotations. If a tool name would
|
|
605
|
+
change, the complete catalog is refused. Rewriting a name changes dispatch;
|
|
606
|
+
dropping only that tool would publish a partial catalog, against INV-8. Later
|
|
607
|
+
cached reads need no credential set because the stored facts are already clean.
|
|
608
|
+
Echoed sensitive header lines are also withheld. The set is never persisted
|
|
609
|
+
or logged. Anything else (a transport, parser, stream, validator, or
|
|
610
|
+
runtime error) reaches it in connecta's words: step, origin, HTTP status, and
|
|
611
|
+
class, classified as the original would have been ([auth](./auth.md#what-a-servers-errors-may-say)).
|
|
612
|
+
Operators read logs, status messages, and activity, and none of them carries
|
|
613
|
+
any error's text, allowed or not (INV-6). `src/operator-record.ts` builds every
|
|
614
|
+
failure record from an explicit list of fields, each checked against a constant
|
|
615
|
+
table or grammar (connector, catalog-listed tool, step, origin, HTTP status,
|
|
616
|
+
class, code, retryability, errno, counts), and `logFailure` writes a fixed
|
|
617
|
+
`<rejected>` for a record it did not build, never throwing. Provenance is by
|
|
618
|
+
identity, never by shape: an error's class is read from its prototype chain,
|
|
619
|
+
never its `name`; a classification only from one connecta registered; a tool
|
|
620
|
+
only from the catalog entry a call resolved to, and only if it fits MCP's
|
|
621
|
+
tool-name grammar (else `<withheld>`, in activity rows too); and a status
|
|
622
|
+
message only from the snapshot connecta took of a status it created, so a
|
|
623
|
+
plugin `status()` or a decorator contributes its state alone. workerd quotes a
|
|
624
|
+
body's Content-Type in its own output when a text read meets a type it does not
|
|
625
|
+
parse as text, so connecta reads downstream bodies as bytes
|
|
626
|
+
(`src/byte-read-response.ts`), `ctx.oauth.fetch()` answers included. The
|
|
627
|
+
operator page may show catalog metadata the operator loaded (descriptions,
|
|
628
|
+
schemas), but withholds a tool name outside the grammar as it does in records.
|
|
629
|
+
`test/operator-record-sources.node.test.ts` is a secondary lint over every other
|
|
630
|
+
log call in `src/`. Fix the sink, not the source: a filter at each source missed
|
|
631
|
+
the next one.
|
|
632
|
+
|
|
633
|
+
## Configuration
|
|
634
|
+
|
|
635
|
+
`ConnectaConfig` is one schema (`src/config.ts`, combinators in
|
|
636
|
+
`src/config-schema.ts`). The same value produces the TypeScript type a
|
|
637
|
+
deployment writes, the unknown-key walk createConnecta runs before reading any
|
|
638
|
+
value, every default (`src/config-defaults.ts`, which the enforcing modules
|
|
639
|
+
read too), and one validation policy: a present value that is wrong throws
|
|
640
|
+
with its path at construction (INV-11). Nothing warns and falls back. The walk
|
|
641
|
+
reads property descriptors, never properties, and builds the plain copy that
|
|
642
|
+
is resolved in place of the caller's object, so no getter or Proxy trap runs;
|
|
643
|
+
an accessor on a config object or array is refused by path, as is an object
|
|
644
|
+
that cannot be inspected. A closed object or record slot takes a plain object
|
|
645
|
+
only: an array or a class instance there is refused by path, unread. String
|
|
646
|
+
maps (static `headers`, `api()`'s `authorizationParams` and
|
|
647
|
+
`tokenRequestHeaders`) are copied the same way and each value must be a
|
|
648
|
+
string. A discriminated union (`remoteMcp()`'s `auth.type`,
|
|
649
|
+
a provider's `surface`) with a missing or unrecognized discriminant throws
|
|
650
|
+
with the valid values. Only plain data is copied. Connectors, modules,
|
|
651
|
+
executors, storage, loggers, handlers, and schemas are opaque and never
|
|
652
|
+
entered. An `api()` tool carries its handler, so it is checked in place (own
|
|
653
|
+
keys declared, no accessor at a declared key, inherited ones included) and
|
|
654
|
+
passed through as given: a class-instance tool keeps its prototype `handler()`
|
|
655
|
+
and its private fields. Configuration is operator-authored and trusted, as in
|
|
656
|
+
#698; the walk refuses mistakes by path and never echoes a value, and is not
|
|
657
|
+
a sandbox for hostile objects. Records keyed by deployment
|
|
658
|
+
names (pools, `classification`) have no prototype, so `__proto__` is a name.
|
|
659
|
+
Built-in factories (`api()`, `remoteMcp()`, every provider, and each module
|
|
660
|
+
factory) walk their own options the same way, against shapes the compiler
|
|
661
|
+
checks against their option types (`optionsOf<T>()`). Provider definitions
|
|
662
|
+
declare their closed `options` shape; `defineProvider()` validates it by the
|
|
663
|
+
same descriptor walk before `create` reads a value and stamps the definition
|
|
664
|
+
name onto `describe().source.provider`. `classify` is accepted by the remote
|
|
665
|
+
shape and validated by the shared reviewed-classification validator. Custom
|
|
666
|
+
connectors remain opaque, including their `classification` field, which the
|
|
667
|
+
registry validates. Checks
|
|
668
|
+
that need the connector set — pool members, exact classification overrides, a
|
|
669
|
+
connector's own `maxResultBytes` — throw from `src/index.ts` and the registry
|
|
670
|
+
under the same policy. `storage` stays opaque, but its check requires `list`
|
|
671
|
+
and `compareAndSet` as well as `get`, `set`, and `delete`, so a leftover
|
|
672
|
+
Workers KV adapter is refused at boot with the replacements named, not at the
|
|
673
|
+
first OAuth callback.
|
|
674
|
+
|
|
675
|
+
`resolveConfig` returns a frozen `ResolvedConfig`: defaults applied, auth
|
|
676
|
+
ordered, `serverInfo` named and versioned.
|
|
677
|
+
Routes and meta-tools read limits from it (`ServerOptions.config`) instead of
|
|
678
|
+
from fields copied out one at a time. `Connecta.describeConfig()` is a
|
|
679
|
+
secret-free snapshot of it, built once (`src/describe-config.ts`): an allowlist
|
|
680
|
+
serializer that copies named fields and never spreads a config object, plus
|
|
681
|
+
`Connector.describe()` on `remoteMcp()`, `api()`, and the providers. Header values, keys, client secrets, credentials, URL
|
|
682
|
+
userinfo, queries, and fragments, and function bodies never appear; every URL
|
|
683
|
+
a description emits, from `describeConfig()` or a direct `describe()`, passes
|
|
684
|
+
through the sanitizers in `src/described.ts`, which keep http(s) URLs only
|
|
685
|
+
(`blob:` wraps a URL with its userinfo in the path);
|
|
686
|
+
`test/describe-config.test.ts` plants one in each position. Both deployment
|
|
687
|
+
shapes write their configuration as `defineConfig((env) => …)` in
|
|
688
|
+
`src/connecta.config.ts`, with optional modules as type-checked expressions
|
|
689
|
+
switched by the environment, and keep their entries under 30 lines.
|
|
690
|
+
|
|
691
|
+
### Providers and reviewed classification
|
|
692
|
+
|
|
693
|
+
Infisical separates explicit value reads from identifier results. `get_secret`
|
|
694
|
+
and `list_secrets` with `includeValues: true` return secret values and comments.
|
|
695
|
+
All other results, including imported secrets and pending approvals, allow only
|
|
696
|
+
checked IDs, keys, environment and tag slugs, paths, timestamps, positive versions
|
|
697
|
+
and enum fields. They omit project slugs derived from free-text names, comments,
|
|
698
|
+
descriptions, free-form names, reminder notes and arbitrary nested objects.
|
|
699
|
+
`metadataOmitted: true` reports withheld or unavailable metadata.
|
|
700
|
+
This assumes an honest server and protects against careless human entry in free
|
|
701
|
+
text. A malicious server encoding values into valid identifiers is out of scope.
|
|
702
|
+
Known, accepted residual risk: environment slugs are returned as required
|
|
703
|
+
routing identifiers. Infisical's UI derives them from environment names by
|
|
704
|
+
default, so secrets entered in those names can appear in returned slugs.
|
|
705
|
+
Operators must not put secrets in environment names or slugs. Secrets placed in
|
|
706
|
+
identifiers or key names are out of scope.
|
|
707
|
+
Write input schemas mark secret values, secret comments and folder descriptions
|
|
708
|
+
as `writeOnly`, so recovery argument echoes omit them without changing dispatch.
|
|
709
|
+
Submitted values never join the request's sent-credential set, preserving later
|
|
710
|
+
program arguments. Client credentials and tokens still join that set on every
|
|
711
|
+
call. The [provider guide](https://github.com/zackbart/connecta/blob/main/src/providers/infisical/SKILL.md) owns routing and
|
|
712
|
+
value-read conventions.
|
|
713
|
+
|
|
714
|
+
A maintained provider is one `defineProvider()` call (`src/provider.ts`): a
|
|
715
|
+
name, title, kind (`"mcp"`, `"api"`, or `"composed"`), a maintained skill, an
|
|
716
|
+
optional reviewed classification, a closed `options` shape declared with
|
|
717
|
+
`optionsOf<O>()`, and a synchronous `create`. The root exports the option-shape
|
|
718
|
+
combinators and `PROVIDER_COMMON` so provider authors use the same validation
|
|
719
|
+
path. The factory refuses undeclared keys and accessors before reading any
|
|
720
|
+
option, copies plain data, preserves behaviour objects, then validates common
|
|
721
|
+
values (purpose, title, instructions, `authScope`) before `create` runs, and renders the guide: heading, the
|
|
722
|
+
connection context `create` supplies, the maintained text, then deployment
|
|
723
|
+
instructions, which append and never replace. `src/provider.ts` imports neither
|
|
724
|
+
transport, so an `api()` provider gains no MCP client or Effect graph from it
|
|
725
|
+
(`test/purity.node.test.ts`). The factory carries its `definition`, which build and
|
|
726
|
+
check tools read instead of keeping provider lists. All twenty providers
|
|
727
|
+
live in their own folders. The eight hosted implementations, including the
|
|
728
|
+
MCP branches of Notion, Vercel, and Cloudflare, use reviewed presets over
|
|
729
|
+
`remoteMcp({ classify })`. The eleven API-only factories retain an internal
|
|
730
|
+
adapter to the same definition and description-stamping path. GitHub composes hosted tools with a scope-enforced REST complement.
|
|
731
|
+
Notion, Vercel and Cloudflare default to hosted MCP and select explicit REST
|
|
732
|
+
complements with `surface: "api"`. Configure both under distinct connector ids
|
|
733
|
+
with independent credentials when both are needed. Notion preserves its
|
|
734
|
+
internal-integration identity under `integration_*` names; Vercel and Cloudflare
|
|
735
|
+
retain documented REST gaps rather than duplicate hosted operations. See the
|
|
736
|
+
[provider migration guide](./provider-migration-0.29.md).
|
|
737
|
+
|
|
738
|
+
`remoteMcp({ classify })` is the public way to declare what a downstream's
|
|
739
|
+
tools do: `{ tools: { name: "read" | "write" | "destructive" | { verdict,
|
|
740
|
+
reason?, schemaDigest? } }, unlisted?: "hide" }`, validated at construction. It fails closed
|
|
741
|
+
(INV-1): a reviewed read fills silence but yields to an explicit write
|
|
742
|
+
annotation, a reviewed write stays a write whatever the downstream claims, an
|
|
743
|
+
unlisted tool is a read only when it says so, and a reviewed tool whose
|
|
744
|
+
`schemaDigest` no longer matches, or cannot be checked, is a write on discovery
|
|
745
|
+
and every invocation path until a release reviews it again. A digest covers
|
|
746
|
+
the whole schema; one too large to hash whole is unchecked. `unlisted: "hide"`
|
|
747
|
+
removes every unreviewed name, even an explicitly annotated read, on every
|
|
748
|
+
registry read, including persisted catalogs. `remoteMcp` also refuses direct
|
|
749
|
+
calls to unlisted names before authentication. Tool classification does not
|
|
750
|
+
authorize targets; providers such as GitHub check arguments and configured
|
|
751
|
+
scopes separately before minting credentials.
|
|
752
|
+
|
|
753
|
+
Connectors report facts; the registry is the only classifier. A reviewed
|
|
754
|
+
connector carries its review as data, the deep-frozen
|
|
755
|
+
`Connector.classification`, and its `listTools` returns the downstream's
|
|
756
|
+
listing unclassified. The registry validates the field when it first reads a
|
|
757
|
+
connector (INV-11) and classifies the intake-redacted facts on every read into
|
|
758
|
+
fresh objects. Remote MCP caches the underlying SDK response before connector
|
|
759
|
+
decorators; decorators run again on every read. Neither a cache layer nor a
|
|
760
|
+
decorator holding a served or listed tool can carry a verdict. The request-scoped catalog also owns a deep copy
|
|
761
|
+
and returns fresh copies to discovery and invocation. Each connector call
|
|
762
|
+
receives a separate deep copy of its definition, so mutations cannot change
|
|
763
|
+
classification, schemas, or write accounting later in the same program.
|
|
764
|
+
A restart applies the current review to cached SDK facts. Retired registry
|
|
765
|
+
manifest versions 2 and 3 are ignored and expire on their original TTL.
|
|
766
|
+
During successful listings the registry counts drift against the same record, and `scripts/drift-check.mjs` compares
|
|
767
|
+
its names with published inventories. The internal `withVettedCatalog()`
|
|
768
|
+
helper has been removed. Public provider `*_VETTED_CATALOG` exports remain
|
|
769
|
+
deprecated aliases derived from each definition
|
|
770
|
+
(`test/classified-decorators.test.ts`, `test/hosted-presets.test.ts`).
|
|
771
|
+
|
|
772
|
+
Wrappers must forward `classification` to keep the review. `{ ...connector }`,
|
|
773
|
+
`Object.assign`, and `Object.create` keep it. A forwarding class that omits it
|
|
774
|
+
serves an unreviewed connector: the downstream's annotations are its own
|
|
775
|
+
claims and fail closed when absent, and nothing it persists carries safety.
|
|
776
|
+
Phase 2's deployment-level classifier overrides, keyed by connector id and
|
|
777
|
+
tool ([#706](https://github.com/zackbart/connecta/issues/706)), apply
|
|
778
|
+
regardless of wrapping.
|
|
779
|
+
|
|
264
780
|
## Optional deployment modules
|
|
265
781
|
|
|
266
|
-
`createConnecta` takes closed typed `ui`, `vault`,
|
|
267
|
-
slots, with factories at `/ui`, `/credentials`,
|
|
268
|
-
and
|
|
782
|
+
`createConnecta` takes closed typed `ui`, `vault`, and `activity`
|
|
783
|
+
slots, with factories at `/ui`, `/credentials`, and `/activity`
|
|
784
|
+
and machine tokens at `/auth/access-tokens`. Root exports the contracts, never the implementations, and there
|
|
269
785
|
is no module array, runtime registration, or plugin lifecycle. Core keeps
|
|
270
786
|
discovery, the executor contract, invocation, permissions, and OAuth callback
|
|
271
787
|
verification; an omitted module contributes no runtime work at all.
|
|
272
788
|
|
|
273
789
|
The operator UI — `src/ui.ts` (data-free shell and `/ui/data` payload),
|
|
274
|
-
`src/routes/ui.ts`, `src/operator-ui/` (the
|
|
790
|
+
`src/routes/ui.ts`, `src/operator-ui/` (the React app, owned Radix primitives and its pure rules) —
|
|
275
791
|
shows a human what a deployment exposes and manages only the authentication
|
|
276
792
|
material code explicitly permitted; it never edits the connector set, catalog,
|
|
277
793
|
annotations, scopes, or permission rules. Two invariants shape it: a status read
|
|
@@ -280,16 +796,16 @@ and each lazy details request owns a bounded downstream scope, so one failing
|
|
|
280
796
|
provider leaves the other connections usable. A connector's status message
|
|
281
797
|
never reaches the page: it can quote a downstream error body, and that body can
|
|
282
798
|
quote the secret it rejected, so the details payload carries only a classified
|
|
283
|
-
`problem
|
|
284
|
-
|
|
285
|
-
same rule: those routes answer
|
|
286
|
-
answers only `{ ok }`), log
|
|
287
|
-
|
|
799
|
+
`problem`, which picks fixed on-screen copy and a fixed fix prompt. Server
|
|
800
|
+
logs contain only checked failure records, with no raw text. OAuth and
|
|
801
|
+
credential Test notices keep the same rule: those routes answer failures in
|
|
802
|
+
fixed words (a Test answers only `{ ok }`), log checked facts, and let the page
|
|
803
|
+
pick a sentence by outcome. Credential handoff URLs exist only while the UI is
|
|
288
804
|
mounted; OAuth callbacks never need it.
|
|
289
805
|
|
|
290
806
|
Its appearance is one token layer, and it is not the UI's alone: every page
|
|
291
807
|
connecta shows a person — the operator shell, the OAuth callback, a browser's
|
|
292
|
-
404
|
|
808
|
+
404 — reads it. `src/operator-ui/tokens.css`
|
|
293
809
|
resolves every color, radius, and font through a custom property and mixes the
|
|
294
810
|
rest from those with `color-mix`; `page.css` holds the base typography and the
|
|
295
811
|
primitives (shell, masthead, buttons, badges, messages, the one-message status
|
|
@@ -303,111 +819,49 @@ block after the stylesheet. The five tokens it accepts are gated in
|
|
|
303
819
|
`src/branding.ts`, each by a narrow syntactic check: deployment config reaches a
|
|
304
820
|
`<style>` element here, and an unvalidated value would be CSS injection. The
|
|
305
821
|
dark palette is the same tokens under `prefers-color-scheme`; `colorScheme` pins
|
|
306
|
-
one with a `data-scheme` attribute on the page
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
module
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
starts no background jobs. One tick scans at most 1,000 heads and starts at
|
|
327
|
-
most 10 due runs. Every run uses the configured executor, host-call budget,
|
|
328
|
-
and watchdog, with a registry view containing only shared connectors. The
|
|
329
|
-
owner admitted to `set_refresh` is stored with the program. Every run rechecks
|
|
330
|
-
that identity's current `connectorAccess` and pool grant, including continued
|
|
331
|
-
access to `artifacts.set_refresh`, then intersects those grants with shared
|
|
332
|
-
connectors. Revoking either grant stops later runs.
|
|
333
|
-
Approval exemptions are off, so a refresh writes nothing. A refused call fails the
|
|
334
|
-
entire refresh even if its program catches the error. The head CAS claims one
|
|
335
|
-
run per artifact; the data commit checks the claim ID and program version in
|
|
336
|
-
the same CAS. The claim deadline starts before executor admission and aborts
|
|
337
|
-
a queued or active play when it expires. An expired old run cannot publish over
|
|
338
|
-
a newer claim. Run history retains 50 records with bounded logs. A failure leaves the
|
|
339
|
-
last good data intact and marks the artifact stale. Library and viewer show
|
|
340
|
-
only outcome enums and times, never downstream messages or logs.
|
|
341
|
-
|
|
342
|
-
With the operator UI mounted, `/artifacts` lists the team pages and
|
|
343
|
-
`/artifacts/<id>` opens one. The shell contains no page data. It reads the
|
|
344
|
-
artifact through an authenticated JSON route with the operator UI's inbound
|
|
345
|
-
identity and connector visibility, then passes the document into a sandboxed
|
|
346
|
-
frame. Snapshot links pin the view and document versions. The frame's response
|
|
347
|
-
CSP gives scripts an opaque origin, refuses fetch, XHR, images and other
|
|
348
|
-
unlisted requests. External script, style, and font origins default to none;
|
|
349
|
-
a deployment opts in to each one it trusts with page data. The shell tightens
|
|
350
|
-
its own `frame-src` to `'none'` after the fixed bootstrap loads and before it
|
|
351
|
-
hands over the page,
|
|
352
|
-
so a page script cannot navigate even its own frame to send data in a URL.
|
|
353
|
-
This browser boundary is covered by Chromium tests, including a
|
|
354
|
-
`document.open/write` rewrite.
|
|
355
|
-
|
|
356
|
-
`artifactOrigin` optionally names a distinct HTTPS origin for these pages.
|
|
357
|
-
The main host redirects every `/artifacts` path to it; the artifact host
|
|
358
|
-
answers every other path, including `/mcp` and operator APIs, with 404. The
|
|
359
|
-
artifact shell still uses the same inbound auth providers and checks the
|
|
360
|
-
identity on each data read. The bare shell and fixed frame bootstrap carry no
|
|
361
|
-
artifact data, so a visitor can reach sign-in before an authenticated read.
|
|
362
|
-
Bearer tokens in browser localStorage belong to one origin: signing in on the
|
|
363
|
-
main host does not sign in on the artifact host. A redirected visitor enters
|
|
364
|
-
the token again there; the redirect carries no credential. Cookie or session
|
|
365
|
-
auth likewise depends on the provider admitting that separate origin.
|
|
366
|
-
|
|
367
|
-
Storage is an `ArtifactStore`, and `kvArtifactStore` is the reference: over any
|
|
368
|
-
`KVStorage` with `compareAndSet` and `list`, one head record per artifact is
|
|
369
|
-
the only key ever compared-and-set, earlier versions sit beside it immutable,
|
|
370
|
-
and bodies are content-addressed — in the key-value store, or in R2 through the
|
|
371
|
-
Worker example's `r2-artifact-blobs.ts`. Workers KV is refused at construction,
|
|
372
|
-
because a write that cannot swap its head can lose a teammate's edit. Every
|
|
373
|
-
rule lives once, in `src/artifacts/operations.ts`: a write checks its base
|
|
374
|
-
against the one stream it touches, so of two writes from the same base exactly
|
|
375
|
-
one wins and the other is a `conflict` carrying a bounded `current` map, while
|
|
376
|
-
a head that moved for an unrelated reason is re-read and retried rather than
|
|
377
|
-
reported. Patches match exactly once or change nothing, rollback is a new
|
|
378
|
-
version reusing an old body, and the limits are checked on every write.
|
|
379
|
-
Validation is a hand-written tokenizer and CommonMark subset rather than a
|
|
380
|
-
dependency, because it is a lint: the page's CSP is the boundary.
|
|
381
|
-
|
|
382
|
-
Who made a version is the one fact a built-in connector needs that
|
|
383
|
-
`ConnectorContext` deliberately does not carry. `src/connector-caller.ts`
|
|
384
|
-
attaches the admitted identity beside the context on the request's scoped
|
|
385
|
-
view, the way the OAuth sealer rides beside it — readable only by in-repo
|
|
386
|
-
code, set from the authorization and never from arguments
|
|
387
|
-
(`test/identity-scope.test.ts`, `test/artifacts-connector.test.ts`).
|
|
822
|
+
one with a `data-scheme` attribute on the page.
|
|
823
|
+
|
|
824
|
+
Maintained providers act as the caller downstream. `src/connector-caller.ts`
|
|
825
|
+
attaches the admitted identity beside the request-scoped context, readable only
|
|
826
|
+
by in-repo code and set from authorization, never arguments. Google Workspace
|
|
827
|
+
domain-wide delegation
|
|
828
|
+
(`src/providers/google/`, first consumed by `./providers/gmail`) hands the
|
|
829
|
+
admitted identity to a deployment-config `subject` function and mints a
|
|
830
|
+
service-account token as whatever Workspace address it returns. The caller
|
|
831
|
+
carries whether an inbound provider _authenticated_ it: an open deployment
|
|
832
|
+
admits everyone as the anonymous actor, and the function is never asked about
|
|
833
|
+
them. No caller, an unauthenticated one, or no address fails `auth_required`
|
|
834
|
+
before any request leaves. The provider never chooses the account and a
|
|
835
|
+
deployment's own connector still cannot read the caller (PRINCIPLES.md, INV-3; `test/google-workspace-delegation.test.ts`). Its tokens live in a
|
|
836
|
+
bounded module-level map, in memory only, keyed by the key's own digest as well
|
|
837
|
+
as the account, subject, and scopes. A mint in flight is shared across requests
|
|
838
|
+
the way OAuth flights are: the owner settles a plain outcome
|
|
839
|
+
inside its own request — a token, Google's refusal, or `abandoned` — and each
|
|
840
|
+
follower waits on a promise of its own under its own signal and timer to the
|
|
841
|
+
flight's deadline, never touching the owner's signal or response.
|
|
388
842
|
|
|
389
843
|
## Import-graph purity
|
|
390
844
|
|
|
391
845
|
Nothing reachable from `src/index.ts` may import a `node:` builtin, so the same
|
|
392
846
|
core runs unchanged in workerd and in Node. The Node-touching paths — `src/node.ts`
|
|
393
|
-
(the `node:http` adapter), `src/
|
|
847
|
+
(the `node:http` adapter), `src/sqlite.ts` (`node:sqlite`), and the QuickJS process pool
|
|
394
848
|
(`src/executors/quickjs.ts` plus its child) — each sit behind an explicit subpath
|
|
395
849
|
and must stay unreachable from the root. `./auth/clerk` is separate because
|
|
396
850
|
`@clerk/backend` is an optional peer rather than a dependency, and
|
|
397
851
|
`./auth/cloudflare-access` for a third reason: it is Web-API-pure, but its trust
|
|
398
852
|
contract is specific to a direct Worker invocation carrying `ctx.access`.
|
|
399
853
|
|
|
400
|
-
`test/purity.test.ts` walks the relative-import graph and fails on any `node:`
|
|
854
|
+
`test/purity.node.test.ts` walks the relative-import graph and fails on any `node:`
|
|
401
855
|
specifier in a reachable file, or on any of those modules — plus the UI bundle,
|
|
402
856
|
encrypted vault, and activity implementation — being reachable at all;
|
|
403
|
-
`test/package-surface.test.ts` and `scripts/check-package.mjs` guard the same
|
|
857
|
+
`test/package-surface.node.test.ts` and `scripts/check-package.mjs` guard the same
|
|
404
858
|
boundary in the published tarball. The failure mode is not theoretical: one
|
|
405
859
|
convenience import of `node:crypto` in a shared helper stops the whole Worker
|
|
406
860
|
shape from building, in someone else's repository rather than this one.
|
|
407
861
|
|
|
408
862
|
## Effect inside
|
|
409
863
|
|
|
410
|
-
The core runs on stable Effect v4
|
|
864
|
+
The core runs on stable Effect v4 through a compatible `^4.0.0` range; no API a deployment
|
|
411
865
|
touches does. `createConnecta`,
|
|
412
866
|
`remoteMcp()`, `api()`, the `Connector` contract, and every shipped `.d.ts`
|
|
413
867
|
are Promise-shaped and name no Effect type, so a connector author never meets a
|
|
@@ -441,7 +895,7 @@ and `npm run check:declarations` fails if what remains names an Effect type.
|
|
|
441
895
|
### One runner
|
|
442
896
|
|
|
443
897
|
`src/runtime/run.ts` is the only place a fiber starts, and
|
|
444
|
-
`test/purity.test.ts` fails if any other file calls `Effect.run*`, `runFork`,
|
|
898
|
+
`test/purity.node.test.ts` fails if any other file calls `Effect.run*`, `runFork`,
|
|
445
899
|
`forkDaemon`, or `ManagedRuntime.make`. It exports two ways out:
|
|
446
900
|
|
|
447
901
|
- **`runEdge(effect, { signal, runtime? })`** runs an effect at a Promise
|
|
@@ -456,39 +910,35 @@ and `npm run check:declarations` fails if what remains names an Effect type.
|
|
|
456
910
|
to `ctx.waitUntil` when there is one.
|
|
457
911
|
|
|
458
912
|
Beside them sit `withDeadlineEffect`, which aborts the operation's own signal
|
|
459
|
-
with the labelled timeout error
|
|
913
|
+
with the labelled timeout error _before_ interrupting it, so work that honors
|
|
460
914
|
the signal sees the same reason the caller does, and `fromSignal`, which turns
|
|
461
915
|
an `AbortSignal` into a failure to race against. Nothing runs Effect at module
|
|
462
916
|
scope, where a Worker may not start work, and nothing logs through
|
|
463
917
|
`Effect.log`: logging goes through the configured `Logger`, which is what
|
|
464
918
|
honors `logger: "silent"`.
|
|
465
919
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
deliberately do not run on it. The request pipeline runs on no runtime, so
|
|
472
|
-
`/health` and a closed deployment's 503 keep answering after `close()`. And a
|
|
473
|
-
registry provides its own storage and logger to its programs
|
|
920
|
+
There is no per-Connecta runtime. createConnecta resolves its configuration
|
|
921
|
+
into plain values once (`src/config.ts`), and nothing needs a service context
|
|
922
|
+
built from them. The request pipeline runs on no runtime, so `/health` and a
|
|
923
|
+
closed deployment's 503 keep answering after `close()`. A registry provides its
|
|
924
|
+
own `Storage` and `Logger` services (`src/runtime/services.ts`) to its programs
|
|
474
925
|
(`runOnPartition` in `src/runtime/storage.ts`), because a personal registry's
|
|
475
|
-
storage is the root's namespaced to its principal
|
|
476
|
-
would be another partition's.
|
|
926
|
+
storage is the root's namespaced to its principal.
|
|
477
927
|
|
|
478
928
|
### What runs on Effect
|
|
479
929
|
|
|
480
|
-
| Area
|
|
481
|
-
|
|
|
930
|
+
| Area | Shape |
|
|
931
|
+
| ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
482
932
|
| Requests (`src/server.ts`, `src/routes/mcp.ts`, `src/routes/oauth.ts`) | One fiber per `fetch`, tied to `request.signal`. An `/mcp` request's admission permit and every `McpServer` it builds live in a Scope the response carries out, closed when the body ends, fails, or is cancelled, when the signal aborts, when its admitted-request lifetime ends in that request, or at once if the handler fails first. If workerd runs none of those callbacks, the next request reclaims only the expired admission record; it cannot close another request's Scope. The OAuth callback is uninterruptible: a single-use code's exchange and catalog invalidation are one commitment. |
|
|
483
|
-
| Admission (`executor-admission.ts`, `call-admission.ts`)
|
|
484
|
-
| One tool call (`invocation.ts`)
|
|
485
|
-
| `execute_code` (`execute.ts`)
|
|
486
|
-
| Discovery (`catalog-service.ts`)
|
|
487
|
-
| Registry (`registry.ts`)
|
|
488
|
-
| Remote MCP (`connectors/remote-mcp.ts`)
|
|
489
|
-
| Downstream OAuth (`auth/downstream-oauth.ts`)
|
|
490
|
-
| Operator and activity data (`routes/operator.ts`)
|
|
491
|
-
| QuickJS pool (`executors/quickjs.ts`, Node only)
|
|
933
|
+
| Admission (`executor-admission.ts`, `call-admission.ts`) | Queued waiters are Deferreds settled by whoever removes them from the queue; a wait is one flat race of grant, Clock timeout, and signal. The uncontended path stays synchronous. Two controllers, as [#453](https://github.com/zackbart/connecta/issues/453) requires. |
|
|
934
|
+
| One tool call (`invocation.ts`) | One fiber: resolution, the read-only and schema refusals, admission, and the downstream attempt sit under a single `withDeadlineEffect`, whose expiry interrupts the call wherever it is. The permit is an `acquireRelease`; the connector call is `Effect.tryPromise` over the unchanged `Connector`. |
|
|
935
|
+
| `execute_code` (`execute.ts`) | One fiber whose Scope owns the run's signal and executor lease, so a result, a throw, the watchdog, and cancellation all release the lease and abort the signal the same way. The executor's `acquire()` and `execute()` stay Promises raced against the signal and `execute.watchdogMs`. Each guest host call is a fiber of its own. |
|
|
936
|
+
| Discovery (`catalog-service.ts`) | A request-scoped cache: one shared read per connector (`runtime/shared-read.ts`), settled by the read itself and carrying its own signal and the probe timeout whichever asker starts it. Each asker waits under its own deadline and signal, so one that times out or is cancelled fails alone, and the read is cancelled only once every asker has gone. Fan-out is `Effect.forEach` under the discovery concurrency. |
|
|
937
|
+
| Registry (`registry.ts`) | The result stash is a program over `Storage`. Same-request catalog loads share one read the way discovery's do. Remote MCP caching uses the SDK ResponseCacheStore adapter over SQL-backed KV, with no cross-request catalog flight. |
|
|
938
|
+
| Remote MCP (`connectors/remote-mcp.ts`) | Each request scope's state holds a Scope, each connection is a lease forked from it, and a connect in flight is a Deferred carrying the client it connected. Closing a session and the transport are each bounded to a second. |
|
|
939
|
+
| Downstream OAuth (`auth/downstream-oauth.ts`) | A refresh flight is a Deferred. Preparation follows caller cancellation; after dispatch, the HTTP exchange owns a 20-second deadline and the grant commit continues through the runtime deferred-work hook. |
|
|
940
|
+
| Operator and activity data (`routes/operator.ts`) | Each JSON route is one program run by `serveOperator` behind the Promise `handle()`. Reads run under the request's signal; writes do not, so a vault write or OAuth disconnect that started reaches its cache invalidation. |
|
|
941
|
+
| QuickJS pool (`executors/quickjs.ts`, Node only) | Each child is a scoped resource whose release sends SIGTERM, then SIGKILL after a second; crash respawn backoff is a `Schedule`. |
|
|
492
942
|
|
|
493
943
|
### What stays plain
|
|
494
944
|
|
|
@@ -505,7 +955,7 @@ would be another partition's.
|
|
|
505
955
|
wrapping them would buy nothing and could starve a request.
|
|
506
956
|
- **The MCP edges.** `@modelcontextprotocol/server` upward and the SDK client
|
|
507
957
|
downstream; Effect's own `McpServer` is not used.
|
|
508
|
-
- **The Node and browser leaves.** `node.ts`, `
|
|
958
|
+
- **The Node and browser leaves.** `node.ts`, `sqlite.ts`, the QuickJS
|
|
509
959
|
child, runtime, and protocol, and the operator UI's browser app.
|
|
510
960
|
|
|
511
961
|
### Across requests
|
|
@@ -541,19 +991,16 @@ when its only pending work is waiting on another request and it has no timer or
|
|
|
541
991
|
I/O of its own. Connecta's deadline timers are what keep a waiting request
|
|
542
992
|
alive and bounded, so no wait may lose its timer.
|
|
543
993
|
|
|
544
|
-
**
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
its
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
current, checked at the same points as the catalog generation, so a stuck
|
|
555
|
-
listing that lands late is never cached or persisted and never overwrites the
|
|
556
|
-
fresh attempt's.
|
|
994
|
+
**Catalog I/O belongs to its request.** Listings share work only within one
|
|
995
|
+
request and authorization partition. Independent requests may read the same
|
|
996
|
+
complete SQL cache entry, but never await another request's listing, retain
|
|
997
|
+
its client, or inherit its sent-secret set. A rotated cache generation makes
|
|
998
|
+
late publication from an invalidated request unreachable. Missing chunks,
|
|
999
|
+
expired entries, storage errors, or a failed page trigger a fresh complete
|
|
1000
|
+
listing. A transient first-page tool-listing failure may use a complete catalog
|
|
1001
|
+
within five minutes of expiry in the same auth partition. Partial walks and
|
|
1002
|
+
auth or permission failures refuse fallback; resources require unexpired
|
|
1003
|
+
inventories, and stale fallback cannot authorize post-entry auth recovery.
|
|
557
1004
|
|
|
558
1005
|
### Why not Schema or HttpApi
|
|
559
1006
|
|
|
@@ -563,16 +1010,18 @@ of Worker cold start for it, and that cost was accepted over deep imports at
|
|
|
563
1010
|
every call site. The catch is that a module the barrel reaches is paid for in
|
|
564
1011
|
full, however little of it is used. Effect Schema for config validation and the
|
|
565
1012
|
meta-tool inputs came to about +62 KB gzip and +13 ms of cold start in the
|
|
566
|
-
root, for a validator no clearer than the hand-written `
|
|
1013
|
+
root, for a validator no clearer than the hand-written schema in `src/config.ts`; the
|
|
567
1014
|
meta-tool inputs stay zod, which the MCP SDK bundles regardless. `HttpApi` for
|
|
568
1015
|
the operator and activity routes cost about +100 KB gzip on `./ui` and +130 KB
|
|
569
1016
|
on `./activity`, and matching the wire format meant opting out of most of what
|
|
570
1017
|
it does: unowned paths fall through rather than 404, a wrong method is a JSON
|
|
571
1018
|
405, and the content-type and size checks run before a body is read. The root
|
|
572
|
-
entry may import only `effect` itself (`test/purity.test.ts`). Effect v4's
|
|
1019
|
+
entry may import only `effect` itself (`test/purity.node.test.ts`). Effect v4's
|
|
573
1020
|
area imports, such as `effect/http-api` and `effect/ai`, would have to stay
|
|
574
1021
|
behind a subpath. APIs tagged `@stability unstable` can still change in minor
|
|
575
|
-
releases,
|
|
1022
|
+
releases, which is why the root may import only `effect` itself: that is what
|
|
1023
|
+
makes a caret range safe, and it lets a deployment that also uses Effect
|
|
1024
|
+
resolve a single copy. Adopting an unstable subpath is its own pull request.
|
|
576
1025
|
|
|
577
1026
|
The stable `4.0.0` release was re-evaluated on 2026-10-01 with
|
|
578
1027
|
[`scripts/probes/effect-v4-evaluation.mjs`](https://github.com/zackbart/connecta/blob/main/scripts/probes/effect-v4-evaluation.mjs).
|
|
@@ -627,32 +1076,48 @@ src/
|
|
|
627
1076
|
server.ts route ordering, HTTPS upgrade, security wrapper
|
|
628
1077
|
routes/ one file per surface; shared.ts holds the auth gate
|
|
629
1078
|
skills.ts MCP instructions, the usage skill, connector guides
|
|
630
|
-
catalog-drift.ts
|
|
1079
|
+
catalog-drift.ts reviewed classifications and the drift counts a refresh produces
|
|
1080
|
+
provider.ts defineProvider(): the one maintained-provider shape
|
|
631
1081
|
activity.ts optional history factory and best-effort recorder
|
|
632
1082
|
auth/ bearer, Cloudflare Access, clerk (optional peer), downstream OAuth (remote and static)
|
|
633
1083
|
executors/ the QuickJS pool and child (Node only)
|
|
634
|
-
node.ts listen()
|
|
1084
|
+
node.ts listen() (Node only)
|
|
1085
|
+
d1.ts, sqlite.ts the two storage drivers; storage/ holds the SQL core and key families
|
|
635
1086
|
```
|
|
636
1087
|
|
|
637
1088
|
There are exactly two deployment shapes — `templates/node/`, which
|
|
638
1089
|
`connecta init` copies with its container files, and `examples/worker/` — and
|
|
639
|
-
`test/deployment-shapes.test.ts` with `npm run check:examples` keeps both
|
|
1090
|
+
`test/deployment-shapes.node.test.ts` with `npm run check:examples` keeps both
|
|
640
1091
|
compiling and configuring the real thing.
|
|
641
1092
|
|
|
642
1093
|
## Sharp edges
|
|
643
1094
|
|
|
644
|
-
- **The root registry is shared; identity views are partitioned.**
|
|
645
|
-
connector
|
|
646
|
-
|
|
647
|
-
authenticated subject. Putting a downstream client or credential on the wrong
|
|
1095
|
+
- **The root registry is shared; identity views are partitioned.** Public remote catalogs are shared only after intake redaction, keyed by the
|
|
1096
|
+
connector configuration. Private entries include the admitted principal and
|
|
1097
|
+
pool even for shared-auth connectors. Personal connectors use a bounded
|
|
1098
|
+
principal registry and transient results the authenticated subject. Putting a downstream client or credential on the wrong
|
|
648
1099
|
side of those lines is the highest-severity mistake available here.
|
|
649
1100
|
- **Route order is behavior.** Moving a mutation route below the wildcard
|
|
650
1101
|
`OPTIONS` opts it into CORS preflight; reordering admission after auth makes
|
|
651
1102
|
the cheapest possible attack the most expensive request.
|
|
652
1103
|
- **`close()` is idempotent and ordered.** Both admission pools, then the
|
|
653
|
-
connector limiters, then the executor
|
|
1104
|
+
connector limiters, then the executor; Node's `listen()` calls it on
|
|
654
1105
|
SIGTERM/SIGINT.
|
|
655
1106
|
- **Structural mistakes throw at construction.** A duplicate connector id, an
|
|
656
|
-
invalid admission rule,
|
|
1107
|
+
invalid admission rule or limit, an unknown option at any depth, the old
|
|
1108
|
+
boolean `accessTokens` option, a missing executor, or an executor without a
|
|
1109
|
+
lifecycle brand:
|
|
657
1110
|
all refuse to boot (`test/config.test.ts`, `test/registry.test.ts`). Starting
|
|
658
1111
|
in the wrong shape is worse than not starting.
|
|
1112
|
+
Shipped `/worker` and `/quickjs` executors carry a non-enumerable global-symbol
|
|
1113
|
+
brand that survives package duplication and bundling. Custom sandboxes opt in
|
|
1114
|
+
through `customExecutor(executor, { lifecycle: "self-managed" })` from the root
|
|
1115
|
+
entry and own their termination and cleanup. Plain upstream executors need
|
|
1116
|
+
the `/worker` adapter, regardless of constructor name.
|
|
1117
|
+
|
|
1118
|
+
## Operator data contract
|
|
1119
|
+
|
|
1120
|
+
The contract now lives in [Operator UI](./operator-ui.md#operator-data-contract),
|
|
1121
|
+
including configuration provenance, live facts, activity disclosure, doctor,
|
|
1122
|
+
and the browser implementation. [Optional deployment modules](#optional-deployment-modules)
|
|
1123
|
+
keeps the module composition and request-lifetime rules here.
|