@zackbart/connecta 0.27.0 → 0.28.1
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/AGENTS.md +6 -6
- package/CHANGELOG.md +229 -0
- package/README.md +23 -24
- package/bin/connecta.mjs +1 -5
- package/dist/activity.d.ts +5 -4
- package/dist/activity.js +1 -10
- package/dist/artifacts/connector.js +1 -1
- package/dist/artifacts/json.js +10 -0
- package/dist/artifacts/operations.js +1 -1
- package/dist/artifacts/validate.js +1 -2
- package/dist/auth/downstream-oauth.js +147 -28
- package/dist/auth/static-oauth.js +504 -0
- package/dist/catalog-service.js +1 -1
- package/dist/connectors/api-connector.d.ts +168 -0
- package/dist/connectors/api-connector.js +127 -0
- package/dist/connectors/api.d.ts +8 -71
- package/dist/connectors/api.js +10 -99
- package/dist/connectors/guarded-fetch.js +37 -3
- package/dist/connectors/remote-mcp.js +7 -32
- package/dist/execute.js +92 -368
- package/dist/executors/quickjs.js +1 -1
- package/dist/exempt-writes.js +162 -0
- package/dist/index.d.ts +13 -32
- package/dist/index.js +14 -47
- package/dist/invocation.js +6 -19
- package/dist/meta-tools.js +1 -2
- package/dist/oauth-partition.js +17 -0
- package/dist/operator-ui/generated.js +1 -1
- package/dist/operator-ui/model.d.ts +3 -3
- package/dist/operator-ui/view.js +6 -4
- package/dist/providers/basecamp.d.ts +65 -0
- package/dist/providers/basecamp.js +445 -0
- package/dist/providers/breeze.d.ts +34 -0
- package/dist/providers/breeze.js +1402 -0
- package/dist/providers/ccb.d.ts +103 -0
- package/dist/providers/ccb.js +1570 -0
- package/dist/providers/cloudflare.js +1 -1
- package/dist/providers/linear.js +1 -1
- package/dist/providers/mixpanel.js +1 -1
- package/dist/providers/notion.js +1 -1
- package/dist/providers/overflow.d.ts +60 -0
- package/dist/providers/overflow.js +1572 -0
- package/dist/providers/planning-center.d.ts +59 -0
- package/dist/providers/planning-center.js +2039 -0
- package/dist/providers/revenuecat.js +1 -1
- package/dist/providers/stripe.js +1 -1
- package/dist/providers/tithely.d.ts +49 -0
- package/dist/providers/tithely.js +1034 -0
- package/dist/providers/vercel.js +1 -1
- package/dist/routes/mcp.js +5 -21
- package/dist/routes/oauth.js +29 -3
- package/dist/routes/shared.d.ts +1 -6
- package/dist/server.js +0 -4
- package/dist/skills.js +8 -23
- package/dist/tool-safety.d.ts +1 -1
- package/dist/tool-safety.js +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +15 -17
- package/documentation/auth.md +120 -6
- package/documentation/code-mode.md +91 -194
- package/documentation/meta-tools.md +38 -41
- package/ethos.md +8 -9
- package/examples/worker/README.md +10 -14
- package/examples/worker/src/d1-activity-row.ts +4 -1
- package/examples/worker/src/index.ts +1 -9
- package/package.json +26 -2
- package/templates/node/AGENTS.md +4 -3
- package/templates/node/README.md +3 -4
- package/templates/node/package.json +1 -1
- package/dist/resumable.js +0 -1323
- package/dist/run-journal.js +0 -347
package/dist/providers/vercel.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* those rows with Vercel's published OpenAPI document at
|
|
10
10
|
* https://openapi.vercel.sh/ without needing a credential.
|
|
11
11
|
*/
|
|
12
|
-
import { api, defined } from "../connectors/api.js";
|
|
12
|
+
import { apiConnector as api, defined } from "../connectors/api-connector.js";
|
|
13
13
|
import { remoteMcp } from "../connectors/remote-mcp.js";
|
|
14
14
|
import { vettedCatalog, withVettedCatalog } from "../catalog-drift.js";
|
|
15
15
|
import { guardedFetch, retryAfterMs, } from "../connectors/guarded-fetch.js";
|
package/dist/routes/mcp.js
CHANGED
|
@@ -4,9 +4,8 @@ import { notFoundResponse } from "../branding.js";
|
|
|
4
4
|
import { registerExecuteTool } from "../execute.js";
|
|
5
5
|
import { ExecutorAdmissionError, } from "../executor-admission.js";
|
|
6
6
|
import { registerMetaTools } from "../meta-tools.js";
|
|
7
|
-
import { registerResumeTool } from "../resumable.js";
|
|
8
7
|
import { intersectAccess } from "../connector-access.js";
|
|
9
|
-
import {
|
|
8
|
+
import { CONNECTA_INSTRUCTIONS } from "../skills.js";
|
|
10
9
|
import { msg } from "../errors.js";
|
|
11
10
|
import { detach } from "../runtime/run.js";
|
|
12
11
|
import { authorize, loggableValue, mayManageConnector, validateAuthPermissions, } from "./shared.js";
|
|
@@ -218,19 +217,14 @@ function toolkitRetired(logger) {
|
|
|
218
217
|
},
|
|
219
218
|
});
|
|
220
219
|
}
|
|
221
|
-
function serveMcp(request, requestSignal, opts, baseUrl, actor, registry, canManageAuth,
|
|
222
|
-
// A paused run belongs to the endpoint it started on: resuming it through
|
|
223
|
-
// another pool's endpoint finds nothing, as another subject would.
|
|
224
|
-
const resumable = opts.resumable
|
|
225
|
-
? { ...opts.resumable, pool: poolName ?? null, approval: opts.approval }
|
|
226
|
-
: undefined;
|
|
220
|
+
function serveMcp(request, requestSignal, opts, baseUrl, actor, registry, canManageAuth, runtimeContext) {
|
|
227
221
|
// Every McpServer the request builds is fresh and closes with its scope.
|
|
228
222
|
// The modern handler tears its own down after the exchange; the legacy
|
|
229
223
|
// transport never does, and neither may stay wired to an ended request.
|
|
230
224
|
const servers = [];
|
|
231
225
|
const createServer = () => {
|
|
232
226
|
const server = new McpServer(opts.serverInfo, {
|
|
233
|
-
instructions:
|
|
227
|
+
instructions: CONNECTA_INSTRUCTIONS,
|
|
234
228
|
cacheHints: {
|
|
235
229
|
"tools/list": {
|
|
236
230
|
ttlMs: 3_600_000,
|
|
@@ -274,7 +268,7 @@ function serveMcp(request, requestSignal, opts, baseUrl, actor, registry, canMan
|
|
|
274
268
|
: {}),
|
|
275
269
|
approval: opts.approval,
|
|
276
270
|
});
|
|
277
|
-
|
|
271
|
+
registerExecuteTool(server, registry, {
|
|
278
272
|
baseUrl,
|
|
279
273
|
executor: opts.executor,
|
|
280
274
|
logger: opts.logger,
|
|
@@ -304,19 +298,9 @@ function serveMcp(request, requestSignal, opts, baseUrl, actor, registry, canMan
|
|
|
304
298
|
...(opts.watchdogMs !== undefined
|
|
305
299
|
? { watchdogMs: opts.watchdogMs }
|
|
306
300
|
: {}),
|
|
307
|
-
resumable,
|
|
308
301
|
approval: opts.approval,
|
|
309
302
|
maxWrites: opts.maxWrites,
|
|
310
303
|
});
|
|
311
|
-
// Always registered, so the surface is the same eight tools on every
|
|
312
|
-
// deployment; without resumable writes it answers that there is nothing
|
|
313
|
-
// to resume.
|
|
314
|
-
registerResumeTool(server, registry, {
|
|
315
|
-
runner,
|
|
316
|
-
settings: resumable,
|
|
317
|
-
...(activity ? { activity } : {}),
|
|
318
|
-
requestSignal,
|
|
319
|
-
});
|
|
320
304
|
servers.push(server);
|
|
321
305
|
return server;
|
|
322
306
|
};
|
|
@@ -527,7 +511,7 @@ export function createMcpRoute(opts) {
|
|
|
527
511
|
if (new URL(request.url).searchParams.has("toolkit")) {
|
|
528
512
|
return cors(toolkitRetired(opts.logger));
|
|
529
513
|
}
|
|
530
|
-
return cors(yield* serveMcp(localRequest, localAbort.signal, opts, baseUrl, authz.actor, scopedRegistry, id => { const connector = scopedRegistry.getConnector(id); return Boolean(connector && mayManageConnector(authz, connector)); },
|
|
514
|
+
return cors(yield* serveMcp(localRequest, localAbort.signal, opts, baseUrl, authz.actor, scopedRegistry, id => { const connector = scopedRegistry.getConnector(id); return Boolean(connector && mayManageConnector(authz, connector)); }, runtimeContext));
|
|
531
515
|
});
|
|
532
516
|
if (remainingMs === undefined)
|
|
533
517
|
return yield* handled;
|
package/dist/routes/oauth.js
CHANGED
|
@@ -102,6 +102,30 @@ export function routeOAuthCallback(context) {
|
|
|
102
102
|
return Effect.succeed(null);
|
|
103
103
|
return Effect.uninterruptible(Effect.promise(() => finishOAuthCallback(context)));
|
|
104
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* RFC 6749's error codes (sections 4.1.2.1 and 5.2) and RFC 8707's
|
|
107
|
+
* `invalid_target`. Only a code on this list is logged: the `code` an OAuth
|
|
108
|
+
* error carries is the provider's own text.
|
|
109
|
+
*/
|
|
110
|
+
const TOKEN_ERROR_CODES = new Set([
|
|
111
|
+
"invalid_request",
|
|
112
|
+
"invalid_client",
|
|
113
|
+
"invalid_grant",
|
|
114
|
+
"unauthorized_client",
|
|
115
|
+
"unsupported_grant_type",
|
|
116
|
+
"invalid_scope",
|
|
117
|
+
"invalid_target",
|
|
118
|
+
"access_denied",
|
|
119
|
+
"server_error",
|
|
120
|
+
"temporarily_unavailable",
|
|
121
|
+
]);
|
|
122
|
+
/** ` with OAuth error <code>` for a known code; nothing for anything else. */
|
|
123
|
+
function exchangeErrorCode(err) {
|
|
124
|
+
const code = err?.code;
|
|
125
|
+
return typeof code === "string" && TOKEN_ERROR_CODES.has(code)
|
|
126
|
+
? ` with OAuth error ${code}`
|
|
127
|
+
: "";
|
|
128
|
+
}
|
|
105
129
|
async function finishOAuthCallback(context) {
|
|
106
130
|
const { path, url, baseUrl, opts } = context;
|
|
107
131
|
const error = url.searchParams.get("error");
|
|
@@ -198,10 +222,12 @@ async function finishOAuthCallback(context) {
|
|
|
198
222
|
return html("connected", opts, connector);
|
|
199
223
|
}
|
|
200
224
|
catch (err) {
|
|
201
|
-
//
|
|
202
|
-
//
|
|
225
|
+
// Neither the page nor the log repeats what the exchange threw: the SDK
|
|
226
|
+
// quotes the token endpoint's error_description or raw body, and a
|
|
227
|
+
// provider echoing a client_secret_post request puts the secret there.
|
|
203
228
|
opts.logger.warn(`[connecta] OAuth callback for connector ${loggableValue(id)} failed ` +
|
|
204
|
-
`with 500: the authorization code exchange
|
|
229
|
+
`with 500: the authorization code exchange failed${exchangeErrorCode(err)}. ` +
|
|
230
|
+
"Check the connector's client configuration and re-run authorization.");
|
|
205
231
|
return html("exchange_failed", opts, connector);
|
|
206
232
|
}
|
|
207
233
|
}
|
package/dist/routes/shared.d.ts
CHANGED
|
@@ -41,14 +41,9 @@ export interface ServerOptions {
|
|
|
41
41
|
hostCallTimeoutMs?: number | undefined;
|
|
42
42
|
/** Hard ceiling on one execute_code run, outside the sandbox. Default 120_000. */
|
|
43
43
|
watchdogMs?: number | undefined;
|
|
44
|
-
/** Resumable writes' settings; absent when they are off. */
|
|
45
|
-
resumable?: {
|
|
46
|
-
maxWrites: number;
|
|
47
|
-
ttlSeconds: number;
|
|
48
|
-
} | undefined;
|
|
49
44
|
/** Config approval exemptions (`execute.approval`), resolved at construction. */
|
|
50
45
|
approval?: ApprovalPolicy | undefined;
|
|
51
|
-
/**
|
|
46
|
+
/** Exempt writes one program may send (`execute.maxWrites`). */
|
|
52
47
|
maxWrites?: number | undefined;
|
|
53
48
|
/** Required sandbox backing the execute_code meta-tool. */
|
|
54
49
|
executor: Executor;
|
package/dist/server.js
CHANGED
|
@@ -52,10 +52,6 @@ export function createFetchHandler(opts) {
|
|
|
52
52
|
...(opts.executorName !== undefined
|
|
53
53
|
? { executor: { name: opts.executorName } }
|
|
54
54
|
: {}),
|
|
55
|
-
// Whether a program's write pauses for resume_execution or fails E4.
|
|
56
|
-
// The tool is listed either way; this is the only place an operator
|
|
57
|
-
// can see which answer it gives (#565).
|
|
58
|
-
resumableWrites: opts.resumable !== undefined,
|
|
59
55
|
// Counts only, from refreshes that already happened — the endpoint
|
|
60
56
|
// asks no downstream anything, and `connecta doctor` reads it to
|
|
61
57
|
// report a stale allowlist without a probe of its own (#343).
|
package/dist/skills.js
CHANGED
|
@@ -1,24 +1,17 @@
|
|
|
1
1
|
import { boundedEchoText } from "./errors.js";
|
|
2
|
-
const ROUTE = "Choose a route before discovery. A known-address read needs only call_tool. Unknown-address read-only work starts with execute_code to discover, call, and return the answer; use the same route for reduction, multiple or dependent calls, loops, joins, or branches. Keep discovery and calls together when schemas suffice; do not return catalog matches alone.
|
|
2
|
+
const ROUTE = "Choose a route before discovery. A known-address read needs only call_tool. Unknown-address read-only work starts with execute_code to discover, call, and return the answer; use the same route for reduction, multiple or dependent calls, loops, joins, or branches. Keep discovery and calls together when schemas suffice; do not return catalog matches alone. Sample unfamiliar reads.";
|
|
3
3
|
const RECOVERY = 'After auth_required use authorize_connector. After a truncated direct result use get_result. Guidance is on demand: fetch skills({ name: "usage" }) only when these instructions and the tool description are insufficient or a run needs repair.';
|
|
4
4
|
/**
|
|
5
|
-
* The MCP instructions
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* only that programs *may* write reads each write after a lookup as a known
|
|
9
|
-
* one and sends them one prompt at a time.
|
|
5
|
+
* The always-loaded MCP `instructions` string. A program runs reads and the
|
|
6
|
+
* writes config exempts from approval; every other write is its own top-level
|
|
7
|
+
* call_destructive_tool, where the host's permission prompt is the approval.
|
|
10
8
|
*/
|
|
11
|
-
export const CONNECTA_INSTRUCTIONS =
|
|
12
|
-
/**
|
|
13
|
-
* The instructions of a deployment without resumable writes, whose programs
|
|
14
|
-
* refuse every write: the routing it always had.
|
|
15
|
-
*/
|
|
16
|
-
export const READ_ONLY_PROGRAM_INSTRUCTIONS = `${ROUTE} Only readOnlyHint: true tools run there. Keep catalog inspection and unannotated, write-capable, or destructive work top level: search_tools then call_destructive_tool when a call is needed. ${RECOVERY}`;
|
|
9
|
+
export const CONNECTA_INSTRUCTIONS = `${ROUTE} Only readOnlyHint: true tools and config-exempt writes run there. Keep catalog inspection and other unannotated, write-capable, or destructive work top level: search_tools then call_destructive_tool when a call is needed. Never repeat a write to recover its output. ${RECOVERY}`;
|
|
17
10
|
const USAGE_SKILL_BASE = `# Connecta usage
|
|
18
11
|
|
|
19
12
|
## The surface
|
|
20
13
|
|
|
21
|
-
|
|
14
|
+
Seven tools: \`execute_code\`, \`search_tools\`, \`call_tool\`, \`call_destructive_tool\`, \`authorize_connector\`, \`get_result\`, \`skills\`. Discovery and multi-call work live in a program. Top-level search remains for catalog inspection and approval-required work.
|
|
22
15
|
|
|
23
16
|
Follow the MCP instructions for routing. Read at most once for syntax or repair.
|
|
24
17
|
|
|
@@ -48,16 +41,14 @@ For top-level catalog inspection or approval-required discovery, omit \`limit\`
|
|
|
48
41
|
- Preserve the schema's JSON types exactly: a numeric id is a number, not a numeric-looking string.
|
|
49
42
|
- Validate tabular headers, row arrays, and row widths before mapping them. Never let a header or partial row become data.
|
|
50
43
|
|
|
51
|
-
Only explicitly \`readOnlyHint: true\`
|
|
44
|
+
Only explicitly \`readOnlyHint: true\` calls and writes the deployment's config exempts from approval run in a program. Any other write is refused before it is sent: send it through top-level \`call_destructive_tool\`, where the host asks. Sandbox code cannot widen host gates.
|
|
52
45
|
|
|
53
46
|
## Errors and repair
|
|
54
47
|
|
|
55
48
|
Caught Connecta errors expose \`message\`, \`code\`, \`retryable\`, and \`details\`. Promise rejections retain these fields. Branch on fields, never prose. After a shared argument failure, repair one call before repeating it across other records. Do not retry \`retryable: false\`, and do not retry \`rate_limited\` immediately because portable code has no timer.
|
|
56
49
|
|
|
57
50
|
- \`destructive_tool_requires_approval\`: stop and send the returned address through top-level \`call_destructive_tool\`.
|
|
58
|
-
- \`
|
|
59
|
-
- \`write_outcome_unknown\`: sent but unanswered, never re-sent; check the target.
|
|
60
|
-
- \`execution_expired\`, \`_diverged\`, \`_interrupted\`: check \`writes\`, then rerun.
|
|
51
|
+
- \`write_outcome_unknown\`: an exempt write was sent but unanswered and is never re-sent; check the target.
|
|
61
52
|
- \`auth_required\`: let the failure reach the model, then use top-level \`authorize_connector\`, give its handoff to the operator, and retry after recovery.
|
|
62
53
|
- Truncated direct call: page with \`get_result\`, never repeat a write. A program result has no page handle; reduce a read in a new program. After a write, check the target or report the gap.
|
|
63
54
|
- Unknown addresses and tools carry scoped search recovery. Use it inside the current run. Do not invent an address.
|
|
@@ -72,8 +63,6 @@ The \`execute_code\` description states this deployment's host-call and write bu
|
|
|
72
63
|
|
|
73
64
|
Portable code uses standard JavaScript builtins, \`connecta\`, and \`console.*\`. QuickJS blocks imports and lacks fetch, process, timers, crypto, and WebSocket. Dynamic Workers must use only \`{ loader }\`; bindings, modules, or globalOutbound grant ambient authority. With loader only, environment maps are empty; node:fs/http/https are absent; outbound fetch, WebSocket, node:net, and node:tls are denied; DNS is unresolved. Runtime builtins remain through \`import()\` and \`process.getBuiltinModule()\`, including node:path and cloudflare:workers; this set can drift. Timers, process, crypto, WebSocket, and data: fetch remain. Avoid every runtime-only capability because QuickJS fails.
|
|
74
65
|
|
|
75
|
-
\`Date.now()\` is the run's start and does not advance; \`Math.random()\` is seeded per run.
|
|
76
|
-
|
|
77
66
|
## Examples
|
|
78
67
|
|
|
79
68
|
One read-only call at a known address:
|
|
@@ -113,10 +102,6 @@ Connector guides appear in \`skills({})\` and discovery with an exact \`guide\`
|
|
|
113
102
|
`;
|
|
114
103
|
/** Shared Connecta routing guidance, byte-identical across deployments. */
|
|
115
104
|
export const USAGE_SKILL = USAGE_SKILL_BASE + CONNECTOR_GUIDES_SECTION;
|
|
116
|
-
/** The always-loaded MCP `instructions` string. */
|
|
117
|
-
export function instructionsFor(resumableWrites) {
|
|
118
|
-
return resumableWrites ? CONNECTA_INSTRUCTIONS : READ_ONLY_PROGRAM_INSTRUCTIONS;
|
|
119
|
-
}
|
|
120
105
|
/** True when at least one of `connectors` carries a usage guide. */
|
|
121
106
|
export function hasConnectorGuides(connectors) {
|
|
122
107
|
return connectors.some((connector) => connectorGuide(connector) !== undefined);
|
package/dist/tool-safety.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export interface ApprovalPolicy {
|
|
|
20
20
|
}
|
|
21
21
|
export declare const NO_EXEMPTIONS: ApprovalPolicy;
|
|
22
22
|
/**
|
|
23
|
-
* Whether a program may call this tool without
|
|
23
|
+
* Whether a program may call this tool without asking for approval.
|
|
24
24
|
*
|
|
25
25
|
* Only a tool that is *not* explicitly read-only can be exempt — a read-only
|
|
26
26
|
* tool needs no exemption, and reporting one would blur the two classes an
|
package/dist/tool-safety.js
CHANGED
|
@@ -14,7 +14,7 @@ export const NO_EXEMPTIONS = {
|
|
|
14
14
|
tools: new Map(),
|
|
15
15
|
};
|
|
16
16
|
/**
|
|
17
|
-
* Whether a program may call this tool without
|
|
17
|
+
* Whether a program may call this tool without asking for approval.
|
|
18
18
|
*
|
|
19
19
|
* Only a tool that is *not* explicitly read-only can be exempt — a read-only
|
|
20
20
|
* tool needs no exemption, and reporting one would blur the two classes an
|
package/dist/types.d.ts
CHANGED
|
@@ -253,8 +253,8 @@ export interface Connector {
|
|
|
253
253
|
/**
|
|
254
254
|
* This connector's own default for its tools that are not explicitly
|
|
255
255
|
* read-only, inside `execute_code`: `"never"` lets a program call them
|
|
256
|
-
* without
|
|
257
|
-
* whose writes are cheap to undo (the
|
|
256
|
+
* without asking for approval. Reserved for connectors connecta ships
|
|
257
|
+
* whose writes are cheap to undo (the artifacts connector, whose
|
|
258
258
|
* every write is a new immutable version); a deployment's own connectors
|
|
259
259
|
* leave it unset and exempt through `execute.approval`, which overrides
|
|
260
260
|
* this either way. Never read from a downstream catalog — a server cannot
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
|
@@ -21,8 +21,10 @@ health, and the per-connector call limiters. It is built once and lives as long
|
|
|
21
21
|
as the isolate — on Workers a lazy module-scope singleton, which is why both
|
|
22
22
|
deployment shapes construct it outside the request handler.
|
|
23
23
|
|
|
24
|
-
An OAuth `remoteMcp()
|
|
25
|
-
|
|
24
|
+
An OAuth connector — `remoteMcp()`, or `api()` with a static grant
|
|
25
|
+
(`src/auth/static-oauth.ts`, the same provider with its discovery and client
|
|
26
|
+
answered from config) — also owns a runtime-local refresh completion gate
|
|
27
|
+
(`src/auth/downstream-oauth.ts`). It coordinates credential mutation across
|
|
26
28
|
concurrent request scopes while sharing no client, transport, or response, and
|
|
27
29
|
never lets a follower cancel the owner. The subtle part is that a valid token
|
|
28
30
|
response consumes the refresh token whether or not the owner survives to save it,
|
|
@@ -61,7 +63,7 @@ top to bottom.
|
|
|
61
63
|
| 2 | MCP preflight | Allowed `OPTIONS` on `/mcp*`: 204 without admission or auth. |
|
|
62
64
|
| 2 | Other `OPTIONS` | Auth metadata first, otherwise compatibility CORS preflight. |
|
|
63
65
|
| 3 | `/.well-known/*` | Auth metadata, or 404. |
|
|
64
|
-
| 4 | `/health` | Open and payload-free: health, executor,
|
|
66
|
+
| 4 | `/health` | Open and payload-free: health, executor, admission, and deployment metadata, with drift as stable short hashes. |
|
|
65
67
|
| 5 | `/oauth/callback/<connectorId>` | Core downstream OAuth completion, state and personal-ownership checked, independent of the UI. |
|
|
66
68
|
| 6 | `/mcp`, `/mcp/<pool>` | Admission, then auth, then a request-local MCP server. An undeclared pool, a refusing grant, and a throwing grant are one identical 404; see [pools](./auth.md#pools). |
|
|
67
69
|
| 7 | Other paths | 404. Custom HTTP routes belong to the deployment. |
|
|
@@ -94,7 +96,7 @@ An admitted non-preflight `/mcp` request then takes five steps in
|
|
|
94
96
|
5. **Serve.** Refuse `?toolkit=` with a 404 — the toolkits are gone
|
|
95
97
|
([#178](https://github.com/zackbart/connecta/issues/178)) but their URLs were
|
|
96
98
|
handed out, and retiring a scoping boundary into fail-open is worse than any
|
|
97
|
-
404 — then register the
|
|
99
|
+
404 — then register the seven meta-tools on a fresh `McpServer`
|
|
98
100
|
(`test/server.test.ts`, `test/code-first-surface.test.ts`).
|
|
99
101
|
|
|
100
102
|
## Layers below the meta-tools
|
|
@@ -109,7 +111,7 @@ or hands out, and a change usually belongs in exactly one of them:
|
|
|
109
111
|
| `src/invocation.ts` | One tool call: argument validation, call admission, one-attempt timeout, provider retry hints, result unwrapping, size capping, and the activity record. |
|
|
110
112
|
| `src/catalog.ts` | Ranking, description summarizing, and the compact and TypeScript schema renderers discovery shows. |
|
|
111
113
|
| `src/result-shapes.ts` | Bounded runtime-only inference and merging for output shapes learned from successful read-only calls whose providers declared none. |
|
|
112
|
-
| `src/
|
|
114
|
+
| `src/exempt-writes.ts` | The writes a program may send, which config exempted from asking: the write budget's default, what a dispatched write's outcome says about whether it landed, and the tracking that lets one outlive the program without hiding an unknown outcome. Every other program write is refused before it reaches this. |
|
|
113
115
|
|
|
114
116
|
`src/meta-tools.ts` and `src/execute.ts` are two front doors onto the same two
|
|
115
117
|
services, `CatalogService` and `InvocationService`. That is the point: a
|
|
@@ -222,12 +224,10 @@ successful call's preview and a paging-unavailable notice rather than a result i
|
|
|
222
224
|
A second optional method, `compareAndSet(key, expected, next, options?)`, is an
|
|
223
225
|
atomic claim: `null` means absent (expired counts) on the way in and delete on
|
|
224
226
|
the way out. A successful write accepts the same optional `ttlSeconds` as
|
|
225
|
-
`set`.
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
exactly when the store has it, and asking for it over a store without it
|
|
230
|
-
refuses to construct. Downstream OAuth uses it where the store has it, so
|
|
227
|
+
`set`. Artifacts require it, because every write commits by swapping a head,
|
|
228
|
+
and so does issuing access tokens; a read and a write standing in for the
|
|
229
|
+
claim would let two writers both win. Downstream OAuth uses it where the store
|
|
230
|
+
has it, so
|
|
231
231
|
resealing legacy plaintext and discarding a refused grant cannot overwrite a
|
|
232
232
|
consent that landed in between, and falls back to a read and a write where it
|
|
233
233
|
does not.
|
|
@@ -330,7 +330,7 @@ owner admitted to `set_refresh` is stored with the program. Every run rechecks
|
|
|
330
330
|
that identity's current `connectorAccess` and pool grant, including continued
|
|
331
331
|
access to `artifacts.set_refresh`, then intersects those grants with shared
|
|
332
332
|
connectors. Revoking either grant stops later runs.
|
|
333
|
-
|
|
333
|
+
Approval exemptions are off, so a refresh writes nothing. A refused call fails the
|
|
334
334
|
entire refresh even if its program catches the error. The head CAS claims one
|
|
335
335
|
run per artifact; the data commit checks the claim ID and program version in
|
|
336
336
|
the same CAS. The claim deadline starts before executor admission and aborts
|
|
@@ -483,7 +483,6 @@ would be another partition's.
|
|
|
483
483
|
| Admission (`executor-admission.ts`, `call-admission.ts`) | Queued waiters are Deferreds settled by whoever removes them from the queue; a wait is one flat race of grant, Clock timeout, and signal. The uncontended path stays synchronous. Two controllers, as [#453](https://github.com/zackbart/connecta/issues/453) requires. |
|
|
484
484
|
| One tool call (`invocation.ts`) | One fiber: resolution, the read-only and schema refusals, admission, and the downstream attempt sit under a single `withDeadlineEffect`, whose expiry interrupts the call wherever it is. The permit is an `acquireRelease`; the connector call is `Effect.tryPromise` over the unchanged `Connector`. |
|
|
485
485
|
| `execute_code` (`execute.ts`) | One fiber whose Scope owns the run's signal and executor lease, so a result, a throw, the watchdog, and cancellation all release the lease and abort the signal the same way. The executor's `acquire()` and `execute()` stay Promises raced against the signal and `execute.watchdogMs`. Each guest host call is a fiber of its own. |
|
|
486
|
-
| Resumable writes (`resumable.ts`) | A play's stop is a Deferred its write gate completes, raced against the executor like the watchdog; the gate for call n awaits the decision Deferreds of every lower-numbered call; header writes are one compare-and-set at a time on a per-play turn. A paused run holds nothing request-bound — its journal is data, and the next play is a new request's. |
|
|
487
486
|
| Discovery (`catalog-service.ts`) | A request-scoped cache: one shared read per connector (`runtime/shared-read.ts`), settled by the read itself and carrying its own signal and the probe timeout whichever asker starts it. Each asker waits under its own deadline and signal, so one that times out or is cancelled fails alone, and the read is cancelled only once every asker has gone. Fan-out is `Effect.forEach` under the discovery concurrency. |
|
|
488
487
|
| Registry (`registry.ts`) | Catalog persistence and the result stash are programs over `Storage`. A refresh flight is a Deferred its publishing request completes, bounded by its owner's deadline (the default probe timeout when it has none); persisted-catalog writes take per-connector turns, each a Deferred its own request completes. Same-request loads share one read the way discovery's do. |
|
|
489
488
|
| Remote MCP (`connectors/remote-mcp.ts`) | Each request scope's state holds a Scope, each connection is a lease forked from it, and a connect in flight is a Deferred carrying the client it connected. Closing a session and the transport are each bounded to a second. |
|
|
@@ -589,9 +588,8 @@ cap. Effect can reject excess properties when both decoding and rendering use
|
|
|
589
588
|
`onExcessProperty: "error"`; its default strips them. The generated schemas
|
|
590
589
|
still differ from the exact meta-tool goldens, including record rendering and
|
|
591
590
|
composed numeric bounds. A Schema replacement would need to preserve that
|
|
592
|
-
contract
|
|
593
|
-
|
|
594
|
-
does not remove Zod from an installation.
|
|
591
|
+
contract and memoized rendering. Both MCP SDK packages still depend on Zod,
|
|
592
|
+
so changing our inputs alone does not remove Zod from an installation.
|
|
595
593
|
|
|
596
594
|
A one-endpoint `HttpApi` prototype adds 106,634 bytes gzip to `./ui` and 106,453
|
|
597
595
|
to `./activity`. Even plain `HttpRouter` adds 45,324 to `./ui`, taking it to
|
|
@@ -631,7 +629,7 @@ src/
|
|
|
631
629
|
skills.ts MCP instructions, the usage skill, connector guides
|
|
632
630
|
catalog-drift.ts vetted manifests and the counts a refresh produces
|
|
633
631
|
activity.ts optional history factory and best-effort recorder
|
|
634
|
-
auth/ bearer, Cloudflare Access, clerk (optional peer), downstream OAuth
|
|
632
|
+
auth/ bearer, Cloudflare Access, clerk (optional peer), downstream OAuth (remote and static)
|
|
635
633
|
executors/ the QuickJS pool and child (Node only)
|
|
636
634
|
node.ts listen() + fileStorage re-export (Node only)
|
|
637
635
|
```
|
package/documentation/auth.md
CHANGED
|
@@ -305,7 +305,9 @@ or epoch does not open. Anything that fails to open — a tampered value, a
|
|
|
305
305
|
rotated key — reads as absent: the connector reports `auth_required` and logs a
|
|
306
306
|
warning. The flow bookkeeping stays plaintext: `state`, the pending URL,
|
|
307
307
|
discovery metadata, and the generation. The callback route reads `state`
|
|
308
|
-
directly, and none of it authenticates anything by itself.
|
|
308
|
+
directly, and none of it authenticates anything by itself. An
|
|
309
|
+
[`api()` OAuth connector](#downstream-oauth-on-api) seals its tokens and
|
|
310
|
+
verifier the same way and stores no client at all.
|
|
309
311
|
|
|
310
312
|
Plaintext left by an older release is read, then sealed where it lies through
|
|
311
313
|
the same generation fence as any write, so an upgrade keeps the grant. Sealing
|
|
@@ -326,8 +328,8 @@ The operator's `POST /ui/oauth/<id>` takes a `mode` query parameter:
|
|
|
326
328
|
|
|
327
329
|
| Request | What it does |
|
|
328
330
|
| --- | --- |
|
|
329
|
-
| `POST /ui/oauth/<id>` or `?mode=restart` | `startAuth(ctx, { force: true })`: a new epoch, with tokens,
|
|
330
|
-
| `?mode=continue` | `startAuth(ctx, { force: false })`: hand back the pending authorization URL if it was written in the last ten minutes; otherwise start a flow in the current epoch, reusing the stored client registration. A disconnected connector still gets its new epoch first. |
|
|
331
|
+
| `POST /ui/oauth/<id>` or `?mode=restart` | `startAuth(ctx, { force: true })`: a new epoch, with tokens, discovery, and any pending flow wiped. A dynamically registered client may be carried into it ([below](#human-authentication-management)); one that is not carried is registered again. |
|
|
332
|
+
| `?mode=continue` | `startAuth(ctx, { force: false })`: hand back the pending authorization URL if it was written in the last ten minutes and still names the stored client; otherwise start a flow in the current epoch, reusing the stored client registration. A disconnected connector still gets its new epoch first. |
|
|
331
333
|
| `DELETE /ui/oauth/<id>` | Disconnect: a disconnected epoch that passive reads never turn back into a consent flow. |
|
|
332
334
|
|
|
333
335
|
Any other `mode`, or more than one, is a 400 before anything starts; the
|
|
@@ -592,9 +594,22 @@ the connector's URL, redirect URI, client metadata, auth scope, transport settin
|
|
|
592
594
|
and owner partition still match. It drops the grant and one-shot flow state,
|
|
593
595
|
selects the authorization server again, and re-seals the retained client under
|
|
594
596
|
the replacement epoch key. A different issuer registers a new client. Disconnect
|
|
595
|
-
and issuer-mismatch recovery discard it.
|
|
596
|
-
|
|
597
|
-
|
|
597
|
+
and issuer-mismatch recovery discard it. A URL-based client is never carried:
|
|
598
|
+
nothing was registered, and leaving it behind lets fresh metadata decide
|
|
599
|
+
whether the server still accepts one. Neither is a client whose secret has
|
|
600
|
+
expired.
|
|
601
|
+
|
|
602
|
+
A carried client has to earn its next carry. Building a consent URL sends the
|
|
603
|
+
provider nothing, so a restart cannot learn there that a provider has purged
|
|
604
|
+
the client, and RFC 6749 forbids the provider from redirecting an unknown
|
|
605
|
+
client back to the callback, so nothing arrives later either. Tokens in the
|
|
606
|
+
epoch are the only proof the provider still knows it. A restart that follows
|
|
607
|
+
a restart with no grant in between therefore registers again: a purged
|
|
608
|
+
registration costs one refused consent, not a Restart that can never recover.
|
|
609
|
+
A refusal connecta does hear is handled where it lands. A start whose refresh
|
|
610
|
+
draws `invalid_client` drops the client and registers in that same start; a
|
|
611
|
+
callback whose code exchange draws it drops the client, and Continue will not
|
|
612
|
+
hand back the URL that named it.
|
|
598
613
|
|
|
599
614
|
Credential and OAuth mutation require an admitted interactive human, connector
|
|
600
615
|
visibility, the appropriate shared or personal permission, and an exact
|
|
@@ -636,8 +651,107 @@ existing dynamic registration flow. State validation, PKCE, issuer binding,
|
|
|
636
651
|
encrypted token storage, refresh, and disconnect follow the same code paths.
|
|
637
652
|
The metadata URL and scopes participate in the saved-client configuration
|
|
638
653
|
binding, so restarting after either changes cannot reuse an old registration.
|
|
654
|
+
A restart never carries the URL-based client itself, so a server that stops
|
|
655
|
+
advertising support gets a registered client on the next restart.
|
|
639
656
|
|
|
640
657
|
`scope` supplies space-separated default OAuth scopes through client metadata.
|
|
641
658
|
A downstream challenge or protected-resource scope declaration takes precedence,
|
|
642
659
|
and the SDK adds `offline_access` when advertised for refresh-token grants.
|
|
643
660
|
Omitting both settings preserves the existing discovery and registration flow.
|
|
661
|
+
|
|
662
|
+
## Downstream OAuth on `api()`
|
|
663
|
+
|
|
664
|
+
Some APIs a deployment needs have no MCP server and take nothing but OAuth.
|
|
665
|
+
Church Community Builder's REST API accepts only a three-legged grant, issues
|
|
666
|
+
its clients by hand, and publishes no metadata to discover. `api()` takes a
|
|
667
|
+
static authorization-code configuration for that shape:
|
|
668
|
+
|
|
669
|
+
```ts
|
|
670
|
+
api("church", {
|
|
671
|
+
authScope: "personal",
|
|
672
|
+
oauth: {
|
|
673
|
+
authorizationEndpoint: "https://login.example.com/oauth/authorize",
|
|
674
|
+
tokenEndpoint: "https://api.example.com/oauth/token",
|
|
675
|
+
clientId: env.CHURCH_CLIENT_ID,
|
|
676
|
+
clientSecret: env.CHURCH_CLIENT_SECRET,
|
|
677
|
+
scope: "people:read",
|
|
678
|
+
apiOrigins: ["https://api.example.com"],
|
|
679
|
+
},
|
|
680
|
+
tools: [{
|
|
681
|
+
name: "get_person",
|
|
682
|
+
description: "Read one person",
|
|
683
|
+
annotations: { readOnlyHint: true },
|
|
684
|
+
inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
|
|
685
|
+
handler: async ({ id }, ctx) => {
|
|
686
|
+
const response = await ctx.oauth!.fetch(
|
|
687
|
+
`https://api.example.com/people/${encodeURIComponent(id)}`,
|
|
688
|
+
);
|
|
689
|
+
return response.json();
|
|
690
|
+
},
|
|
691
|
+
}],
|
|
692
|
+
});
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
Nothing is discovered, registered, or learned. Every URL is configuration, so
|
|
696
|
+
the rule for [URLs a downstream advertises](#urls-a-downstream-advertises)
|
|
697
|
+
has nothing to check, and the endpoints are checked once, at construction:
|
|
698
|
+
HTTPS (HTTP only on loopback), no credentials, no fragment. PKCE with S256 is
|
|
699
|
+
on unless `pkce: false`, which drops the challenge from the consent URL and
|
|
700
|
+
the verifier from the exchange, for a server that refuses them.
|
|
701
|
+
`authorizationParams` adds provider parameters but cannot restate the grant's
|
|
702
|
+
own, and `tokenRequestHeaders` adds headers to the code exchange and every
|
|
703
|
+
refresh — CCB's token endpoint wants its own `Accept` media type — but cannot
|
|
704
|
+
set `Authorization`, `Content-Type`, `Content-Length`, `Cookie`, or `Host`. `tokenEndpointAuthMethod` defaults to `client_secret_basic` with a secret
|
|
705
|
+
and `none` without; the mismatched pairings refuse to boot. A static server has
|
|
706
|
+
no advertised issuer, so no RFC 9207 `iss` is demanded and no RFC 8707
|
|
707
|
+
`resource` is sent. The grant is bound to the token endpoint instead:
|
|
708
|
+
repointing it fences the old tokens behind a new epoch rather than sending
|
|
709
|
+
them to the new server.
|
|
710
|
+
|
|
711
|
+
**The client lives in deployment configuration, not a vault slot.** It is the
|
|
712
|
+
deployment's identity at the provider — one per deployment, like Clerk's
|
|
713
|
+
`secretKey` — while a vault slot belongs to an owner: on a personal connector
|
|
714
|
+
every human would paste the deployment's secret into their own partition.
|
|
715
|
+
Configuration also keeps it out of storage altogether. It is never written,
|
|
716
|
+
sealed or otherwise, so a leaked store holds no client secret, Disconnect has
|
|
717
|
+
nothing of it to delete, and Restart has no registration to carry forward.
|
|
718
|
+
Read it from the environment or a Worker secret; an empty string, the usual
|
|
719
|
+
unset variable, refuses to boot without quoting it.
|
|
720
|
+
|
|
721
|
+
Handlers never see the grant. `ctx.oauth.fetch(url, init)` sends the calling
|
|
722
|
+
owner's access token as `Authorization: Bearer` and does four things a
|
|
723
|
+
hand-rolled header would not:
|
|
724
|
+
|
|
725
|
+
- **It sends the token to `apiOrigins` and nowhere else.** A request to any
|
|
726
|
+
other origin is refused before it leaves, so an untrusted URL in a response
|
|
727
|
+
— a pagination link, a webhook target — cannot carry the token off.
|
|
728
|
+
Redirects come back unfollowed, and a handler cannot set `Authorization`
|
|
729
|
+
itself.
|
|
730
|
+
- **A 401 earns exactly one recovery.** If another request has already
|
|
731
|
+
rotated a token in, that token is used; otherwise one refresh runs through
|
|
732
|
+
the same coordinator as `remoteMcp()`, coalesced across requests, with the
|
|
733
|
+
rotating refresh token persisted even if its owner is cancelled after the
|
|
734
|
+
answer arrives. The request is then replayed once, which is why a stream
|
|
735
|
+
body is refused.
|
|
736
|
+
- **Failures land in the existing classes.** No grant, a second 401, or a
|
|
737
|
+
[dead refresh](#refresh-failures), whose refused tokens are deleted on the
|
|
738
|
+
spot, is `auth_required` and routes the agent to `authorize_connector`. An
|
|
739
|
+
authorization-server outage is a retryable `unavailable` that keeps the
|
|
740
|
+
grant. A second 401 is latched for the rest of the request scope, so a
|
|
741
|
+
program's next fifty calls do not spend fifty refreshes on it.
|
|
742
|
+
- **It reads and writes nothing a handler can name.** Storage, sealing, and the
|
|
743
|
+
owner partition are the registry's, exactly as for `remoteMcp()`.
|
|
744
|
+
|
|
745
|
+
Everything else is the `remoteMcp()` grant, unchanged: the epoch fence and its
|
|
746
|
+
cleanup lineage, sealing under the vault key, shared and personal ownership,
|
|
747
|
+
the callback route and its state and principal checks, `authorize_connector`,
|
|
748
|
+
and the operator's Connect, Restart, and Disconnect. Status reports a stored
|
|
749
|
+
grant as healthy without asking the downstream — failing at use is enough —
|
|
750
|
+
and never starts authorization. One difference is deliberate: the first start
|
|
751
|
+
publishes a modern epoch at once. The legacy generation exists so a grant from
|
|
752
|
+
before epochs survives an upgrade, and an `api()` grant has no such past.
|
|
753
|
+
|
|
754
|
+
`oauth` and `credential` are exclusive on one connector, so `auth_required`
|
|
755
|
+
names one recovery. A provider that offers both a personal access token and
|
|
756
|
+
OAuth, as Planning Center does, lets the deployment choose which to pass;
|
|
757
|
+
handlers branch on whether `ctx.oauth` is present.
|