@zackbart/connecta 0.38.0 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +91 -0
- package/README.md +3 -2
- package/bin/connecta.mjs +4 -8
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.js +6 -4
- package/dist/agent-routes.js +231 -0
- package/dist/auth/downstream-oauth/consent.js +519 -0
- package/dist/auth/downstream-oauth/grant-store.js +439 -0
- package/dist/auth/downstream-oauth/refresh-coordinator.js +562 -0
- package/dist/auth/downstream-oauth/refresh-records.js +447 -0
- package/dist/auth/downstream-oauth/sdk-hooks.js +242 -0
- package/dist/auth/downstream-oauth.js +66 -2382
- package/dist/auth/native.js +37 -0
- package/dist/auth/oauth-grant.js +13 -0
- package/dist/auth/static-oauth.js +153 -131
- package/dist/auth-elicitation.js +7 -1
- package/dist/authorization-handoff.js +11 -5
- package/dist/catalog-cache.js +219 -59
- package/dist/catalog-policy.js +18 -0
- package/dist/catalog-service.js +284 -47
- package/dist/catalog.js +18 -2
- package/dist/config.d.ts +16 -11
- package/dist/config.js +9 -6
- package/dist/connector-caller.d.ts +1 -1
- package/dist/connector-health.js +59 -0
- package/dist/connector-status.js +117 -0
- package/dist/connectors/api-connector.d.ts +7 -2
- package/dist/connectors/api-connector.js +7 -4
- package/dist/connectors/negotiation-cache.js +22 -4
- package/dist/connectors/remote-mcp/call.js +164 -0
- package/dist/connectors/remote-mcp/catalog.js +278 -0
- package/dist/connectors/remote-mcp/config.js +263 -0
- package/dist/connectors/remote-mcp/connection.js +682 -0
- package/dist/connectors/remote-mcp/errors.js +488 -0
- package/dist/connectors/remote-mcp/oauth.js +473 -0
- package/dist/connectors/remote-mcp/resources.js +473 -0
- package/dist/connectors/remote-mcp/runtime.js +1 -0
- package/dist/connectors/remote-mcp/transport.js +555 -0
- package/dist/connectors/remote-mcp.js +62 -2863
- package/dist/credential-contract.d.ts +23 -0
- package/dist/credential-reads.js +17 -0
- package/dist/credential-verification.js +53 -0
- package/dist/credentials.d.ts +7 -0
- package/dist/credentials.js +60 -25
- package/dist/describe-config.d.ts +12 -2
- package/dist/describe-config.js +2 -0
- package/dist/discovery-health.js +199 -0
- package/dist/downstream-input.js +2 -0
- package/dist/errors.d.ts +55 -17
- package/dist/errors.js +95 -41
- package/dist/execute.js +152 -114
- package/dist/executor-result.js +3 -1
- package/dist/guest-functions.js +83 -0
- package/dist/guest-runtime.js +60 -19
- package/dist/guest-types.d.ts +19 -5
- package/dist/http/argument-problems.js +35 -0
- package/dist/http/errors.js +5 -4
- package/dist/http/fields.js +192 -13
- package/dist/http/hatch-response.js +73 -0
- package/dist/http/hatch.js +88 -23
- package/dist/http/oauth.js +3 -2
- package/dist/http/provider.js +32 -4
- package/dist/http/safety.js +8 -0
- package/dist/http/tools.js +44 -23
- package/dist/index.d.ts +1 -1
- package/dist/index.js +9 -2
- package/dist/invocation.js +26 -8
- package/dist/membership.d.ts +13 -5
- package/dist/membership.js +34 -2
- package/dist/meta-tools.js +54 -31
- package/dist/native-recovery.js +4 -16
- package/dist/native-tools.js +5 -2
- package/dist/oauth-sealing.js +10 -0
- package/dist/operation-vocabulary.js +135 -13
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +1 -0
- package/dist/operator-ui/contract.d.ts +10 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/model.d.ts +22 -1
- package/dist/provider-call-facts.js +6 -2
- package/dist/providers/_shared/rest/operation-index.js +48 -50
- package/dist/providers/_shared/rest/value-safety.js +80 -0
- package/dist/providers/breeze/api.js +7 -1
- package/dist/providers/breeze/skill.generated.js +1 -1
- package/dist/providers/breeze/tools.js +4 -3
- package/dist/providers/ccb/api.js +2 -1
- package/dist/providers/ccb/index.js +5 -0
- package/dist/providers/ccb/skill.generated.js +1 -1
- package/dist/providers/cloudflare/api.js +8 -1
- package/dist/providers/cloudflare/skill.generated.js +1 -1
- package/dist/providers/docs/lib/schemas.js +24 -22
- package/dist/providers/docs/safety.js +2 -0
- package/dist/providers/docs/skill.generated.js +1 -1
- package/dist/providers/docs/tools.js +2 -4
- package/dist/providers/drive/api.js +1 -0
- package/dist/providers/drive/lib/contracts.js +8 -5
- package/dist/providers/drive/skill.generated.js +1 -1
- package/dist/providers/drive/tools.js +11 -16
- package/dist/providers/forms/api.js +1 -0
- package/dist/providers/forms/skill.generated.js +1 -1
- package/dist/providers/forms/tools.js +10 -7
- package/dist/providers/github/api.js +1 -0
- package/dist/providers/github/lib/discovery.js +1 -1
- package/dist/providers/github/skill.generated.js +1 -1
- package/dist/providers/github/tools.js +61 -17
- package/dist/providers/gmail/lib/schemas.js +60 -50
- package/dist/providers/gmail/skill.generated.js +1 -1
- package/dist/providers/gmail/tools.js +7 -5
- package/dist/providers/granola/index.js +2 -0
- package/dist/providers/granola/skill.generated.js +1 -1
- package/dist/providers/overflow/api.js +7 -1
- package/dist/providers/overflow/skill.generated.js +1 -1
- package/dist/providers/planning-center/api.js +3 -1
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/planning-center/skill.generated.js +1 -1
- package/dist/providers/quickbooks/safety.js +80 -39
- package/dist/providers/quickbooks/skill.generated.js +1 -1
- package/dist/providers/sheets/api.js +1 -0
- package/dist/providers/sheets/skill.generated.js +1 -1
- package/dist/providers/sheets/tools.js +13 -14
- package/dist/providers/slides/skill.generated.js +1 -1
- package/dist/providers/slides/tools.js +15 -13
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/tithely/api.js +3 -1
- package/dist/providers/tithely/skill.generated.js +1 -1
- package/dist/providers/vercel/api.js +1 -1
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/providers/vercel/tools.js +4 -3
- package/dist/registry.d.ts +10 -4
- package/dist/registry.js +41 -13
- package/dist/routes/credentials.js +129 -60
- package/dist/routes/dispatch.js +5 -1
- package/dist/routes/mcp.js +30 -14
- package/dist/routes/operator.js +2 -2
- package/dist/routes/shared.d.ts +6 -1
- package/dist/routes/shared.js +7 -1
- package/dist/routes/ui-config.js +2 -2
- package/dist/routes/ui-data.js +126 -20
- package/dist/routes/ui.js +16 -0
- package/dist/runtime/call.js +7 -2
- package/dist/runtime/native-storage.js +12 -0
- package/dist/runtime/request-reads.js +207 -0
- package/dist/runtime/request-scope.js +16 -0
- package/dist/runtime/storage.js +12 -0
- package/dist/skills.js +37 -20
- package/dist/storage/keys.js +53 -3
- package/dist/storage/memory.js +8 -2
- package/dist/storage/sql-native.js +53 -1
- package/dist/storage/sql.js +12 -2
- package/dist/storage-digest.js +32 -0
- package/dist/timeout.js +8 -0
- package/dist/types.d.ts +33 -0
- package/dist/usage-guide.js +16 -35
- package/dist/validate.d.ts +3 -0
- package/dist/validate.js +220 -158
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/write-outcome.js +0 -3
- package/documentation/README.md +13 -13
- package/documentation/architecture.md +385 -20
- package/documentation/auth.md +233 -64
- package/documentation/code-mode.md +122 -39
- package/documentation/deploying.md +6 -3
- package/documentation/integrating.md +16 -2
- package/documentation/meta-tools.md +240 -49
- package/documentation/migration-0.33.md +5 -1
- package/documentation/migration-0.34.md +3 -1
- package/documentation/operator-tests.md +1 -1
- package/documentation/operator-ui.md +105 -12
- package/documentation/pool-membership-upgrade.md +44 -0
- package/documentation/providers.md +105 -0
- package/examples/worker/AGENTS.md +10 -10
- package/examples/worker/README.md +4 -3
- package/examples/worker/src/connecta.config.ts +1 -0
- package/guides/connecting-agents.md +2 -1
- package/guides/how-it-works.md +4 -2
- package/package.json +1 -1
- package/templates/node/AGENTS.md +13 -12
- package/templates/node/package.json +1 -1
- package/templates/node/src/connecta.config.ts +1 -0
package/documentation/auth.md
CHANGED
|
@@ -13,7 +13,7 @@ credential contracts; use [deployment setup](./deploying.md) for runnable steps.
|
|
|
13
13
|
| Lay out an agent's pools | [Organizing an agent's pools](#organizing-an-agents-pools) |
|
|
14
14
|
| Set up human sign-in | [Access on Workers](#cloudflare-access-on-workers), [Clerk tokens and sessions](#clerk-oauth-tokens-and-operator-sessions), [Clerk Organizations](#one-clerk-instance-one-organization-per-deployment), [management](#human-authentication-management) |
|
|
15
15
|
| Enable credential or connection controls | [Management permissions](#management-permissions), [start/restart/disconnect](#starting-restarting-and-disconnecting) |
|
|
16
|
-
| Diagnose consent or token refresh | [State at rest](#downstream-oauth-state-at-rest), [callbacks](#consents-and-callbacks), [refresh failures](#refresh-failures), [safe errors](#what-a-servers-errors-may-say)
|
|
16
|
+
| Diagnose consent or token refresh | [State at rest](#downstream-oauth-state-at-rest), [callbacks](#consents-and-callbacks), [refreshing before expiry](#refreshing-before-expiry), [refresh failures](#refresh-failures), [safe errors](#what-a-servers-errors-may-say) |
|
|
17
17
|
| Configure a downstream OAuth client or HTTP handler | [Client registration](#downstream-oauth-client-registration), [api() OAuth](#downstream-oauth-on-api), [request-local bearer tokens](#request-local-downstream-bearer-tokens), [principal forwarding](#forwarding-the-admitted-principal), [Google hosted MCP](#google-service-account-tokens-and-hosted-mcp) |
|
|
18
18
|
| Review browser or downstream URL boundaries | [Origins](#origins), [advertised URLs](#urls-a-downstream-advertises), [one server per grant](#one-server-per-grant) |
|
|
19
19
|
|
|
@@ -44,7 +44,9 @@ auth through the operator UI.
|
|
|
44
44
|
with several (Clerk's Frontend API URL or `cloudflare-access` as namespace).
|
|
45
45
|
Keep these stable: personal credentials are partitioned by principal.
|
|
46
46
|
4. Typecheck and restart/redeploy. Sign in as the person and verify their
|
|
47
|
-
intended endpoint and tools. A
|
|
47
|
+
intended endpoint and tools. A non-maintainer listed in exactly one pool
|
|
48
|
+
may use plain `/mcp`, which serves that pool; with zero or several pools,
|
|
49
|
+
plain `/mcp` answers the private 404 and they use `/mcp/<pool>`.
|
|
48
50
|
With the UI enabled, the [member page](./operator-ui.md#use-the-member-page)
|
|
49
51
|
shows their pool URLs and client setup; `/ui/api/config` returns 200 with
|
|
50
52
|
`role: "member"`. It does not grant the maintainer pages, shared credential
|
|
@@ -66,9 +68,11 @@ In the Node template and Worker example, `CONNECTA_MAINTAINERS` is a
|
|
|
66
68
|
comma-separated list of these ids; update that environment variable when
|
|
67
69
|
using their roster wiring. Pool membership remains code. To remove a person,
|
|
68
70
|
remove their memberships and maintainer entry, restart/redeploy, and remove
|
|
69
|
-
their sign-in admission as appropriate. Never add
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
their sign-in admission as appropriate. Never add a person to an agent's
|
|
72
|
+
pool: a pool lists only people or only agents, and mixing them throws at
|
|
73
|
+
construction. Give people who share an agent tier's reach their own people-only
|
|
74
|
+
pool that reuses the tier's tool-list constant, and keep each `<agent>-<user>`
|
|
75
|
+
pool agent-only; follow [the pool layout](#organizing-an-agents-pools).
|
|
72
76
|
|
|
73
77
|
## Connect a shared credential
|
|
74
78
|
|
|
@@ -119,6 +123,62 @@ recovery. These changes do not change
|
|
|
119
123
|
pool membership. Environment-backed header auth has no editable vault slot:
|
|
120
124
|
rotate that secret in the deployment's secret store and restart/redeploy.
|
|
121
125
|
|
|
126
|
+
### Test then save, and last verification
|
|
127
|
+
|
|
128
|
+
When the connector can test its credential (`testCredential` or
|
|
129
|
+
`testCredentials`, which every credential `remoteMcp()` and kit provider
|
|
130
|
+
has), the Connection tab's **Test and save** tests the entered value before
|
|
131
|
+
storing it. The value is tested in memory, under `discovery.probeTimeoutMs`,
|
|
132
|
+
and stored only if the test passes; a failure stores nothing. If the test
|
|
133
|
+
fails, **Save without testing** stores the value anyway, marked unverified,
|
|
134
|
+
so an outage at the service never blocks a replacement.
|
|
135
|
+
|
|
136
|
+
`PUT /ui/credentials/<id>` takes `{ "value": "…" }` (or `{ "values": {…} }`
|
|
137
|
+
for a multi-field slot) and an optional `"verify"`:
|
|
138
|
+
|
|
139
|
+
- `"test"`: test the candidate, then store it. A failure answers 200
|
|
140
|
+
`{ "saved": false, "failure": "<class>" }` and stores nothing.
|
|
141
|
+
- `"skip"` (the default when `verify` is absent, which keeps earlier API
|
|
142
|
+
callers working): store it unverified.
|
|
143
|
+
|
|
144
|
+
A stored save answers 200 `{ "saved": true, "credential": <metadata>,
|
|
145
|
+
"verification": { "outcome": "ok" | "unverified", "observedAt": "…" } }`.
|
|
146
|
+
`POST /ui/credentials/<id>/test` tests the stored value and answers
|
|
147
|
+
`{ "ok": true }` or `{ "ok": false, "failure": "<class>" }`. A connector
|
|
148
|
+
without a test hook refuses `verify: "test"` with 400, as its test route
|
|
149
|
+
refuses to test.
|
|
150
|
+
|
|
151
|
+
Failure classes are typed facts, never downstream text:
|
|
152
|
+
|
|
153
|
+
| Class | Meaning |
|
|
154
|
+
| ------------- | ------------------------------------------------------------------ |
|
|
155
|
+
| `rejected` | The service refused the credential. |
|
|
156
|
+
| `scope` | It accepted the credential but refused a needed permission. |
|
|
157
|
+
| `unreachable` | The service could not be reached, was unavailable or rate limited. |
|
|
158
|
+
| `timeout` | No answer within the probe deadline. |
|
|
159
|
+
| `failed` | Anything else. |
|
|
160
|
+
|
|
161
|
+
A custom hook may return `{ ok: false, failure: "scope" }` to name its class;
|
|
162
|
+
an unclassified failure is `failed`, and a thrown error is classified from its
|
|
163
|
+
`ConnectorCallError` code. Neither the response nor the log carries the value,
|
|
164
|
+
the hook's `message` or the downstream's reply.
|
|
165
|
+
|
|
166
|
+
Each test of the stored value and each save records a last-verified
|
|
167
|
+
observation for that connector and owner: its outcome class, its time and the
|
|
168
|
+
revision of the save it observed. A Test reads the stored values and their
|
|
169
|
+
revision from one envelope, and a save records the revision its own write
|
|
170
|
+
sealed, so a concurrent replacement is never reported as verified. The
|
|
171
|
+
operator page's connector data reports the record as
|
|
172
|
+
`verification: { outcome, observedAt }` only while that save is the stored
|
|
173
|
+
one, so a replacement reads as unverified until tested. Removing the
|
|
174
|
+
credential removes the record. A custom vault records verification only when
|
|
175
|
+
its `metadata` carries a per-save `revision` and, for Test, it implements
|
|
176
|
+
`snapshot`; otherwise its credentials are tested but never shown verified.
|
|
177
|
+
|
|
178
|
+
Reading the record is side-effect free: the operator page only reads it,
|
|
179
|
+
never probes the downstream or writes (INV-10). OAuth connectors keep no
|
|
180
|
+
verification record; their status is unchanged.
|
|
181
|
+
|
|
122
182
|
## Issue, rotate, or revoke an agent token
|
|
123
183
|
|
|
124
184
|
Each declared agent has exactly one token slot. Keep `accessTokens(storage)`
|
|
@@ -591,6 +651,11 @@ agent's slot is `issued` naming exactly that record. Nothing else counts: a
|
|
|
591
651
|
lookup or record the slot does not name is inert garbage. Every mutation
|
|
592
652
|
commits by one `compareAndSet` on the slot and only moves forward; no security
|
|
593
653
|
outcome depends on cleanup or on undoing a write.
|
|
654
|
+
Admission reads this state afresh on every request; successful token verdicts
|
|
655
|
+
are never cached. D1 and SQLite read the lookup, record, and slot in one
|
|
656
|
+
statement, with the same validation and expiry checks. Custom storage adapters
|
|
657
|
+
retain the sequential reads. Rotation and revocation therefore retain their
|
|
658
|
+
next-request refusal bound.
|
|
594
659
|
|
|
595
660
|
- **Rotate** writes the new record and its lookup (inert), checks the
|
|
596
661
|
request's signal, then compare-and-sets the slot from what it read to
|
|
@@ -727,8 +792,17 @@ bypasses the scoped registry. These refusals happen before any request leaves. A
|
|
|
727
792
|
never reach it; a deployment's own connectors still cannot read the caller.
|
|
728
793
|
|
|
729
794
|
Access is read from one place, deployment code. Top-level
|
|
730
|
-
`maintainers` lists the people who
|
|
731
|
-
|
|
795
|
+
`maintainers` lists the people who receive the whole catalog at plain `/mcp`
|
|
796
|
+
with the deployment's `readOnly`, and who reach the operator routes. A
|
|
797
|
+
non-maintainer person listed in exactly one pool receives that pool at plain
|
|
798
|
+
`/mcp` exactly as `/mcp/<pool>` serves it: its `tools`, `readOnly`, `surface`,
|
|
799
|
+
`executeCode` and `description`. A maintainer keeps the whole catalog even when
|
|
800
|
+
also listed in pools. Membership is matched on the admitted, resolved
|
|
801
|
+
`{ namespace, id }`, so a bare `"bob"` and the explicit reference it resolves
|
|
802
|
+
to are the same person, and the same id in another namespace is not. Authentication, OAuth challenges and resource audiences are plain `/mcp`'s;
|
|
803
|
+
the pool is chosen after authentication. Construction warns, naming each
|
|
804
|
+
non-maintainer person listed in several pools, and `describeConfig()` lists them
|
|
805
|
+
in `peopleInSeveralPools`. Each pool's
|
|
732
806
|
`members` lists who reaches that pool, people at `/mcp/<pool>` and agents at
|
|
733
807
|
`/agent/<pool>`, which serve exactly that pool's `tools`. A maintainer opens no pool implicitly; list them as a member to use
|
|
734
808
|
one. Nothing intersects these lists with anything else, and a request never
|
|
@@ -776,6 +850,12 @@ replacement. A guarded read grant becomes a pool with `readOnly: true`.
|
|
|
776
850
|
|
|
777
851
|
### Team pools
|
|
778
852
|
|
|
853
|
+
Every person in exactly one pool can configure plain `/mcp`; their sign-in
|
|
854
|
+
selects that pool, so one client configuration serves every team. A person in
|
|
855
|
+
several pools uses each pool's `/mcp/<pool>`. Only people pools count: a pool
|
|
856
|
+
lists only people or only agents, so an agent's pools never change where a
|
|
857
|
+
person's plain `/mcp` routes.
|
|
858
|
+
|
|
779
859
|
Keep membership in deployment code; static lists can be built with ordinary
|
|
780
860
|
code at construction:
|
|
781
861
|
|
|
@@ -800,9 +880,9 @@ Marketing's every `campaigns` tool and only `issues.search`. Neither pool
|
|
|
800
880
|
serves `billing`; only the owner's plain `/mcp` does. A person in both pools
|
|
801
881
|
uses two endpoints, never a union.
|
|
802
882
|
|
|
803
|
-
A team pool
|
|
804
|
-
|
|
805
|
-
([
|
|
883
|
+
A human team pool and an agent tier are separate pools. Share a tool-list
|
|
884
|
+
constant between them, with people in `team` and only agent references in
|
|
885
|
+
`assistant-team` ([pool layout](#organizing-an-agents-pools)).
|
|
806
886
|
|
|
807
887
|
| Catalog change | Exact `connector.tool` in a pool |
|
|
808
888
|
| ----------------------- | ----------------------------------------------------------------------- |
|
|
@@ -891,16 +971,16 @@ warning says, every pool path answers the 404 because nobody can be a member,
|
|
|
891
971
|
and every agent endpoint answers the machine-only 401 because no agent can be
|
|
892
972
|
admitted.
|
|
893
973
|
|
|
894
|
-
| Rule | Contract
|
|
895
|
-
| ------------ |
|
|
896
|
-
| Scope | `/mcp/<pool>` and `/agent/<pool>` serve exactly the pool's `tools` (connector ids or exact `connector.tool` addresses), with the pool's `readOnly`, to its member people and member agents respectively. Plain `/mcp` serves the whole catalog to maintainers. Neither intersects with or widens anything.
|
|
897
|
-
| Refusal | A
|
|
898
|
-
| Agents | An agent member reaches its pools only at `/agent/<pool>`, with its `cta_` token. Human endpoints refuse `cta_`; see [agent endpoints](#agent-endpoints).
|
|
899
|
-
| Surface | From 0.35.0, optional `surface`: `"meta"` (default) serves the six meta-tools; `"native"` lists each of the pool's downstream tools directly, for clients with their own tool search, with optional `executeCode: false` to replace `execute_code` with `read_result` (invalid on meta pools). Calls take the same paths either way. See the [native tool surface](./meta-tools.md#native-tool-surface).
|
|
900
|
-
| Description | Optional `description`: plain text telling the model what this endpoint is for. `/mcp/<pool>` and `/agent/<pool>` append it to the MCP `initialize` instructions after `This endpoint: `; plain `/mcp` and a pool without one serve the shared instructions unchanged. It grants, hides, and refuses nothing, and changes no tool or tool description.
|
|
901
|
-
| Construction | Malformed names, unknown options or connectors, empty pools, empty or missing `members`, unresolvable member references, a non-boolean `readOnly`, an `owner` that is not a Workspace email address, a `description` that is not 1 to 500 characters of one-line text after trimming, a retired `trust` or `grant`, and addresses absent from an `api()` static catalog throw. Remote catalogs are checked at lazy load. |
|
|
902
|
-
| Discovery | Clerk's pool 401 names `/.well-known/oauth-protected-resource/mcp/<pool>` with the pool URL as `resource`, matching RFC 9728. Cloudflare Managed OAuth is application-level and needs no pool setup.
|
|
903
|
-
| Drift | No wildcards; pool tools are exact names. Missing live tools are unreachable and warned once while in a bounded 1,024-entry FIFO.
|
|
974
|
+
| Rule | Contract |
|
|
975
|
+
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
976
|
+
| Scope | `/mcp/<pool>` and `/agent/<pool>` serve exactly the pool's `tools` (connector ids or exact `connector.tool` addresses), with the pool's `readOnly`, to its member people and member agents respectively. Plain `/mcp` serves the whole catalog to maintainers and the unique member pool to other people. Neither intersects with or widens anything. |
|
|
977
|
+
| Refusal | A person with zero or several memberships at `/mcp`, a non-member, and an undeclared pool get an identical 404 status, body, and headers, after auth. The operator log records `not_maintainer` (zero memberships), `ambiguous_pool` (several memberships), `not_pool_member`, or `pool_not_declared`. Names are not anonymously enumerable; valid credentials cannot distinguish these cases by content. |
|
|
978
|
+
| Agents | An agent member reaches its pools only at `/agent/<pool>`, with its `cta_` token. Human endpoints refuse `cta_`; see [agent endpoints](#agent-endpoints). |
|
|
979
|
+
| Surface | From 0.35.0, optional `surface`: `"meta"` (default) serves the six meta-tools; `"native"` lists each of the pool's downstream tools directly, for clients with their own tool search, with optional `executeCode: false` to replace `execute_code` with `read_result` (invalid on meta pools). Calls take the same paths either way. See the [native tool surface](./meta-tools.md#native-tool-surface). |
|
|
980
|
+
| Description | Optional `description`: plain text telling the model what this endpoint is for. `/mcp/<pool>` and `/agent/<pool>` append it to the MCP `initialize` instructions after `This endpoint: `; a maintainer at plain `/mcp` and a pool without one serve the shared instructions unchanged. A routed person receives their pool description. It grants, hides, and refuses nothing, and changes no tool or tool description. |
|
|
981
|
+
| Construction | Malformed names, unknown options or connectors, empty pools, empty or missing `members`, mixed people and agent membership (a `ConfigError` naming the pool), unresolvable member references, a non-boolean `readOnly`, an `owner` that is not a Workspace email address, a `description` that is not 1 to 500 characters of one-line text after trimming, a retired `trust` or `grant`, and addresses absent from an `api()` static catalog throw. Remote catalogs are checked at lazy load. |
|
|
982
|
+
| Discovery | Clerk's pool 401 names `/.well-known/oauth-protected-resource/mcp/<pool>` with the pool URL as `resource`, matching RFC 9728. Cloudflare Managed OAuth is application-level and needs no pool setup. |
|
|
983
|
+
| Drift | No wildcards; pool tools are exact names. Missing live tools are unreachable and warned once while in a bounded 1,024-entry FIFO. |
|
|
904
984
|
|
|
905
985
|
Pool descriptions are trimmed before validation. The remaining text must be
|
|
906
986
|
1 to 500 characters and contain no C0 or C1 control characters (including tabs,
|
|
@@ -947,7 +1027,12 @@ Protected-resource metadata stays public at both default aliases,
|
|
|
947
1027
|
Discovery does not authenticate callers or evaluate membership, so fresh
|
|
948
1028
|
OAuth clients can discover how to authenticate. Admission applies after auth
|
|
949
1029
|
to MCP requests and result/page handles. Handles remain bound to their
|
|
950
|
-
originating
|
|
1030
|
+
originating requested pathname and admitted pool; paging re-admits the caller
|
|
1031
|
+
at that endpoint and re-resolves their pool, which must still match. A handle
|
|
1032
|
+
minted at plain `/mcp` never pages at `/mcp/<pool>` or the reverse, even for the
|
|
1033
|
+
same pool, and a membership change fails it closed. Handles minted before this
|
|
1034
|
+
binding existed fail closed. Auth-elicitation and downstream-input
|
|
1035
|
+
continuations carry the same pathname and pool binding. A pool
|
|
951
1036
|
cannot read a `/mcp` handle, and `/mcp` cannot read a pool's handle.
|
|
952
1037
|
|
|
953
1038
|
Operator catalog (`/ui/data`, `/ui/connectors/<id>`), activity, shared
|
|
@@ -964,9 +1049,10 @@ activity. Anyone else gets the same 404, and an agent token the machine-only
|
|
|
964
1049
|
shell carries no catalog data.
|
|
965
1050
|
|
|
966
1051
|
Each endpoint is writable or read-only. The deployment's `readOnly` sets the
|
|
967
|
-
plain `/mcp` endpoint and is required: a deployment that omits it fails at
|
|
1052
|
+
plain `/mcp` endpoint for maintainers and is required: a deployment that omits it fails at
|
|
968
1053
|
construction and must choose `true` or `false`. A pool inherits it unless the
|
|
969
|
-
pool sets its own `readOnly`, in either direction. A
|
|
1054
|
+
pool sets its own `readOnly`, in either direction. A person routed to their
|
|
1055
|
+
pool at plain `/mcp` gets that pool's effective `readOnly`. A writable endpoint lets
|
|
970
1056
|
`call_destructive_tool` and programs write, and the MCP client decides what to
|
|
971
1057
|
approve. On a read-only endpoint, write tools are absent from `search_tools`
|
|
972
1058
|
results, `connecta.search`, and `connecta.describe`, and `call_tool`,
|
|
@@ -997,9 +1083,9 @@ for agents, just as an `authScope: "personal"` connector is: `search_tools`,
|
|
|
997
1083
|
`connecta.search`, `connecta.describe`, `skills`, and connector discovery omit
|
|
998
1084
|
it, `authorize_connector` answers "Unknown connector", and a call by address
|
|
999
1085
|
fails `unknown_address`, the same as for a connector that does not exist.
|
|
1000
|
-
People
|
|
1001
|
-
|
|
1002
|
-
|
|
1086
|
+
People use a separate people-only pool, where they see the connector and
|
|
1087
|
+
act as themselves. Share the same tool-list constant between the people pool
|
|
1088
|
+
and an agent-only pool when both need the same tools.
|
|
1003
1089
|
|
|
1004
1090
|
`owner` is a Workspace email address, checked at construction and listed in
|
|
1005
1091
|
`describeConfig()` (`null` when unset). Over HTTP, `/ui/api/config` serves it
|
|
@@ -1025,7 +1111,7 @@ createConnecta({
|
|
|
1025
1111
|
pools: Object.fromEntries(
|
|
1026
1112
|
team.map((person) => [
|
|
1027
1113
|
`assistant-${person.handle}`,
|
|
1028
|
-
{ tools: ["gmail", "drive"], owner: person.email, members: [
|
|
1114
|
+
{ tools: ["gmail", "drive"], owner: person.email, members: ["agent:assistant"] },
|
|
1029
1115
|
]),
|
|
1030
1116
|
),
|
|
1031
1117
|
connectors: [
|
|
@@ -1037,12 +1123,11 @@ createConnecta({
|
|
|
1037
1123
|
```
|
|
1038
1124
|
|
|
1039
1125
|
The agent harness, not the model, picks the pool for each turn, which is what
|
|
1040
|
-
keeps one person's mailbox from another's.
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
([members of a personal pool](#organizing-an-agents-pools)).
|
|
1126
|
+
keeps one person's mailbox from another's. Personal agent pools list only
|
|
1127
|
+
the agent, whether or not they set an `owner`. People use separate pools;
|
|
1128
|
+
their Workspace calls use the mapping and always act as themselves. A shared-auth
|
|
1129
|
+
connector holding a user's own account runs as that account for every admitted
|
|
1130
|
+
agent ([personal pools](#organizing-an-agents-pools)).
|
|
1046
1131
|
|
|
1047
1132
|
A fixed `subject` address is the explicit shared-account exception: every
|
|
1048
1133
|
caller, person or agent, acts as it in every pool that lists the connector,
|
|
@@ -1067,10 +1152,10 @@ section holds the complete rules.
|
|
|
1067
1152
|
2. **Tiers.** An agent's tiers are named pools ordered from least to most
|
|
1068
1153
|
capable. Recommended names are `<agent>` for the lowest tier and
|
|
1069
1154
|
`<agent>-<tier>` for higher tiers (`assistant-team`, `assistant-leads`).
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1155
|
+
Every tier is agent-only. A human team with the same reach uses a separate
|
|
1156
|
+
pool sharing the same tool-list constant (rule 10). An agent with one tier
|
|
1157
|
+
uses it as the lowest. Tiers never hold per-user connectors and never set
|
|
1158
|
+
an `owner`.
|
|
1074
1159
|
3. **Personal pool `<agent>-<user>`.** `/agent/<agent>-<user>` serves pool
|
|
1075
1160
|
`<agent>-<user>`: the highest tier that user is cleared for, plus that
|
|
1076
1161
|
user's own connectors. Use it only in a one-on-one with that user.
|
|
@@ -1103,26 +1188,24 @@ section holds the complete rules.
|
|
|
1103
1188
|
reaches every tier above. For example, billing restricted to leads goes
|
|
1104
1189
|
in the leads tier. Per-user connectors (`notes_alice`) go only in personal
|
|
1105
1190
|
pools, and so do the Workspace connectors with a mapping `subject` that
|
|
1106
|
-
the agent uses
|
|
1191
|
+
the agent uses. People use a separate pool for their own accounts.
|
|
1107
1192
|
8. **Changes stay in config.** Adding an account for a user adds its
|
|
1108
1193
|
connector and lists it in that user's pool `tools`. Adding a user adds one
|
|
1109
1194
|
`<agent>-<user>` pool. Adding a tier adds one pool and one constant, and
|
|
1110
1195
|
the tier's place in the harness's tier list.
|
|
1111
|
-
9. **
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
are absent for the agent, since a tier has no owner and an agent has no
|
|
1125
|
-
principal.
|
|
1196
|
+
9. **Personal pools list only the agent.** An `<agent>-<user>` pool lists
|
|
1197
|
+
only `"agent:<agent>"`, with or without an `owner`. No person belongs
|
|
1198
|
+
there: mixed membership throws `ConfigError` at construction. A person's
|
|
1199
|
+
own use goes through a separate people-only pool or plain `/mcp`.
|
|
1200
|
+
10. **People use separate team pools.** A tier is never a human team pool.
|
|
1201
|
+
Define `teamTools` once, then use it for `team` with people in
|
|
1202
|
+
`members` and `assistant-team` with only the agent. People open
|
|
1203
|
+
`/mcp/team`; the agent opens `/agent/assistant-team`. Every person
|
|
1204
|
+
listed must be cleared for every shared connector in their pool.
|
|
1205
|
+
The human pool may also list Workspace mapping connectors and
|
|
1206
|
+
`authScope: "personal"` connectors, on which each person acts as
|
|
1207
|
+
themselves. Keep them out of the tier: they are absent for agents,
|
|
1208
|
+
since an agent has no principal and a tier has no owner.
|
|
1126
1209
|
|
|
1127
1210
|
**Choosing the pool each turn**
|
|
1128
1211
|
|
|
@@ -1176,8 +1259,9 @@ createConnecta({
|
|
|
1176
1259
|
pools: {
|
|
1177
1260
|
// Lowest tier and fallback: reads guests may use.
|
|
1178
1261
|
assistant: { tools: guestTools, readOnly: true, members: [agent] },
|
|
1179
|
-
//
|
|
1180
|
-
|
|
1262
|
+
// Same reach, separate membership: people and the agent each have a pool.
|
|
1263
|
+
team: { tools: teamTools, members: ["user_alice", "user_bob"] },
|
|
1264
|
+
"assistant-team": { tools: teamTools, members: [agent] },
|
|
1181
1265
|
"assistant-leads": { tools: leadsTools, members: [agent] },
|
|
1182
1266
|
// Each personal pool spreads its user's highest tier.
|
|
1183
1267
|
"assistant-alice": {
|
|
@@ -1235,8 +1319,8 @@ gets `assistant`, even if the platform marks the chat as direct. Rule 9 is
|
|
|
1235
1319
|
why each personal pool lists only the agent: people act as themselves only
|
|
1236
1320
|
through a Workspace mapping, and a shared-auth connector such as
|
|
1237
1321
|
`notes_alice` acts as Alice for whoever the pool admits.
|
|
1238
|
-
Rule 10
|
|
1239
|
-
|
|
1322
|
+
Rule 10 gives Alice and Bob a separate `team` pool sharing `teamTools`;
|
|
1323
|
+
`poolFor` selects only agent pools and its tier selection stays the same.
|
|
1240
1324
|
|
|
1241
1325
|
## Shared and personal auth
|
|
1242
1326
|
|
|
@@ -1257,8 +1341,8 @@ A personal connector is absent — not refused — from any request without a
|
|
|
1257
1341
|
stable namespaced principal. For one that can see it, connector storage, vault
|
|
1258
1342
|
records, OAuth generations, and observed result shapes are
|
|
1259
1343
|
partitioned under an opaque SHA-256 identity key. Remote catalog cache keys
|
|
1260
|
-
include that owner and the admitted pool, plus the credential digest and
|
|
1261
|
-
epoch; refreshes coalesce within
|
|
1344
|
+
include that owner and the admitted pool, plus the keyed credential digest and
|
|
1345
|
+
OAuth epoch (see [cache keys](./architecture.md#storage-credentials-and-connectors)); refreshes coalesce within
|
|
1262
1346
|
one owner, and another owner's epoch cannot retire them. Keep namespaces and
|
|
1263
1347
|
principal ids stable across upgrades, since changing either selects different
|
|
1264
1348
|
partitions. Literal `auth: { type: "headers" }` keeps its secret in deployment
|
|
@@ -1266,6 +1350,16 @@ code, so `remoteMcp()` refuses it as personal at construction.
|
|
|
1266
1350
|
|
|
1267
1351
|
## Downstream OAuth state at rest
|
|
1268
1352
|
|
|
1353
|
+
An OAuth remote MCP connector requires an access token on passive MCP requests,
|
|
1354
|
+
including handshakes, catalog requests, and tool dispatches, even if the server
|
|
1355
|
+
accepts anonymous requests. This requirement survives provider replacement.
|
|
1356
|
+
If the SDK cannot obtain a token at send time, Connecta returns OAuth recovery
|
|
1357
|
+
before sending. A locally refused write counts as failed, without reconciliation;
|
|
1358
|
+
a write that may already have left remains unknown and is never replayed.
|
|
1359
|
+
Only an explicit authorization start may probe the MCP endpoint without a token
|
|
1360
|
+
for the SDK's challenge, never to call a tool. Authorization-server discovery,
|
|
1361
|
+
client registration, token exchange, and refresh retain their OAuth flows.
|
|
1362
|
+
|
|
1269
1363
|
Each owner of an OAuth connector (the deployment when shared, each principal
|
|
1270
1364
|
when personal) has one **grant record**, `oauth:grant` in the connector's
|
|
1271
1365
|
namespace: the live **epoch**, the latest consent's state digest, and one
|
|
@@ -1286,6 +1380,17 @@ connector](#downstream-oauth-on-api) stores no client. A vault without the
|
|
|
1286
1380
|
optional `seal`/`open` members keeps the body plaintext with a startup
|
|
1287
1381
|
warning; no vault, plaintext as always.
|
|
1288
1382
|
|
|
1383
|
+
The body also holds `expiresAt`, the epoch milliseconds the stored access token
|
|
1384
|
+
expires: its `expires_in`, counted from just before the request that issued it
|
|
1385
|
+
was sent (the code exchange, or the refresh's token request), never from when
|
|
1386
|
+
its answer arrived or was saved, so a slow answer cannot push the expiry past
|
|
1387
|
+
the token's real one. An `expires_in` of `0` expires at issuance. It is written
|
|
1388
|
+
with the tokens it describes, replaced with them and dropped with them, in the
|
|
1389
|
+
sealed and plaintext layouts alike. A grant stored before this field existed,
|
|
1390
|
+
or one whose token response carried no `expires_in`, has none. Reading a grant
|
|
1391
|
+
never adds it (INV-10), so such a grant gains one only at its next token write,
|
|
1392
|
+
and until then is refreshed only after a 401.
|
|
1393
|
+
|
|
1289
1394
|
**Pre-layout-3 grants.** The layout-2 migration and historical-key cleanup
|
|
1290
1395
|
are removed. An owner with only old keys is treated as unauthorized; a warning
|
|
1291
1396
|
names the unsupported layout and the need to Connect again or migrate on
|
|
@@ -1342,7 +1447,7 @@ with the gate a call passes:
|
|
|
1342
1447
|
| Stored grant | Status | Start (Connect, Continue, Restart, `authorize_connector`) |
|
|
1343
1448
|
| ------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1344
1449
|
| Disconnected | `auth_required`; the downstream is not asked | Replaces the epoch, then proceeds as for no tokens |
|
|
1345
|
-
| No tokens |
|
|
1450
|
+
| No tokens | `auth_required`; tokenless MCP requests are refused locally | Connects first, so a 401's `resource_metadata` starts consent as before. A downstream that answers without credentials still gets consent, discovered from RFC 9728 metadata at the configured URL's well-known locations. The start answers `auth_required` with the consent URL. |
|
|
1346
1451
|
| Tokens (any expiry) | Connects; `ok`, or `auth_required` when a 401 needs consent | Connects; `ok` without starting a flow when healthy |
|
|
1347
1452
|
|
|
1348
1453
|
A start without a challenge runs the same SDK flow as a 401 without
|
|
@@ -1419,6 +1524,47 @@ removal and revocation flow when deployment configuration supplies
|
|
|
1419
1524
|
`revocationEndpoint`; without it, removal remains local. The configured
|
|
1420
1525
|
client secret is used in memory and is never saved with the grant.
|
|
1421
1526
|
|
|
1527
|
+
## Refreshing before expiry
|
|
1528
|
+
|
|
1529
|
+
A call refreshes the grant before it sends a token that is within 60 seconds of
|
|
1530
|
+
its stored `expiresAt`, so a write never meets the 401 it could not be repeated
|
|
1531
|
+
after (INV-9). Sixty seconds covers the refresh's own 20-second token-request
|
|
1532
|
+
deadline and the call's send with margin, and matches the early refresh of
|
|
1533
|
+
Google service-account tokens; for a typical one-hour token it is under 2% of
|
|
1534
|
+
its life. A token that lives under a minute, or arrives already expired, is
|
|
1535
|
+
refreshed once per call, in the invocation's check. Later in the same request,
|
|
1536
|
+
`callTool` and `ctx.oauth.fetch` send the token that renewal produced without
|
|
1537
|
+
renewing it again until its stored expiry passes (or, for a token already
|
|
1538
|
+
expired when issued, for the next check only); a token another request rotated
|
|
1539
|
+
in since is checked as usual.
|
|
1540
|
+
|
|
1541
|
+
The renewal is the flow a 401 would start, through the same refresh
|
|
1542
|
+
coordinator: one redemption per owner partition and epoch in an isolate, the
|
|
1543
|
+
shared-storage lease and spent-fingerprint CAS across isolates, and the commit
|
|
1544
|
+
before anyone is released. Its token request names the access token it
|
|
1545
|
+
replaces, so a caller whose own read came before another request's rotation
|
|
1546
|
+
adopts that rotation instead of redeeming again. Two isolates that find the
|
|
1547
|
+
same token due spend one refresh token between them.
|
|
1548
|
+
|
|
1549
|
+
| Where | When | A failed renewal |
|
|
1550
|
+
| --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1551
|
+
| The invocation's authorization check, before it enters the connector | Every `call_tool`, `call_destructive_tool`, and program call, after the grant read that check already makes | Fails the call before anything is sent: `downstream_oauth_required` (or retryable `unavailable` for a waiter deadline), with no `writeOutcome` and no `reconciliationRequired`. |
|
|
1552
|
+
| `remoteMcp()`'s `callTool`, before it sends `tools/call` on a reused client | The token the connection last read has come due since | Ends the call before `tools/call` is sent. A call already entered reports it as the invocation reports any failure after entry. |
|
|
1553
|
+
| `api()`'s `ctx.oauth.fetch`, before each request | The token the request just read is due, as in a handler that runs long | Is thrown into the handler, which may already have sent other requests, so a write keeps the invocation's conservative outcome. |
|
|
1554
|
+
|
|
1555
|
+
Whether a token is due is decided from the grant read the step already made: a
|
|
1556
|
+
grant not yet due costs no extra storage read. A due token costs one more grant
|
|
1557
|
+
read for the renewal's own flow, and then the refresh's own reads and writes.
|
|
1558
|
+
Status, listing (including the stored-catalog read without a connection),
|
|
1559
|
+
discovery, the operator UI, and `authorize_connector` never renew a token
|
|
1560
|
+
(INV-10). Nothing renews in the background: renewal belongs to the call that
|
|
1561
|
+
found the token due, which owns its preparation until the claim CAS is
|
|
1562
|
+
submitted, as for a 401.
|
|
1563
|
+
|
|
1564
|
+
A grant without `expiresAt` keeps the earlier behavior: it is refreshed after a
|
|
1565
|
+
downstream 401, a write's 401 is never resent, and the refresh that follows
|
|
1566
|
+
stores `expiresAt` for every later call.
|
|
1567
|
+
|
|
1422
1568
|
## Refresh failures
|
|
1423
1569
|
|
|
1424
1570
|
A refresh token is spent when dispatch begins. Every refresh request passes
|
|
@@ -1493,10 +1639,20 @@ a dispatched record. The refresh HTTP request and response body have a
|
|
|
1493
1639
|
20-second deadline. After the response or a dispatched exchange failure,
|
|
1494
1640
|
grant commit and cleanup share a separate 20-second deadline, including all
|
|
1495
1641
|
storage reads, CAS retries, fingerprint resolution, and lease release.
|
|
1496
|
-
Contenders wait at most 35 seconds
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1642
|
+
Contenders wait at most 35 seconds, and never past the caller's own signal,
|
|
1643
|
+
which carries the request's deadline. A waiter polls the lease from 50 ms,
|
|
1644
|
+
doubling to at most 2 seconds between polls, so a wait that runs the full 35
|
|
1645
|
+
seconds polls at most 22 times. Each poll is one batched storage read of the
|
|
1646
|
+
lease, the grant, the spent fingerprint, and the holder's liveness key named by
|
|
1647
|
+
the previous poll (one D1 statement); a store without batched reads reads them
|
|
1648
|
+
one at a time, lease first, so a completed lease is never paired with the grant
|
|
1649
|
+
from before its commit. A waiter deadline returns retryable `unavailable`,
|
|
1650
|
+
keeps the grant, and starts no consent. It never permits resending a spent
|
|
1651
|
+
token.
|
|
1652
|
+
|
|
1653
|
+
A grant with `expiresAt` is normally renewed before its token expires
|
|
1654
|
+
([Refreshing before expiry](#refreshing-before-expiry)). A token revoked early,
|
|
1655
|
+
or one stored without an expiry, can still draw a 401.
|
|
1500
1656
|
A `remoteMcp()` write the downstream answered with HTTP 401 is never sent
|
|
1501
1657
|
again (INV-9). The SDK still refreshes the grant, and connecta stops its
|
|
1502
1658
|
resend before it leaves, so any fresh call after the target is reconciled uses
|
|
@@ -2410,6 +2566,15 @@ Before an accepted retry restarts any call, it rechecks every entered read.
|
|
|
2410
2566
|
A write classification, changed classification digest, or stale catalog returns
|
|
2411
2567
|
`auth_replay_refused` with reconciliation guidance, including in programs on a writable endpoint.
|
|
2412
2568
|
Pre-invocation credential-resolution eligibility is unchanged.
|
|
2569
|
+
Custom vaults may implement the host-only `storageDigestKey` method, returning a
|
|
2570
|
+
non-extractable HMAC-SHA-256 signing `CryptoKey` that is stable across the
|
|
2571
|
+
deployment's processes, purpose-specific and never stored.
|
|
2572
|
+
`encryptedCredentialVault()` derives it with HKDF from its credential key.
|
|
2573
|
+
Remote catalog, negotiation-verdict and connector-health keys digest their
|
|
2574
|
+
inputs (configured URLs and headers, credentials, request tokens, OAuth token
|
|
2575
|
+
responses, described configuration) with it, so their partitions are
|
|
2576
|
+
deployment-keyed. Without it they use a random per-process key: partitions stay
|
|
2577
|
+
exact, but each process or isolate keeps its own entries.
|
|
2413
2578
|
Custom vaults may implement the host-only `requestStateKey` method; without it, the
|
|
2414
2579
|
deployment retains the ordinary handoff. It must return a stable,
|
|
2415
2580
|
purpose-specific deployment key of at least 32 bytes. Only the MCP boundary
|
|
@@ -2715,6 +2880,9 @@ owner's access token as `Authorization: Bearer` with these rules:
|
|
|
2715
2880
|
|
|
2716
2881
|
- Only `apiOrigins` receive tokens; refuse other origins before sending, including
|
|
2717
2882
|
untrusted response URLs. Return redirects unfollowed; handlers cannot set `Authorization`.
|
|
2883
|
+
- A token within a minute of its stored expiry is renewed before the handler
|
|
2884
|
+
runs, and again before any request that finds it due
|
|
2885
|
+
([Refreshing before expiry](#refreshing-before-expiry)).
|
|
2718
2886
|
- On 401, recover exactly once using an already rotated token or one coalesced
|
|
2719
2887
|
refresh through `remoteMcp()`'s coordinator. Persist rotation even when its
|
|
2720
2888
|
owner is cancelled after the answer. Only a positively read-classified
|
|
@@ -3140,6 +3308,7 @@ for credential precedence and disclosure boundaries.
|
|
|
3140
3308
|
|
|
3141
3309
|
Downstream OAuth implementation and evidence are in
|
|
3142
3310
|
[downstream-oauth.ts](https://github.com/zackbart/connecta/blob/main/src/auth/downstream-oauth.ts),
|
|
3311
|
+
its [seam modules](https://github.com/zackbart/connecta/tree/main/src/auth/downstream-oauth),
|
|
3143
3312
|
[static-oauth.ts](https://github.com/zackbart/connecta/blob/main/src/auth/static-oauth.ts),
|
|
3144
3313
|
[OAuth connect tests](https://github.com/zackbart/connecta/blob/main/test/oauth-connect.test.ts),
|
|
3145
3314
|
and [API OAuth tests](https://github.com/zackbart/connecta/blob/main/test/api-oauth.test.ts).
|