@zackbart/connecta 0.31.0 → 0.33.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 +97 -0
- package/README.md +30 -20
- package/bin/connecta.mjs +25 -6
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.d.ts +167 -23
- package/dist/access-tokens.js +676 -140
- package/dist/agent-identity.js +47 -0
- package/dist/auth/cloudflare-access.js +13 -5
- package/dist/auth-elicitation.js +1 -1
- package/dist/call-admission-context.js +15 -0
- package/dist/call-admission.js +30 -43
- package/dist/call-recovery.js +6 -2
- package/dist/catalog-service.js +7 -2
- package/dist/config-value-sources.js +14 -9
- package/dist/config.d.ts +58 -78
- package/dist/config.js +143 -47
- package/dist/connector-access.d.ts +4 -33
- package/dist/connector-access.js +15 -99
- package/dist/connector-caller.d.ts +7 -1
- package/dist/connector-caller.js +3 -2
- package/dist/connectors/api-connector.d.ts +27 -4
- package/dist/connectors/api-connector.js +2 -0
- package/dist/connectors/guarded-fetch.d.ts +9 -0
- package/dist/connectors/guarded-fetch.js +7 -0
- package/dist/connectors/remote-mcp.js +2 -0
- package/dist/describe-config.d.ts +32 -7
- package/dist/describe-config.js +9 -18
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -0
- package/dist/execute.js +16 -14
- package/dist/index.d.ts +4 -3
- package/dist/index.js +30 -10
- package/dist/invocation.js +12 -7
- package/dist/membership.d.ts +51 -0
- package/dist/membership.js +121 -0
- package/dist/meta-tools.js +17 -19
- package/dist/module-contracts.d.ts +23 -6
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +2 -2
- package/dist/operator-ui/agents.d.ts +74 -0
- package/dist/operator-ui/agents.js +104 -0
- package/dist/operator-ui/contract.d.ts +151 -29
- package/dist/operator-ui/endpoints.js +192 -0
- package/dist/operator-ui/fix-prompts.js +4 -4
- package/dist/operator-ui/generated.js +5 -4
- package/dist/operator-ui/member-page.js +112 -0
- package/dist/operator-ui/model.d.ts +15 -7
- package/dist/operator-ui/view.d.ts +84 -19
- package/dist/operator-ui/view.js +83 -21
- package/dist/page-styles.js +1 -1
- package/dist/program-writes.js +2 -2
- package/dist/provider.d.ts +63 -11
- package/dist/provider.js +46 -7
- package/dist/providers/_shared/google/workspace.d.ts +53 -16
- package/dist/providers/_shared/google/workspace.js +31 -9
- package/dist/providers/_shared/rest/tools.js +2 -2
- package/dist/providers/_shared/rest/value-safety.d.ts +50 -0
- package/dist/providers/_shared/rest/value-safety.js +699 -7
- package/dist/providers/breeze/skill.generated.js +2 -2
- package/dist/providers/ccb/index.js +1 -1
- package/dist/providers/cloudflare/index.js +3 -3
- package/dist/providers/cloudflare/rest.js +25 -3
- package/dist/providers/cloudflare/skill.generated.js +2 -2
- package/dist/providers/cloudflare/value-safety.js +32 -0
- package/dist/providers/docs/index.d.ts +1 -1
- package/dist/providers/docs/index.js +5 -5
- package/dist/providers/docs/skill.generated.js +2 -2
- package/dist/providers/drive/index.d.ts +1 -1
- package/dist/providers/drive/index.js +4 -4
- package/dist/providers/drive/skill.generated.js +2 -2
- package/dist/providers/forms/index.d.ts +1 -1
- package/dist/providers/forms/index.js +5 -5
- package/dist/providers/forms/skill.generated.js +2 -2
- package/dist/providers/github/skill.generated.js +2 -2
- package/dist/providers/gmail/index.d.ts +1 -1
- package/dist/providers/gmail/index.js +279 -26
- package/dist/providers/gmail/skill.generated.js +2 -2
- package/dist/providers/linear/index.js +1 -1
- package/dist/providers/mixpanel/index.d.ts +27 -12
- package/dist/providers/mixpanel/index.js +58 -6
- package/dist/providers/mixpanel/operations.js +20 -0
- package/dist/providers/mixpanel/rest.js +603 -0
- package/dist/providers/mixpanel/skill.generated.js +3 -2
- package/dist/providers/mixpanel/value-safety.js +16 -0
- package/dist/providers/notion/index.d.ts +7 -0
- package/dist/providers/notion/index.js +116 -11
- package/dist/providers/notion/rest.js +183 -24
- package/dist/providers/notion/skill.generated.js +4 -4
- package/dist/providers/notion/value-safety.js +30 -5
- package/dist/providers/overflow/index.js +1 -1
- package/dist/providers/planning-center/index.d.ts +27 -0
- package/dist/providers/planning-center/index.js +287 -25
- package/dist/providers/planning-center/skill.generated.js +24 -4
- package/dist/providers/sheets/index.d.ts +4 -1
- package/dist/providers/sheets/index.js +9 -6
- package/dist/providers/sheets/skill.generated.js +2 -2
- package/dist/providers/slides/index.d.ts +1 -1
- package/dist/providers/slides/index.js +6 -6
- package/dist/providers/slides/skill.generated.js +2 -2
- package/dist/providers/stripe/rest.js +9 -2
- package/dist/providers/stripe/skill.generated.js +2 -2
- package/dist/providers/stripe/value-safety.js +22 -0
- package/dist/providers/vercel/index.js +2 -2
- package/dist/providers/vercel/rest.js +20 -2
- package/dist/providers/vercel/skill.generated.js +2 -2
- package/dist/providers/vercel/value-safety.js +22 -0
- package/dist/providers/wispr-flow/index.d.ts +14 -0
- package/dist/providers/wispr-flow/index.js +117 -0
- package/dist/providers/wispr-flow/skill.generated.js +8 -0
- package/dist/registry.d.ts +2 -3
- package/dist/registry.js +8 -9
- package/dist/routes/access-tokens.js +82 -75
- package/dist/routes/activity-disclosure.js +17 -18
- package/dist/routes/activity.js +34 -54
- package/dist/routes/actor-labels.js +33 -4
- package/dist/routes/agent-path.js +43 -0
- package/dist/routes/connect.js +16 -12
- package/dist/routes/credentials.js +16 -40
- package/dist/routes/mcp.js +155 -85
- package/dist/routes/oauth-management.js +11 -5
- package/dist/routes/oauth.js +3 -9
- package/dist/routes/operator.js +25 -17
- package/dist/routes/shared.d.ts +60 -20
- package/dist/routes/shared.js +187 -96
- package/dist/routes/ui-config.js +437 -97
- package/dist/routes/ui-data.js +2 -2
- package/dist/routes/ui.js +9 -16
- package/dist/runtime/call-admission.js +9 -0
- package/dist/server.js +8 -0
- package/dist/skills.js +5 -4
- package/dist/storage/keys.js +9 -1
- package/dist/tool-safety.js +13 -5
- package/dist/types.d.ts +12 -1
- package/dist/ui.d.ts +2 -2
- package/dist/ui.js +17 -15
- package/dist/usage-guide.js +3 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/workspace-address.js +9 -0
- package/dist/workspace-subject.js +23 -0
- package/documentation/README.md +46 -22
- package/documentation/architecture.md +378 -58
- package/documentation/auth.md +806 -215
- package/documentation/code-mode.md +93 -81
- package/documentation/deploying.md +128 -26
- package/documentation/integrating.md +145 -22
- package/documentation/meta-tools.md +58 -46
- package/documentation/migration-0.33.md +344 -0
- package/documentation/operating.md +14 -9
- package/documentation/operator-tests.md +117 -31
- package/documentation/operator-ui.md +326 -55
- package/documentation/provider-auth-migration.md +42 -27
- package/documentation/provider-migration-0.29.md +3 -3
- package/examples/worker/AGENTS.md +35 -8
- package/examples/worker/README.md +92 -53
- package/examples/worker/src/connecta.config.ts +41 -20
- package/examples/worker/wrangler.jsonc +3 -0
- package/package.json +7 -2
- package/templates/node/.env.example +3 -0
- package/templates/node/AGENTS.md +33 -10
- package/templates/node/Dockerfile +2 -2
- package/templates/node/README.md +71 -44
- package/templates/node/docker-compose.yml +3 -2
- package/templates/node/package.json +4 -3
- package/templates/node/src/adopt-token.ts +23 -0
- package/templates/node/src/connecta.config.ts +33 -12
- package/templates/node/src/rotate-token.ts +17 -0
- package/dist/tool-safety.d.ts +0 -9
- package/templates/node/src/provision-token.ts +0 -17
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Architecture
|
|
2
2
|
|
|
3
|
+
## Contributor reference
|
|
4
|
+
|
|
5
|
+
This guide describes Connecta implementation and repository verification.
|
|
6
|
+
It is optional for deployment operators. Start with the
|
|
7
|
+
[operator task index](./README.md#operator-tasks) for package-local procedures.
|
|
8
|
+
|
|
3
9
|
One Web-standard `fetch(request) => Promise<Response>` handler, a long-lived
|
|
4
10
|
registry behind it, and a strict rule about what may be imported. Everything
|
|
5
11
|
else in this repository is a detail of those three things.
|
|
@@ -85,20 +91,21 @@ table, security headers. Route _order_ is the contract — several routes would
|
|
|
85
91
|
behave differently if they were reachable in another order — so read the table
|
|
86
92
|
top to bottom.
|
|
87
93
|
|
|
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 | `/
|
|
94
|
-
| 1 |
|
|
95
|
-
| 1 |
|
|
96
|
-
|
|
|
97
|
-
| 2 |
|
|
98
|
-
|
|
|
99
|
-
|
|
|
100
|
-
|
|
|
101
|
-
|
|
|
94
|
+
| Order | Route | Notes |
|
|
95
|
+
| ----- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
96
|
+
| 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. |
|
|
97
|
+
| 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. |
|
|
98
|
+
| 0 | Cloudflare Access (Worker, when enabled) | Edge admission ahead of this table; an admitted invocation carries trusted identity in `ctx.access`. |
|
|
99
|
+
| 1 | Agent namespace, any path starting `/agent` once decoded | First, so no path Access bypasses reaches a human route. Exactly `/agent/<pool>`: admission, then the `cta_` adapter alone, then pool membership; every other spelling is the pool 404 before admission or auth. See [agent endpoints](./auth.md#agent-endpoints). |
|
|
100
|
+
| 1 | `/oauth/client-metadata/<connectorId>` | Public GET-only CIMD for configured self-hosted OAuth connectors; fixed `publicUrl`, no auth or storage. |
|
|
101
|
+
| 1 | Mounted UI routes | Before wildcard OPTIONS, so mutation routes refuse preflight rather than inheriting MCP CORS. No UI module, no routes. |
|
|
102
|
+
| 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. |
|
|
103
|
+
| 2 | MCP preflight | Allowed `OPTIONS` on `/mcp*`: 204 without admission or auth. |
|
|
104
|
+
| 2 | Other `OPTIONS` | Auth metadata first, otherwise compatibility CORS preflight. |
|
|
105
|
+
| 3 | `/.well-known/*` | Public auth metadata, or 404. Protected-resource discovery does not authenticate callers or evaluate endpoint grants. |
|
|
106
|
+
| 4 | `/health` | Open and payload-free: health, executor, admission, and deployment metadata. |
|
|
107
|
+
| 6 | `/mcp`, `/mcp/<pool>` | Admission, then auth, then a request-local MCP server. No body is read before auth and endpoint admission. A request naming `subscriptions/listen` takes its permit only after a bounded classification read (413 past 64 KiB); a body-confirmed modern listen takes none and is refused after SDK validation. A non-maintainer at `/mcp`, a non-member, and an undeclared pool get one identical 404; see [pools](./auth.md#pools). |
|
|
108
|
+
| 7 | Other paths | 404. Custom HTTP routes belong to the deployment. |
|
|
102
109
|
|
|
103
110
|
Every response leaves through `withSecurityHeaders`. HTML uses one common
|
|
104
111
|
security-header helper, including status and error pages. Operator shells
|
|
@@ -112,31 +119,46 @@ reordering reads like a harmless refactor.
|
|
|
112
119
|
An admitted non-preflight `/mcp` request then takes five steps in
|
|
113
120
|
`src/routes/mcp.ts`:
|
|
114
121
|
|
|
115
|
-
1. **Admit.**
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
122
|
+
1. **Admit.** No request body is read before auth on any endpoint. A request
|
|
123
|
+
whose `Mcp-Method` header names `subscriptions/listen` takes no permit
|
|
124
|
+
before auth. After auth and endpoint admission connecta reads at most
|
|
125
|
+
64 KiB (`MAX_LISTEN_CLASSIFICATION_BYTES`) of it: past that it answers
|
|
126
|
+
413 `-33006` and stops reading. A body that confirms a modern listen stays
|
|
127
|
+
permit-free, passes SDK validation, and receives HTTP 404 with JSON-RPC
|
|
128
|
+
`-32601`; anything else takes a permit then, so the header alone bypasses
|
|
129
|
+
nothing. Auth and classification share the configured request lifetime.
|
|
130
|
+
Other requests take one permit from the deployment-wide pool, taken before
|
|
131
|
+
auth so an unauthenticated flood costs a permit rather than a Clerk lookup,
|
|
132
|
+
and held until the response _body_ completes, the caller leaves, or its
|
|
133
|
+
configured lifetime ends, not until the handler returns.
|
|
134
|
+
2. **Authorize.** `/agent/<pool>` consults only the built-in `cta_` adapter
|
|
135
|
+
(`authorizeAgent`), never with the runtime context that carries
|
|
136
|
+
`ctx.access`. Human endpoints refuse a `cta_` token before recognition or
|
|
137
|
+
storage, and never ask the `cta_` adapter. A provider's
|
|
125
138
|
synchronous credential recognition selects the sole verifier for an explicit
|
|
126
|
-
credential; a refusal never falls back to another identity.
|
|
127
|
-
reject machine credentials without verification. With no recognized
|
|
139
|
+
credential; a refusal never falls back to another identity. With no recognized
|
|
128
140
|
credential, configured providers are tried in order. A 401 takes its
|
|
129
141
|
challenge from the actual protected-resource metadata owner. No providers
|
|
130
142
|
means open development and warns at construction.
|
|
131
|
-
3. **
|
|
132
|
-
|
|
133
|
-
|
|
143
|
+
3. **Admit to the endpoint.** Plain `/mcp` admits a principal listed in
|
|
144
|
+
`maintainers` (or, with no inbound auth, the anonymous caller) and serves the
|
|
145
|
+
whole catalog. `/mcp/<pool>` admits the pool's human `members`, matched by
|
|
146
|
+
principal, and `/agent/<pool>` its agent members, matched by
|
|
147
|
+
`identity.agent`; both serve exactly the pool's tools
|
|
148
|
+
(`src/membership.ts`, `admitsMaintainer`, `memberPool`, and `agentPool` in
|
|
149
|
+
`src/routes/shared.ts`). Nothing intersects or widens either view; every
|
|
150
|
+
refusal is one 404 naming no pool.
|
|
151
|
+
The endpoint's `readOnly` is the pool's own value, else the deployment's;
|
|
152
|
+
plain `/mcp` uses the deployment's. It travels to the meta-tools and the
|
|
153
|
+
program as one boolean that drops writes from `CatalogService` search and
|
|
154
|
+
describe and makes `writeRefusal` (`src/tool-safety.ts`) refuse every write
|
|
155
|
+
call. Guides and skills are not filtered.
|
|
134
156
|
4. **Derive the registry view.** One `registry.scoped(...)` call with those
|
|
135
|
-
connector ids, exact `connector.tool` addresses when the
|
|
157
|
+
connector ids, exact `connector.tool` addresses when the pool declares a
|
|
136
158
|
narrower slice, and the subject and principal keys. Personal connectors use
|
|
137
159
|
the principal partition and result paging the subject partition with principal,
|
|
138
|
-
endpoint/pool, origin, connector, and tool bindings. Paging
|
|
139
|
-
|
|
160
|
+
endpoint/pool, origin, connector, and tool bindings. Paging re-admits the
|
|
161
|
+
caller at its endpoint and rechecks the endpoint's `readOnly`; no caller parameter selects a partition (`test/identity-scope.test.ts`).
|
|
140
162
|
5. **Serve.** Refuse `?toolkit=` with a 404 — the toolkits are gone
|
|
141
163
|
([#178](https://github.com/zackbart/connecta/issues/178)) but their URLs were
|
|
142
164
|
handed out, and retiring a scoping boundary into fail-open is worse than any
|
|
@@ -186,13 +208,14 @@ request-scoped sent-credential set and pass through `redactAgentOutput` before
|
|
|
186
208
|
serialization. The SDK's `server/discover` response and registered meta-tool
|
|
187
209
|
results use that same set; connector contexts retain only an opaque scope.
|
|
188
210
|
|
|
189
|
-
| Refusal
|
|
190
|
-
|
|
|
191
|
-
| Request capacity exhausted
|
|
192
|
-
| Server shutting down
|
|
193
|
-
| Request lifetime exceeded
|
|
194
|
-
| Pool missing or refused
|
|
195
|
-
| Origin or admitted access forbidden
|
|
211
|
+
| Refusal | HTTP status | Application code | Previous code |
|
|
212
|
+
| ------------------------------------ | ----------- | ---------------- | ------------- |
|
|
213
|
+
| Request capacity exhausted | 503 | `-33001` | `-31001` |
|
|
214
|
+
| Server shutting down | 503 | `-33002` | `-31002` |
|
|
215
|
+
| Request lifetime exceeded | 504 | `-33003` | none |
|
|
216
|
+
| Pool missing or refused | 404 | `-33004` | none |
|
|
217
|
+
| Origin or admitted access forbidden | 403 | `-33005` | none |
|
|
218
|
+
| Listen classification body too large | 413 | `-33006` | none |
|
|
196
219
|
|
|
197
220
|
These allocations are outside JSON-RPC's reserved `-32768..-32000` range and
|
|
198
221
|
MCP's `-32020..-32099` range. The prior admission codes were also outside those
|
|
@@ -217,13 +240,15 @@ or hands out, and a change usually belongs in exactly one of them:
|
|
|
217
240
|
| `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
241
|
| `src/catalog.ts` | Ranking, description summarizing, and the compact and TypeScript schema renderers discovery shows. |
|
|
219
242
|
| `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` |
|
|
243
|
+
| `src/program-writes.ts` | Program write accounting on writable endpoints: write budgets, dispatch draining, and known or unknown outcomes. |
|
|
221
244
|
|
|
222
245
|
`src/meta-tools.ts` and `src/execute.ts` are two front doors onto the same two
|
|
223
246
|
services, `CatalogService` and `InvocationService`. That is the point: a
|
|
224
247
|
program's `connecta.call` and a top-level `call_tool` reach
|
|
225
248
|
`InvocationService.invoke` by different routes and get the same admission, the
|
|
226
|
-
same credential resolution, and the same fail-closed read-only check
|
|
249
|
+
same credential resolution, and the same fail-closed read-only check: one
|
|
250
|
+
`writeRefusal` decision per call, from the stored classification, the calling
|
|
251
|
+
surface, and the endpoint's `readOnly`.
|
|
227
252
|
`test/execute.test.ts` asserts the parity directly, because a sandbox path that
|
|
228
253
|
quietly diverges is how generated code would mint a capability.
|
|
229
254
|
|
|
@@ -266,6 +291,40 @@ cannot be faked as sequential leases: consuming a rolling token before a later
|
|
|
266
291
|
rule refuses would charge a call that never reached the provider, the exact
|
|
267
292
|
accounting error a budget exists to prevent. Both layers are pinned by
|
|
268
293
|
`test/request-admission.test.ts` and `test/call-admission.test.ts`.
|
|
294
|
+
Every defined budget must be an object with `kind: "rolling-window"` and positive
|
|
295
|
+
whole `maxCalls` and `windowMs` values. Malformed budgets, including `false`,
|
|
296
|
+
`null`, `0`, and `""`, throw during construction, even when the rule also sets
|
|
297
|
+
`maxConcurrency`.
|
|
298
|
+
|
|
299
|
+
An admitted tool call can charge sequential auxiliary downstream requests with the internal
|
|
300
|
+
`chargeConnectorCall(ctx)` hook in `src/call-admission-context.ts`. Invocation
|
|
301
|
+
binds the context to its existing permit; the hook returns a Promise over an
|
|
302
|
+
internal Effect program. The charge uses the enclosing call's partition
|
|
303
|
+
and concurrency slot, so a connector with `maxConcurrency: 1` can perform a
|
|
304
|
+
lookup without queuing behind itself. Exhaustion returns the same
|
|
305
|
+
`CallAdmissionError` and retry delay as ordinary call admission, before the
|
|
306
|
+
auxiliary request is sent. Charges against a configured policy also refuse
|
|
307
|
+
after cancellation, permit release, or controller shutdown. The hook adds no
|
|
308
|
+
public connector option or context member. Contexts outside registry invocation
|
|
309
|
+
have no bound permit, so the hook does nothing there. Providers await the hook
|
|
310
|
+
before sending and propagate its refusal; it does not acquire concurrency for
|
|
311
|
+
parallel auxiliary requests.
|
|
312
|
+
|
|
313
|
+
Cloudflare's REST connector charges each uncached zone-ownership lookup used
|
|
314
|
+
to enforce an account pin, in addition to the enclosing tool-call attempt.
|
|
315
|
+
Explicitly pinned zones and cached ownership need no extra request or charge.
|
|
316
|
+
Its limits of three ownership lookups per call and 60 per five minutes per
|
|
317
|
+
connector remain: they bound pin checks separately from the connector's larger
|
|
318
|
+
shared call budget. The Cloudflare provider tests cover these checks through
|
|
319
|
+
registry invocation as well as the existing pin refusals.
|
|
320
|
+
|
|
321
|
+
Only an omitted or `undefined` `callAdmission` selects a connector's default.
|
|
322
|
+
The shared schema rejects malformed defined policies at factory construction,
|
|
323
|
+
and the registry validates custom connector policies before registration.
|
|
324
|
+
Falsy values such as `false`, `null`, `0`, and `""` are construction errors
|
|
325
|
+
(INV-11), never a way to disable admission. The generated provider fixtures
|
|
326
|
+
exercise these refusals and unchanged defaults in
|
|
327
|
+
`test/call-admission-options.test.ts`.
|
|
269
328
|
|
|
270
329
|
### Production Worker disconnects
|
|
271
330
|
|
|
@@ -328,6 +387,173 @@ and deletes the Worker in `finally`. It uses no `waitUntil`, which would change
|
|
|
328
387
|
the lifetime being measured. The production runtime build number was not
|
|
329
388
|
exposed by the deployment; these findings are tied to the date and flags above.
|
|
330
389
|
|
|
390
|
+
### Production Access bypass for agent endpoints
|
|
391
|
+
|
|
392
|
+
P0 of [#837](https://github.com/zackbart/connecta/issues/837) asked whether
|
|
393
|
+
`/agent/*` can skip a Worker-level Cloudflare Access application, so an agent
|
|
394
|
+
sends only its `cta_` token, while every other path keeps Access and
|
|
395
|
+
`ctx.access`. It can. The probe ran on 2026-10-15 against a disposable Worker on
|
|
396
|
+
its `workers.dev` hostname, with compatibility date `2026-09-01`, `cf`
|
|
397
|
+
1.0.0-beta.14, Wrangler 4.114.0, and curl 8.7.1 with `--path-as-is`, so raw
|
|
398
|
+
paths reached the edge unnormalized. The Worker-level application allowed only
|
|
399
|
+
a probe service token. Its unauthenticated refusal was therefore Access's 403
|
|
400
|
+
page rather than a login redirect. The [observations](https://github.com/zackbart/connecta/blob/main/scripts/probes/access-agent-bypass-2026-10-15.json)
|
|
401
|
+
record three identical rounds after each configuration change had produced
|
|
402
|
+
three consecutive matching responses.
|
|
403
|
+
|
|
404
|
+
- **Precedence.** A self-hosted application whose destination is
|
|
405
|
+
`{ "type": "public", "uri": "<host>/agent/*" }`, with one `bypass` policy
|
|
406
|
+
including `everyone`, overrides the Worker-level application on matching
|
|
407
|
+
paths only. Unauthenticated requests reach the Worker there. `/mcp`, `/`,
|
|
408
|
+
and every unmatched path still get the Access refusal. A service token is
|
|
409
|
+
still admitted on them with `ctx.access` from the Worker-level application.
|
|
410
|
+
This confirms Cloudflare's documented hierarchy, and that Worker-level
|
|
411
|
+
protection still covers the `workers.dev` hostname.
|
|
412
|
+
- **`ctx.access` on a bypassed request is present, not absent.** Its `aud` is
|
|
413
|
+
nonempty and is the _path_ application's AUD, not the Worker-level one.
|
|
414
|
+
`getIdentity()` resolved to `undefined` for every credential tested: none, a
|
|
415
|
+
valid service token, a `cta_`-style bearer token, and a forged
|
|
416
|
+
`Cf-Access-Jwt-Assertion`. A service token on a protected path also gave
|
|
417
|
+
`undefined` identity, carrying the Worker-level AUD. A human
|
|
418
|
+
`CF_Authorization` cookie on a bypassed path was not tested, so what
|
|
419
|
+
`getIdentity()` returns there is unknown. That is one more reason the agent
|
|
420
|
+
route ignores Access context rather than relying on its contents.
|
|
421
|
+
- **API shape.** A Worker destination is exclusive to one application. A
|
|
422
|
+
second application with `{ "type": "worker", "worker_id" }` plus `uri` or
|
|
423
|
+
`path` was refused with 409 `destination belongs to another application`. The
|
|
424
|
+
bypass must be hostname-and-path, stored with domain `<host>/agent/*`, and it
|
|
425
|
+
covers only that exact hostname.
|
|
426
|
+
- **Propagation.** Creating or deleting each application took 15–19 seconds to
|
|
427
|
+
settle across runs. While a deployment or application change was propagating,
|
|
428
|
+
the edge returned transient 500 (`error code: 1104`) and 404 (`error code:
|
|
429
|
+
1042` or `1043`) responses. The recorded run has one such 404, on the
|
|
430
|
+
Worker-level-only service-token control for `/agent/x`. Deleting the path
|
|
431
|
+
application restored the 403 on `/agent/x`.
|
|
432
|
+
|
|
433
|
+
| Raw request path | Access outcome | Worker pathname (+ search) |
|
|
434
|
+
| -------------------------- | -------------- | -------------------------- |
|
|
435
|
+
| `/agent/x` | bypassed | `/agent/x` |
|
|
436
|
+
| `/agent/assistant-alice` | bypassed | `/agent/assistant-alice` |
|
|
437
|
+
| `/agent/` | bypassed | `/agent/` |
|
|
438
|
+
| `/agent/x/` | bypassed | `/agent/x/` |
|
|
439
|
+
| `/agent/x?y` | bypassed | `/agent/x?y` |
|
|
440
|
+
| `/AGENT/x` | bypassed | `/AGENT/x` |
|
|
441
|
+
| `/%61gent/x` | bypassed | `/%61gent/x` |
|
|
442
|
+
| `//agent/x` | bypassed | `//agent/x` |
|
|
443
|
+
| `/agent/%2f/mcp` | bypassed | `/agent/%2f/mcp` |
|
|
444
|
+
| `/agent/x%2f..%2f..%2fmcp` | bypassed | `/agent/x%2f..%2f..%2fmcp` |
|
|
445
|
+
| `/mcp/../agent/x` | bypassed | `/agent/x` |
|
|
446
|
+
| `/mcp` | protected | not reached |
|
|
447
|
+
| `/` | protected | not reached |
|
|
448
|
+
| `/agent` | protected | not reached |
|
|
449
|
+
| `/agentx` | protected | not reached |
|
|
450
|
+
| `/mcp/agent/x` | protected | not reached |
|
|
451
|
+
| `/agent/../mcp` | protected | not reached |
|
|
452
|
+
| `/agent/x/../../mcp` | protected | not reached |
|
|
453
|
+
| `/agent/%2e%2e/mcp` | protected | not reached |
|
|
454
|
+
| `/agent/%2E%2E/mcp` | protected | not reached |
|
|
455
|
+
| `/agent%2f..%2fmcp` | protected | not reached |
|
|
456
|
+
|
|
457
|
+
Access matches after resolving dot segments, including `%2e` forms. It matches
|
|
458
|
+
case-insensitively, decodes unreserved escapes such as `%61`, collapses a
|
|
459
|
+
leading `//`, and treats `%2f` as data, not a separator. The effective Worker
|
|
460
|
+
pathname, `new URL(request.url).pathname`, is the raw request path with case,
|
|
461
|
+
escapes, and slashes untouched, except that dot segments arrive resolved:
|
|
462
|
+
`/mcp/../agent/x` arrives as `/agent/x`. Every path that
|
|
463
|
+
resolves outside `/agent/*`, including every `/mcp` form, stayed protected. The
|
|
464
|
+
inverse is the risk: Access bypasses paths
|
|
465
|
+
that connecta must not treat as an agent pool. Connecta's router compares raw
|
|
466
|
+
pathnames without decoding, case folding, or slash collapsing, so an `/agent/`
|
|
467
|
+
route must keep that property. It should accept exactly
|
|
468
|
+
`/agent/<pool>`, with `<pool>` byte-equal to a declared pool name. It should
|
|
469
|
+
refuse an empty segment, a trailing slash, any extra segment, `%`, uppercase
|
|
470
|
+
variants, and a leading `//`, all with the same 404. It must ignore
|
|
471
|
+
`ctx.access` entirely, since a bypassed request still carries an AUD.
|
|
472
|
+
[Agent endpoints](./auth.md#agent-endpoints) implement exactly this in
|
|
473
|
+
`src/routes/agent-path.ts`, and `test/agent-endpoints.test.ts` replays every
|
|
474
|
+
row of the table above. The hyphenated row stands for any two-part pool name
|
|
475
|
+
such as `<agent>-<user>`. The pool names in the recorded paths were replaced
|
|
476
|
+
with placeholders after the run; the edge behavior for that path shape was
|
|
477
|
+
unaffected.
|
|
478
|
+
|
|
479
|
+
To configure the bypass, keep the Worker-level application described in
|
|
480
|
+
[Cloudflare Access on Workers](./auth.md#cloudflare-access-on-workers). Then,
|
|
481
|
+
for each hostname agents use (the `workers.dev` hostname, each Custom Domain,
|
|
482
|
+
or route host), add a self-hosted Access application. In the dashboard, set
|
|
483
|
+
domain to that hostname and path to `agent/*`, with one policy whose action is
|
|
484
|
+
**Bypass** and whose include rule is **Everyone**. Through the API, that is:
|
|
485
|
+
|
|
486
|
+
```json
|
|
487
|
+
{
|
|
488
|
+
"type": "self_hosted",
|
|
489
|
+
"name": "connecta agents",
|
|
490
|
+
"destinations": [{ "type": "public", "uri": "connecta.example.com/agent/*" }],
|
|
491
|
+
"policies": [{ "name": "agents bypass", "decision": "bypass", "include": [{ "everyone": {} }] }]
|
|
492
|
+
}
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
Only the `workers.dev` hostname was probed. A Custom Domain application is the
|
|
496
|
+
same documented hostname-and-path form, but it was not observed here. Do not
|
|
497
|
+
rely on a fixed wait after a change. Changes settled in 15–19 seconds, yet one
|
|
498
|
+
earlier round still saw a 403 on a bypassed path more than 30 seconds after
|
|
499
|
+
the change. Test only after the positive and negative controls agree together
|
|
500
|
+
for several consecutive rounds. The positive controls are bypassed paths
|
|
501
|
+
reaching the Worker with the path AUD and a service token reaching a protected
|
|
502
|
+
path with the Worker-level AUD; the negative controls are `/mcp` and `/`
|
|
503
|
+
returning the Access refusal. The current script requires five such rounds
|
|
504
|
+
after creating the bypass and three after other changes. The recorded run
|
|
505
|
+
predates this: after the bypass it converged only the positive `/agent/x`
|
|
506
|
+
control, then waited a fixed 90 seconds, and its negative controls were checked
|
|
507
|
+
in the recorded rounds rather than before them.
|
|
508
|
+
|
|
509
|
+
To repeat from a repository checkout, run
|
|
510
|
+
`node scripts/probes/access-agent-bypass.mjs --profile <name> [output.json]`.
|
|
511
|
+
`--profile` is required and has no default; every `cf` call passes it
|
|
512
|
+
explicitly. Name the profile for the account that should host the probe; it
|
|
513
|
+
needs
|
|
514
|
+
Access application, policy, and service-token writes, plus Workers script
|
|
515
|
+
writes. The script deploys and deletes the Worker with `cf`. `cf deploy` builds
|
|
516
|
+
through Wrangler 4.136.0 or newer, so the probe installs Wrangler 4.145.0 into
|
|
517
|
+
its temporary project. Wrangler needs no login. The script records only the
|
|
518
|
+
count of existing Access applications, then names everything it creates
|
|
519
|
+
`connecta-probe-<run>`. It force-deletes and verifies each resource in
|
|
520
|
+
`finally`, then checks that the non-probe application ids are unchanged.
|
|
521
|
+
Observations record statuses, outcome classes, pathnames, and which
|
|
522
|
+
application's AUD matched, compared by digest inside the Worker. They contain no
|
|
523
|
+
hostnames, account ids, tokens, cookies, or JWTs. `cf` delete commands prompt
|
|
524
|
+
for confirmation and exit 0 without deleting when stdin is not a terminal, so
|
|
525
|
+
the script passes `--force`. `cf deploy` printed `Deploy complete` and then
|
|
526
|
+
kept running, so the script stops it after that line. The recorded run deployed
|
|
527
|
+
with Wrangler 4.114.0 and used `cf` for Access. A standalone deploy and delete
|
|
528
|
+
verified the `cf` deploy and delete path. The full probe has not been rerun on
|
|
529
|
+
that path.
|
|
530
|
+
|
|
531
|
+
Every subprocess (`cf`, `curl`, `npm`) runs through one runner. It enforces a
|
|
532
|
+
deadline by killing the process group, keeps stdout and stderr in memory, and
|
|
533
|
+
returns only facts: success, exit code, timeout, HTTP status, numeric API
|
|
534
|
+
code, and an error slug from a fixed set (anything else is `unknown`). Commands
|
|
535
|
+
that return data are parsed inside the runner against schemas that copy only
|
|
536
|
+
whitelisted fields: ids, names, counts, booleans, and the Worker's `ctx.access`
|
|
537
|
+
summary. Credentials stay in a holder that prints as `[secret]`. An unexpected
|
|
538
|
+
shape, including `null` where a list belongs, is a failure, never an empty
|
|
539
|
+
result. Errors are fixed diagnostics plus those facts. Teardown deletes every
|
|
540
|
+
tracked id on its own. Then Access applications, service tokens, and Workers
|
|
541
|
+
each get their own bounded discovery-and-delete pass by name, for resources
|
|
542
|
+
whose create response was lost, so a failure in one family never skips
|
|
543
|
+
another. Each discovered resource is deleted independently. Access policies are
|
|
544
|
+
inline on their applications and go with them. A resource counts as gone only
|
|
545
|
+
when a successful lookup omits it, and the final inventory verifies each family
|
|
546
|
+
separately. Run
|
|
547
|
+
`node scripts/probes/access-agent-bypass.mjs --self-test` to check the runner
|
|
548
|
+
offline. It feeds canary secrets, suffixed slugs, malformed JSON, and a stalled
|
|
549
|
+
process tree through it. It also runs teardown against a fake `cf` whose
|
|
550
|
+
application listing always fails, after a lost token-create response, and
|
|
551
|
+
checks that the token is still found and deleted. It also checks that a Worker
|
|
552
|
+
that refuses deletion does not block another Worker's deletion. It runs no real
|
|
553
|
+
`cf`, `curl`, or network command. This
|
|
554
|
+
hardening and the joint convergence are verified offline only and have not run
|
|
555
|
+
against Cloudflare.
|
|
556
|
+
|
|
331
557
|
## Storage, credentials, and connectors
|
|
332
558
|
|
|
333
559
|
A deployment has one store. On Workers it is one D1 database
|
|
@@ -367,8 +593,8 @@ storage clock. Supply at most one option. Custom adapters must implement both;
|
|
|
367
593
|
a delayed absolute-expiry write must not restart a relative TTL at commit time. Each SQL claim
|
|
368
594
|
is one statement, which SQLite executes atomically and D1 serializes on its
|
|
369
595
|
primary. Everything that must claim a key exactly once relies on it with no
|
|
370
|
-
read-then-write fallback:
|
|
371
|
-
|
|
596
|
+
read-then-write fallback: agent-token slot swaps and
|
|
597
|
+
adoption, OAuth handoff ownership, single-use connect links, every downstream
|
|
372
598
|
OAuth grant write and consent claim, and the result stash.
|
|
373
599
|
|
|
374
600
|
Every key is built in `src/storage/keys.ts`, which lists each family with its
|
|
@@ -537,9 +763,9 @@ configuration, never runtime registration. `authScope: "shared" | "personal"`
|
|
|
537
763
|
partitions connecta-owned context — state, credentials, OAuth, catalogs, observed
|
|
538
764
|
shapes — by principal, and _only_ connecta-owned context: a secret a custom
|
|
539
765
|
handler closes over is shared JavaScript state, and `remoteMcp()` refuses the
|
|
540
|
-
literal-headers-plus-personal version of that mistake. Visibility
|
|
541
|
-
|
|
542
|
-
|
|
766
|
+
literal-headers-plus-personal version of that mistake. Visibility (maintainers
|
|
767
|
+
and pool `tools`) is a separate rule; hiding a connector does not change who
|
|
768
|
+
owns its auth.
|
|
543
769
|
|
|
544
770
|
### Errors and records
|
|
545
771
|
|
|
@@ -723,18 +949,26 @@ connection context `create` supplies, the maintained text, then deployment
|
|
|
723
949
|
instructions, which append and never replace. `src/provider.ts` imports neither
|
|
724
950
|
transport, so an `api()` provider gains no MCP client or Effect graph from it
|
|
725
951
|
(`test/purity.node.test.ts`). The factory carries its `definition`, which build and
|
|
726
|
-
check tools read instead of keeping provider lists.
|
|
727
|
-
live in their own folders.
|
|
952
|
+
check tools read instead of keeping provider lists. Providers
|
|
953
|
+
live in their own folders. Hosted implementations, including the
|
|
728
954
|
MCP branches of Notion, Vercel, and Cloudflare, use reviewed presets over
|
|
729
|
-
`remoteMcp({ classify })`. The
|
|
955
|
+
`remoteMcp({ classify })`. The migrated API-only factories retain an internal
|
|
730
956
|
adapter to the same definition and description-stamping path. GitHub composes hosted tools with a scope-enforced REST complement.
|
|
731
|
-
A `"dual"` provider
|
|
957
|
+
A `"dual"` provider selects one implementation at construction, usually through `auth`
|
|
732
958
|
([decision 0005](https://github.com/zackbart/connecta/blob/main/decisions/0005-auth-selects-implementation.md)):
|
|
733
959
|
`variants(["auth", "type"], …)` closes each case's options, and
|
|
734
960
|
`byAuth()` in `src/providers/_shared/rest/dispatch.ts` sends OAuth to
|
|
735
961
|
`hostedOAuth()` (`remoteMcp()`, OAuth only, the reviewed classification) and a
|
|
736
|
-
key to Connecta's REST connector. Stripe, Notion, Cloudflare, and Vercel
|
|
737
|
-
|
|
962
|
+
key to Connecta's REST connector. Stripe, Notion, Cloudflare, and Vercel require `auth`. Mixpanel is also dual,
|
|
963
|
+
with explicit `via: "mcp" | "api"` and MCP as the unchanged default: service accounts
|
|
964
|
+
reach both implementations, so auth cannot distinguish them. API mode requires
|
|
965
|
+
operator-managed service-account credentials and never falls back to MCP.
|
|
966
|
+
Mixpanel uses the same REST transport, operation matching and value-safety engine
|
|
967
|
+
for a narrow reviewed subset. Its official contracts are YAML and app identity
|
|
968
|
+
has no published response schema, so its guide and drift record carry explicit
|
|
969
|
+
manual review evidence instead of claiming a machine-checkable JSON pin.
|
|
970
|
+
|
|
971
|
+
The generic REST connector (`src/providers/_shared/rest/`) serves
|
|
738
972
|
`<vendor>_api_search`, `_api_details`, `_api_read`, and `_api_write` over an
|
|
739
973
|
`OperationIndex` that `npm run providers:spec` compiles offline from the
|
|
740
974
|
provider's pinned `openapi.source.json` into `openapi.generated.ts`.
|
|
@@ -812,8 +1046,90 @@ every vendor ships one reviewed table through one mechanism
|
|
|
812
1046
|
labelled or typed secret records lose their value, environment containers
|
|
813
1047
|
lose their values, header, cookie, and query rules lose theirs, external
|
|
814
1048
|
destinations keep their origin, and every URL loses userinfo and
|
|
815
|
-
credential-named query parameters or fragments
|
|
816
|
-
shared; each vendor states its own.
|
|
1049
|
+
credential-named query parameters or fragments under its URL policy.
|
|
1050
|
+
Exemptions are never shared; each vendor states its own. Every REST vendor
|
|
1051
|
+
reviews the pinned spec's free-text fields for embedded vendor-minted links
|
|
1052
|
+
and records its verdict and source limitations beside its value-safety table.
|
|
1053
|
+
Stripe, Vercel, and Cloudflare use **reviewed paths plus global signing
|
|
1054
|
+
parameters**. Literal AWS SigV4/Google V4 `X-Amz-`/`X-Goog-` Signature,
|
|
1055
|
+
Credential and Security-Token values are withheld across all decoded string
|
|
1056
|
+
values. Generic credential names (`code`, `key`, `token`, `signature`, `sig`,
|
|
1057
|
+
etc.) and credential-looking fragments apply only to reviewed diagnostic
|
|
1058
|
+
paths and vendor error text. Matching destinations lose their entire query
|
|
1059
|
+
and fragment. The tables list exact operations/resource paths with evidence:
|
|
1060
|
+
Stripe charge failure/outcome messages, Vercel deployment failures and
|
|
1061
|
+
build/runtime log text, Cloudflare Workers/Pages build logs and telemetry
|
|
1062
|
+
message/error and raw string/object source payloads in both events and
|
|
1063
|
+
invocation maps. The shared review harness rejects embedded-URL operation
|
|
1064
|
+
keys absent from each vendor's pinned index and invalid path syntax.
|
|
1065
|
+
Vercel's unavailable full pin remains a review limitation.
|
|
1066
|
+
Generic parameters in descriptions, metadata, nicknames, billing footers,
|
|
1067
|
+
notes, DNS TXT, KV values and operator HTML retain their original bytes,
|
|
1068
|
+
including whole URL strings. Ordinary code/key URLs can be false positives
|
|
1069
|
+
in reviewed diagnostics. Existing structured-field/resource rules, reviewed
|
|
1070
|
+
`url:` per-parameter rules, and payer-facing exceptions still apply.
|
|
1071
|
+
Scans read the original escaped text before URL parsing or serialization;
|
|
1072
|
+
adjacent HTML or Markdown is text even without whitespace. Relative Markdown
|
|
1073
|
+
destinations are checked at their own query/fragment, without re-anchoring
|
|
1074
|
+
a nested proxy URL. Ordinary proxy parameters retain their bytes.
|
|
1075
|
+
Scans precede success/select/named projections and error mapping. Named
|
|
1076
|
+
Vercel streams use the same paths before clamping. Binary responses encoded
|
|
1077
|
+
as base64, including Cloudflare Worker JavaScript served as
|
|
1078
|
+
`application/javascript`, are returned unchanged, without decoding/scanning.
|
|
1079
|
+
Cloudflare omits an envelope cursor that URL sanitizing would change;
|
|
1080
|
+
ordinary cursors retain their bytes. No vendor-wide host/bucket policy is
|
|
1081
|
+
inferred. Each string is scanned with bounded linear passes over disjoint
|
|
1082
|
+
destinations, including large inputs.
|
|
1083
|
+
These three vendors enforce write admission with the shared
|
|
1084
|
+
`carriesWithheldLink(value)` scanner and `refuseWithheldLinks`/
|
|
1085
|
+
`refuseWithheldUpload` helpers, plus `refuseWithheldBase64` for documented
|
|
1086
|
+
base64 JSON fields. Every supported write transport (JSON,
|
|
1087
|
+
form-encoded, raw/base64 bytes, multipart parts and named writes) refuses
|
|
1088
|
+
with `invalid_args` and zero dispatch when a URL destination's own query
|
|
1089
|
+
or fragment contains the exact `[redacted]` placeholder, plain or up to four
|
|
1090
|
+
layers encoded. Upload admission reads decoded bytes and each multipart
|
|
1091
|
+
string/blob. Vercel deployment `files[].data` with `encoding: "base64"`
|
|
1092
|
+
and Cloudflare KV bulk `value` with `base64: true` pass through the same
|
|
1093
|
+
byte inspection with one decode, bounded by the field's own encoded length
|
|
1094
|
+
(decoded bytes are at most three quarters of encoded characters). Cloudflare
|
|
1095
|
+
KV additionally enforces its documented 25 MiB decoded value limit;
|
|
1096
|
+
malformed base64 and values beyond that limit refuse with `invalid_args`.
|
|
1097
|
+
The host's inbound request cap still applies (Node `maxBodyBytes`, or the
|
|
1098
|
+
Workers platform); response and raw-upload limits do not bound JSON fields.
|
|
1099
|
+
JSON admission iteratively checks every key and value, with no separate
|
|
1100
|
+
depth or node-count ceiling below the accepted request's size. Cyclic
|
|
1101
|
+
direct arguments refuse with `invalid_args`; repeated non-cyclic references
|
|
1102
|
+
are inspected. The scans remain linear in the supplied and decoded bytes.
|
|
1103
|
+
Withheld values must not be written back; obtain the original
|
|
1104
|
+
link or omit the field. A placeholder outside a destination is ordinary
|
|
1105
|
+
text. Notion keeps its stricter reviewed-host admission and proxy policy.
|
|
1106
|
+
Vendors whose responses embed the file links they mint can
|
|
1107
|
+
use `withholdSignedLinks(text, hosts)` or
|
|
1108
|
+
`withholdSignedLinksIn(value, hosts)` with a reviewed `FileHosts` policy
|
|
1109
|
+
(`domains` and S3 `buckets`). Notion applies the latter in its `result`
|
|
1110
|
+
hook and before mapping errors, so Markdown, async task results, paging,
|
|
1111
|
+
named-tool results, and `select` all pass through it. The linear scan
|
|
1112
|
+
matches only the current URL/destination's actual authority: a reviewed
|
|
1113
|
+
domain or subdomain, or a reviewed S3 bucket, virtual-hosted or exactly
|
|
1114
|
+
the first path segment on an S3 endpoint. A secondary scheme or `//` in
|
|
1115
|
+
another URL's path or query cannot re-anchor it; those proxy links keep
|
|
1116
|
+
their bytes. Schemes and slashes may be escaped. File links lose their
|
|
1117
|
+
whole query and fragment from a literal, percent-, entity-, or
|
|
1118
|
+
`\u`-encoded opener through the next whitespace, quote, or angle bracket;
|
|
1119
|
+
trailing closing punctuation comes back. AWS SigV4 and Google V4
|
|
1120
|
+
`Signature`, `Credential`, and `Security-Token` parameter values are
|
|
1121
|
+
withheld wherever their literal signing names occur, at any host, with
|
|
1122
|
+
plain or encoded equals signs. `carriesWithheldLink(value, hosts)` is
|
|
1123
|
+
the shared write-admission helper: Notion refuses a write before dispatch
|
|
1124
|
+
when a file link's own query/fragment carries the exact `[redacted]`
|
|
1125
|
+
placeholder, plain or up to four layers encoded. Admission resumes at
|
|
1126
|
+
each actual Markdown destination's close, including adjacent links and
|
|
1127
|
+
images and balanced/escaped parentheses; leading spaces, tabs, and one
|
|
1128
|
+
line ending before the destination are recognized. A marker after the close is
|
|
1129
|
+
ordinary text. This boundary is independent of redaction's fail-closed
|
|
1130
|
+
span. Each string is scanned alone; split rich-text runs, encoded hosts
|
|
1131
|
+
or signing names, bare hosts without `//`, and userinfo before a reviewed
|
|
1132
|
+
host are outside this scan's threat model.
|
|
817
1133
|
4. **Errors.** A reviewed operation, or an unreviewed one whose request
|
|
818
1134
|
accepts a credential-named field, answers failures with the vendor's codes
|
|
819
1135
|
and status but not its text, unless its verdict is wrapped in
|
|
@@ -893,7 +1209,7 @@ regardless of wrapping.
|
|
|
893
1209
|
|
|
894
1210
|
`createConnecta` takes closed typed `ui`, `vault`, and `activity`
|
|
895
1211
|
slots, with factories at `/ui`, `/credentials`, and `/activity`
|
|
896
|
-
and
|
|
1212
|
+
and agent tokens at `/auth/access-tokens`. Root exports the contracts, never the implementations, and there
|
|
897
1213
|
is no module array, runtime registration, or plugin lifecycle. Core keeps
|
|
898
1214
|
discovery, the executor contract, invocation, permissions, and OAuth callback
|
|
899
1215
|
verification; an omitted module contributes no runtime work at all.
|
|
@@ -942,8 +1258,11 @@ admitted identity to a deployment-config `subject` function and mints a
|
|
|
942
1258
|
service-account token as whatever Workspace address it returns. The caller
|
|
943
1259
|
carries whether an inbound provider _authenticated_ it: an open deployment
|
|
944
1260
|
admits everyone as the anonymous actor, and the function is never asked about
|
|
945
|
-
them.
|
|
946
|
-
|
|
1261
|
+
them. It also carries the admitted pool's `owner`, which core copies from
|
|
1262
|
+
config: an agent acts as it, before and instead of the function, and an agent
|
|
1263
|
+
in a pool without one fails closed. A fixed `subject` address is every
|
|
1264
|
+
caller's. No caller, an unauthenticated one, or no address fails
|
|
1265
|
+
`auth_required` before any request leaves. The provider never chooses the account and a
|
|
947
1266
|
deployment's own connector still cannot read the caller (PRINCIPLES.md, INV-3; `test/google-workspace-delegation.test.ts`). Its tokens live in a
|
|
948
1267
|
bounded module-level map, in memory only, keyed by the key's own digest as well
|
|
949
1268
|
as the account, subject, and scopes. A mint in flight is shared across requests
|
|
@@ -1217,7 +1536,8 @@ compiling and configuring the real thing.
|
|
|
1217
1536
|
SIGTERM/SIGINT.
|
|
1218
1537
|
- **Structural mistakes throw at construction.** A duplicate connector id, an
|
|
1219
1538
|
invalid admission rule or limit, an unknown option at any depth, the old
|
|
1220
|
-
boolean `accessTokens` option,
|
|
1539
|
+
boolean `accessTokens` option, malformed or repeated `agents` or agents
|
|
1540
|
+
without `accessTokens`, a missing executor, or an executor without a
|
|
1221
1541
|
lifecycle brand:
|
|
1222
1542
|
all refuse to boot (`test/config.test.ts`, `test/registry.test.ts`). Starting
|
|
1223
1543
|
in the wrong shape is worse than not starting.
|