@zackbart/connecta 0.41.0 → 0.42.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 +61 -0
- package/bin/connecta.mjs +26 -17
- package/bin/meta-tool-names.d.mts +3 -1
- package/bin/meta-tool-names.mjs +4 -1
- package/bin/version.mjs +1 -1
- package/dist/activity.d.ts +2 -1
- package/dist/activity.js +5 -5
- package/dist/advertised-schema.js +15 -0
- package/dist/agent-routes.js +31 -5
- package/dist/auth/downstream-oauth/refresh-coordinator.js +4 -1
- package/dist/auth/downstream-oauth.js +3 -0
- package/dist/auth/static-oauth.js +26 -18
- package/dist/auth-elicitation.js +118 -61
- package/dist/authorization-handoff.js +36 -3
- package/dist/catalog-cache.js +292 -24
- package/dist/catalog-freshness.js +12 -0
- package/dist/catalog-service.js +373 -70
- package/dist/catalog.js +147 -58
- package/dist/config-defaults.d.ts +2 -1
- package/dist/config-defaults.js +4 -1
- package/dist/config.d.ts +43 -17
- package/dist/config.js +26 -10
- package/dist/connectors/remote-mcp/catalog.js +52 -28
- package/dist/connectors/remote-mcp/connection.js +62 -5
- package/dist/connectors/remote-mcp.js +10 -1
- package/dist/describe-config.d.ts +3 -2
- package/dist/discovery-health.js +26 -11
- package/dist/errors.d.ts +52 -3
- package/dist/errors.js +86 -3
- package/dist/execute.js +476 -200
- package/dist/executor-admission.js +2 -0
- package/dist/executor-contract.js +6 -0
- package/dist/executor-result.js +2 -2
- package/dist/executors/quickjs-child.js +13 -4
- package/dist/executors/quickjs-runtime.d.ts +2 -0
- package/dist/executors/quickjs-runtime.js +36 -19
- package/dist/executors/quickjs.js +23 -6
- package/dist/guest-functions.js +5 -4
- package/dist/guest-runtime.js +37 -1
- package/dist/guest-types.d.ts +18 -0
- package/dist/http/client.js +7 -2
- package/dist/http/errors.js +9 -4
- package/dist/http/fields.js +17 -5
- package/dist/http/hatch.js +9 -3
- package/dist/http/paging.js +1 -1
- package/dist/http/provider.js +2 -6
- package/dist/http/vendor.js +6 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +5 -0
- package/dist/invocation.js +98 -34
- package/dist/meta-output.js +24 -18
- package/dist/meta-tool-names.d.ts +1 -1
- package/dist/meta-tool-names.js +1 -1
- package/dist/meta-tools.js +259 -60
- package/dist/native-recovery.js +4 -1
- package/dist/native-tools.js +166 -51
- package/dist/node.js +3 -1
- package/dist/oauth-handoff.js +53 -43
- package/dist/operation-activity.js +2 -1
- package/dist/operation-vocabulary.js +6 -0
- package/dist/operator-record.d.ts +1 -1
- package/dist/operator-record.js +1 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/program-result.js +2 -0
- package/dist/program-source.js +1 -1
- package/dist/program-writes.js +112 -19
- package/dist/provider.d.ts +0 -13
- package/dist/provider.js +0 -3
- package/dist/providers/_shared/google/kit.js +2 -2
- package/dist/providers/breeze/api.js +2 -2
- package/dist/providers/breeze/skill.generated.js +1 -1
- package/dist/providers/ccb/api.js +2 -2
- package/dist/providers/ccb/skill.generated.js +1 -1
- package/dist/providers/cloudflare/api.js +2 -2
- package/dist/providers/cloudflare/index.js +2 -0
- package/dist/providers/cloudflare/skill.generated.js +1 -1
- package/dist/providers/docs/skill.generated.js +1 -1
- package/dist/providers/drive/skill.generated.js +1 -1
- package/dist/providers/forms/index.js +1 -0
- package/dist/providers/forms/skill.generated.js +1 -1
- package/dist/providers/github/api.js +98 -6
- package/dist/providers/github/index.js +7 -2
- package/dist/providers/github/lib/auth.d.ts +3 -1
- package/dist/providers/github/lib/auth.js +43 -12
- package/dist/providers/github/lib/discovery.js +11 -1
- package/dist/providers/github/lib/operations.js +8 -0
- package/dist/providers/github/safety.js +39 -0
- package/dist/providers/github/skill.generated.js +1 -1
- package/dist/providers/github/tools.js +12 -5
- package/dist/providers/gmail/index.js +1 -0
- package/dist/providers/gmail/skill.generated.js +1 -1
- package/dist/providers/granola/skill.generated.js +1 -1
- package/dist/providers/overflow/index.js +2 -0
- package/dist/providers/overflow/skill.generated.js +1 -1
- package/dist/providers/planning-center/index.js +11 -5
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/planning-center/skill.generated.js +1 -1
- package/dist/providers/quickbooks/index.js +2 -5
- package/dist/providers/quickbooks/skill.generated.js +1 -1
- package/dist/providers/sheets/index.js +1 -0
- package/dist/providers/sheets/skill.generated.js +1 -1
- package/dist/providers/slides/index.js +2 -2
- package/dist/providers/slides/skill.generated.js +1 -1
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/tithely/index.js +1 -0
- package/dist/providers/tithely/skill.generated.js +1 -1
- package/dist/providers/vercel/api.js +2 -2
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/registry.d.ts +36 -4
- package/dist/registry.js +127 -27
- package/dist/result-preview.js +210 -0
- package/dist/result-seal.js +168 -0
- package/dist/route-names.js +13 -4
- package/dist/routes/connect.js +21 -13
- package/dist/routes/mcp.js +55 -26
- package/dist/routes/shared.d.ts +6 -0
- package/dist/runtime/call.js +15 -10
- package/dist/runtime/request-scope.js +36 -1
- package/dist/runtime/run.js +4 -1
- package/dist/sent-secrets.d.ts +20 -0
- package/dist/sent-secrets.js +49 -1
- package/dist/skills.js +100 -33
- package/dist/storage/keys.js +9 -6
- package/dist/storage/sql-native.js +1 -1
- package/dist/tool-safety.d.ts +33 -0
- package/dist/tool-safety.js +51 -15
- package/dist/types.d.ts +31 -0
- package/dist/usage-guide.js +14 -13
- package/dist/validate.js +29 -10
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +28 -8
- package/documentation/architecture.md +104 -42
- package/documentation/auth.md +46 -19
- package/documentation/code-mode.md +378 -116
- package/documentation/deploying.md +10 -9
- package/documentation/hosted-mcp-migration.md +9 -3
- package/documentation/integrating.md +27 -5
- package/documentation/meta-tools.md +655 -153
- package/documentation/operating.md +5 -3
- package/documentation/providers.md +18 -6
- package/examples/worker/README.md +3 -3
- package/guides/how-it-works.md +2 -1
- package/guides/safety-and-trust.md +1 -3
- package/package.json +1 -1
- package/templates/node/AGENTS.md +3 -3
- package/templates/node/README.md +1 -1
- package/templates/node/package.json +1 -1
package/dist/validate.js
CHANGED
|
@@ -190,7 +190,7 @@ function receivedType(value) {
|
|
|
190
190
|
* No caller values are used to invent an example, and a writeOnly position or
|
|
191
191
|
* anything inside one never renders a schema literal. A candidate must validate.
|
|
192
192
|
*/
|
|
193
|
-
function exampleCandidate(schema, root, depth = 0, budget = { remaining: 128, active: new Set() }, secret = false) {
|
|
193
|
+
function exampleCandidate(schema, root, depth = 0, budget = { remaining: 128, active: new Set(), placeholders: false }, secret = false, key) {
|
|
194
194
|
if (--budget.remaining < 0)
|
|
195
195
|
throw new Error("Example synthesis budget exhausted");
|
|
196
196
|
if (depth > 12 || schema === false)
|
|
@@ -224,7 +224,7 @@ function exampleCandidate(schema, root, depth = 0, budget = { remaining: 128, ac
|
|
|
224
224
|
candidate = Object.fromEntries(required
|
|
225
225
|
.filter((key) => typeof key === "string")
|
|
226
226
|
.slice(0, 30)
|
|
227
|
-
.map((key) => [key, exampleCandidate(properties[key], root, depth + 1, budget, secret)]));
|
|
227
|
+
.map((key) => [key, exampleCandidate(properties[key], root, depth + 1, budget, secret, key)]));
|
|
228
228
|
if (Array.isArray(node.allOf)) {
|
|
229
229
|
for (const branch of node.allOf.slice(0, 20)) {
|
|
230
230
|
candidate = mergeExample(candidate, exampleCandidate(branch, root, depth + 1, budget, secret));
|
|
@@ -258,7 +258,7 @@ function exampleCandidate(schema, root, depth = 0, budget = { remaining: 128, ac
|
|
|
258
258
|
// Branches may declare required keys while their types live on the parent.
|
|
259
259
|
for (const key of Object.keys(schemaObject(candidate))) {
|
|
260
260
|
if (schemaObject(candidate)[key] === undefined) {
|
|
261
|
-
schemaObject(candidate)[key] = exampleCandidate(properties[key], root, depth + 1, budget, secret);
|
|
261
|
+
schemaObject(candidate)[key] = exampleCandidate(properties[key], root, depth + 1, budget, secret, key);
|
|
262
262
|
}
|
|
263
263
|
}
|
|
264
264
|
}
|
|
@@ -290,10 +290,11 @@ function exampleCandidate(schema, root, depth = 0, budget = { remaining: 128, ac
|
|
|
290
290
|
uri: "https://example.com",
|
|
291
291
|
uuid: "00000000-0000-4000-8000-000000000000",
|
|
292
292
|
};
|
|
293
|
+
const named = budget.placeholders && !secret && key !== undefined ? namedPlaceholder(key, node) : undefined;
|
|
293
294
|
candidate =
|
|
294
295
|
typeof node.format === "string" && formats[node.format]
|
|
295
296
|
? formats[node.format]
|
|
296
|
-
: "x".repeat(Math.min(typeof node.minLength === "number" ? node.minLength : 0, 128));
|
|
297
|
+
: (named ?? "x".repeat(Math.min(typeof node.minLength === "number" ? node.minLength : 0, 128)));
|
|
297
298
|
}
|
|
298
299
|
return candidate;
|
|
299
300
|
}
|
|
@@ -301,6 +302,19 @@ function exampleCandidate(schema, root, depth = 0, budget = { remaining: 128, ac
|
|
|
301
302
|
budget.active.delete(node);
|
|
302
303
|
}
|
|
303
304
|
}
|
|
305
|
+
/**
|
|
306
|
+
* `<id>` for a plain string argument: an empty string would satisfy local
|
|
307
|
+
* validation and reach the downstream looking like a real value. A secret
|
|
308
|
+
* position, an odd key, and a length bound it misses keep the plain sample.
|
|
309
|
+
*/
|
|
310
|
+
function namedPlaceholder(key, node) {
|
|
311
|
+
if (!/^[\w.-]{1,64}$/.test(key))
|
|
312
|
+
return undefined;
|
|
313
|
+
const text = `<${key}>`;
|
|
314
|
+
const min = typeof node.minLength === "number" ? node.minLength : 0;
|
|
315
|
+
const max = typeof node.maxLength === "number" ? node.maxLength : Infinity;
|
|
316
|
+
return text.length >= min && text.length <= max ? text : undefined;
|
|
317
|
+
}
|
|
304
318
|
function mergeExample(a, b) {
|
|
305
319
|
if (a !== null &&
|
|
306
320
|
b !== null &&
|
|
@@ -501,12 +515,17 @@ function validationIssues(schema, args, units) {
|
|
|
501
515
|
export function schemaExample(schema) {
|
|
502
516
|
try {
|
|
503
517
|
const validator = validators.get(schema) ?? new Validator(schema, "2020-12", false);
|
|
504
|
-
//
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
518
|
+
// Named placeholders first; a schema that refuses them, such as one with a
|
|
519
|
+
// pattern, falls back to the plain sample rather than losing its example.
|
|
520
|
+
for (const placeholders of [true, false]) {
|
|
521
|
+
const budget = { remaining: 128, active: new Set(), placeholders };
|
|
522
|
+
// Serialization makes the validation cover exactly what the agent receives.
|
|
523
|
+
const example = JSON.parse(JSON.stringify(exampleCandidate(schema, schema, 0, budget)));
|
|
524
|
+
if (validator.validate(example).valid) {
|
|
525
|
+
minimizeExample(example, validator);
|
|
526
|
+
if (validator.validate(example).valid)
|
|
527
|
+
return { example };
|
|
528
|
+
}
|
|
510
529
|
}
|
|
511
530
|
}
|
|
512
531
|
catch {
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CONNECTA_VERSION = "0.
|
|
1
|
+
export declare const CONNECTA_VERSION = "0.42.0";
|
package/dist/version.js
CHANGED
package/dist/worker.js
CHANGED
|
@@ -7,6 +7,7 @@ import { awaitExecutorLease, provideExecutorProgram, executorProgram } from "./r
|
|
|
7
7
|
import { DynamicWorkerExecutor, normalizeCode } from "@cloudflare/codemode";
|
|
8
8
|
import { AdmissionController, ExecutorExecutionError } from "./executor-admission.js";
|
|
9
9
|
import { brandExecutor } from "./executor-contract.js";
|
|
10
|
+
import { TOKEN_END } from "./errors.js";
|
|
10
11
|
import { InvocationFailure } from "./invocation.js";
|
|
11
12
|
import { parse } from "acorn";
|
|
12
13
|
import { guestInitializer, guestPrelude, guestPromiseInitializer, isolateGuestProgram } from "./guest-runtime.js";
|
|
@@ -43,15 +44,25 @@ function routeGuestImports(source) {
|
|
|
43
44
|
}
|
|
44
45
|
return source;
|
|
45
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* The guest deadline when `timeout` is unset: below `execute.watchdogMs`, and
|
|
49
|
+
* so below the 60 s MCP clients give one request. Upstream's own default is
|
|
50
|
+
* that 60 s, which races them.
|
|
51
|
+
*/
|
|
52
|
+
const DEFAULT_TIMEOUT_MS = 50_000;
|
|
53
|
+
const DEFAULT_QUEUE_TIMEOUT_MS = 40_000;
|
|
46
54
|
/** Loader-only upstream sandbox whose lease disposes handles at run end. */
|
|
47
55
|
export function workerExecutor(options) {
|
|
48
|
-
|
|
56
|
+
const timeoutMs = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
57
|
+
// One pool per executor, so per isolate. A queued run may wait most of a
|
|
49
58
|
// default guest deadline: programs spend it awaiting host calls, and a
|
|
50
|
-
// five-second wait refused nearly every run queued behind one.
|
|
59
|
+
// five-second wait refused nearly every run queued behind one. The wait
|
|
60
|
+
// spends the request's response ceiling, so it stops short enough to leave
|
|
61
|
+
// the run time to work and the refusal time to reach the client.
|
|
51
62
|
const admission = new AdmissionController(options.admission ?? {
|
|
52
63
|
concurrency: 2,
|
|
53
64
|
maxQueueSize: 8,
|
|
54
|
-
queueTimeoutMs:
|
|
65
|
+
queueTimeoutMs: DEFAULT_QUEUE_TIMEOUT_MS,
|
|
55
66
|
retryAfterMs: 1_000,
|
|
56
67
|
});
|
|
57
68
|
const active = new Set();
|
|
@@ -177,19 +188,27 @@ export function workerExecutor(options) {
|
|
|
177
188
|
isolated = isolated.replace("setTimeout(() => reject(timeoutError)", "nativeSetTimeout(() => reject(timeoutError)");
|
|
178
189
|
isolated = isolated.replace("return { result: undefined, error: err.message, logs: __logs };", `
|
|
179
190
|
const failureId = lookup(failures, err);
|
|
191
|
+
// tokenPrefix, for text the host redacts after this cut.
|
|
192
|
+
const tokenEnd = (unit) => ${[...TOKEN_END].map((unit) => `unit === ${unit}`).join(" || ")};
|
|
193
|
+
const prefix = (text, units) => {
|
|
194
|
+
if (units >= text.length) return text;
|
|
195
|
+
let end = units;
|
|
196
|
+
while (end > 0 && !tokenEnd(charCodeAt(text, end))) end--;
|
|
197
|
+
return slice(text, 0, end);
|
|
198
|
+
};
|
|
180
199
|
let name = "Error", message = "Program threw a value.", stack = "";
|
|
181
200
|
try {
|
|
182
|
-
if (typeof err === "string") message =
|
|
201
|
+
if (typeof err === "string") message = prefix(err, 1000);
|
|
183
202
|
else if (err) {
|
|
184
|
-
if (typeof err.name === "string") name =
|
|
185
|
-
if (typeof err.message === "string") message =
|
|
203
|
+
if (typeof err.name === "string") name = prefix(err.name, 64);
|
|
204
|
+
if (typeof err.message === "string") message = prefix(err.message, 1000);
|
|
186
205
|
if (typeof err.stack === "string") stack = slice(err.stack, 0, 1000);
|
|
187
206
|
}
|
|
188
207
|
} catch {}
|
|
189
208
|
const location = exec(/connecta-guest\\.js:(\\d+):\\d+/, stack);
|
|
190
209
|
return { result: undefined, error: message, logs: __logs, failure: {
|
|
191
210
|
name, ...(failureId ? { failureId } : {}), ...(location ? { line: toNumber(location[1]) - 1 } : {}),
|
|
192
|
-
...(err === timeoutError ? { timeout: { elapsedMs: ${
|
|
211
|
+
...(err === timeoutError ? { timeout: { elapsedMs: ${timeoutMs}, deadlineMs: ${timeoutMs} } } : {})
|
|
193
212
|
} };`);
|
|
194
213
|
const preludes = [];
|
|
195
214
|
let firstInitializer;
|
|
@@ -362,7 +381,7 @@ const {
|
|
|
362
381
|
try: () => {
|
|
363
382
|
const upstream = new DynamicWorkerExecutor({
|
|
364
383
|
loader,
|
|
365
|
-
|
|
384
|
+
timeout: timeoutMs,
|
|
366
385
|
}).execute(code, detached);
|
|
367
386
|
// The SDK's own result, which settles only after endUpstream(),
|
|
368
387
|
// carries nothing; the native evaluation below owns the outcome.
|
|
@@ -463,6 +482,7 @@ const {
|
|
|
463
482
|
const acquireShell = (request) => runEdge(acquire(request));
|
|
464
483
|
const executor = {
|
|
465
484
|
name: "DynamicWorkerExecutor",
|
|
485
|
+
deadlineMs: timeoutMs,
|
|
466
486
|
acquire: acquireShell,
|
|
467
487
|
execute: (code, providers) => runEdge(execute(code, providers)),
|
|
468
488
|
admissionSnapshot: () => admission.snapshot(),
|
|
@@ -240,7 +240,7 @@ An admitted non-preflight `/mcp` request then takes five steps in
|
|
|
240
240
|
5. **Serve.** Refuse `?toolkit=` with a 404 — the toolkits are gone
|
|
241
241
|
([#178](https://github.com/zackbart/connecta/issues/178)) but their URLs were
|
|
242
242
|
handed out, and retiring a scoping boundary into fail-open is worse than any
|
|
243
|
-
404 — then register the
|
|
243
|
+
404 — then register the meta-tools on a fresh `McpServer`
|
|
244
244
|
(`test/server.test.ts`).
|
|
245
245
|
|
|
246
246
|
### Modern request metadata and transport refusals
|
|
@@ -358,6 +358,13 @@ reader. `program-result.ts` owns executor-independent byte limits;
|
|
|
358
358
|
values through provider preludes. Cursors hold only an ID and offset for the
|
|
359
359
|
current run. Result bytes, expiry, authority and capacity charges stay in the
|
|
360
360
|
existing storage and ledger; no executor-specific store or storage key is added.
|
|
361
|
+
`result-seal.ts` keeps a run's results unreadable to other requests until they pass
|
|
362
|
+
its final secret set ([`R4a`](./code-mode.md#results-and-projection)): chunk 0 waits in a
|
|
363
|
+
request-owned ledger, and absence of chunk 0 is the refusal, so every exit leaves
|
|
364
|
+
unpublished entries unreadable with no further write. Sealing waits for the work that can
|
|
365
|
+
add to the set (`HeldResults.track` for the run's host calls, `RequestOwner.credentialReads`
|
|
366
|
+
for credential reads, which outlast a caller that stopped waiting), then publishes each
|
|
367
|
+
entry that passes. Stored bytes are never rewritten.
|
|
361
368
|
|
|
362
369
|
## Admission, in two places
|
|
363
370
|
|
|
@@ -749,8 +756,8 @@ the search returns, never raced or detached, since a storage write cannot be
|
|
|
749
756
|
withdrawn once sent. The budget bounds when health work starts, not when it
|
|
750
757
|
finishes, so slow storage can carry a search past it by one operation's
|
|
751
758
|
latency. A scoped search,
|
|
752
|
-
`connecta.describe
|
|
753
|
-
`
|
|
759
|
+
`connecta.describe` and tool calls keep the full `discovery.probeTimeoutMs`,
|
|
760
|
+
as does a native `tools/list` for a connector with no stale catalog. A probe that times out on its own deadline, or a
|
|
754
761
|
retryable failure that itself took at least one second or named its own wait,
|
|
755
762
|
writes a record in the `connector-health:v1:` family: the connecta
|
|
756
763
|
classification code, observation time, retry time (60 seconds later, or the
|
|
@@ -800,7 +807,56 @@ retain a complete private fallback, and static catalogs remain
|
|
|
800
807
|
in configuration. After a transient first-page tool listing failure, ordinary calls
|
|
801
808
|
may use a complete catalog for up to five minutes after its expiry in the same
|
|
802
809
|
auth partition. Partial walks, permission and auth failures do not use this
|
|
803
|
-
fallback
|
|
810
|
+
fallback, and a refusal keeps later failures from using it until a listing succeeds.
|
|
811
|
+
Resources require unexpired inventories.
|
|
812
|
+
|
|
813
|
+
**Stale listings.** Discovery alone may list a tool catalog up to 24 hours past
|
|
814
|
+
its expiry (`STALE_DISCOVERY_MS`), so chunks and manifests keep that storage TTL
|
|
815
|
+
and a publish deletes the revision it supersedes. Each discovery first peeks the
|
|
816
|
+
entry without a connection (`Connector.peekCatalog`, or the retained custom
|
|
817
|
+
catalog once its listing has failed): storage reads only, through the listing's
|
|
818
|
+
address, fingerprint, generation and credential fences (INV-5, INV-8, INV-10). A
|
|
819
|
+
peek takes no probe slot, and a listing that follows it in the request takes what
|
|
820
|
+
it read, so a resolver still runs once and the entry is read once. A current peek
|
|
821
|
+
skips the probe queue. A stale one stands in for a probe that did not answer, on
|
|
822
|
+
the failures [meta-tools](./meta-tools.md#lexical-discovery) lists, marked in host-only
|
|
823
|
+
provenance (`markCatalogStale`). It stays out of the request's retained catalogs,
|
|
824
|
+
so `resolve` and every call read the live one. A probe that exhausts its own
|
|
825
|
+
deadline hands one refresh to `defer`: a listing in a new request scope, with its
|
|
826
|
+
own signal and transport, closed in that scope and settling the health record it
|
|
827
|
+
leased. Nothing of the original request reaches it (INV-7). One clock, started as
|
|
828
|
+
it is scheduled, bounds its whole tail at 20 seconds, inside Workers' `waitUntil`:
|
|
829
|
+
queueing and the claim spend it, the listing gets what remains less a second for
|
|
830
|
+
settlement and two for cleanup, and a claim that spends it all dispatches nothing.
|
|
831
|
+
Only the request whose settlement wrote the health record takes that refresh;
|
|
832
|
+
settlement and the claim are compare-and-set against the record the request read,
|
|
833
|
+
including creating an absent one, so concurrent failures start one refresh.
|
|
834
|
+
|
|
835
|
+
A peek read before a wait is not served after it. Once a probe or a slot has kept
|
|
836
|
+
the request waiting, the entry is read again through every fence, so an invalidation
|
|
837
|
+
or a refusal in the meantime retires it, in this isolate or another.
|
|
838
|
+
|
|
839
|
+
**Refusal fences.** `auth_required`, `downstream_oauth_required` or
|
|
840
|
+
`provider_permission_denied` from a listing or its handshake writes one key
|
|
841
|
+
(`fenceTools`) in the namespace of the catalog's current generation, addressed by
|
|
842
|
+
the owner partition and credential identity, or by the auth partition alone on a
|
|
843
|
+
shared connector, whose credential gets one answer whoever asks. A tool listing that
|
|
844
|
+
sees it misses, fresh or stale, so `peekTools` and the five-minute fallback have
|
|
845
|
+
nothing to serve; both also read it fresh before serving an expired entry. A listing
|
|
846
|
+
that succeeds lifts by compare-and-set only the fence it first saw, so a refusal
|
|
847
|
+
that lands while it runs stays. The key outlives every entry it guards
|
|
848
|
+
(`CATALOG_FENCE_TTL_SECONDS`) and the generation's rotation discards it with them.
|
|
849
|
+
It adds no round trip to a read: it travels in the catalog snapshot or the manifest
|
|
850
|
+
read. Timeouts and retryable failures write nothing here, and neither does a
|
|
851
|
+
caller-local refusal repeated within one request.
|
|
852
|
+
|
|
853
|
+
The 24-hour limit stays because of these. Everything that can end a caller's access
|
|
854
|
+
and that Connecta can observe (a refusal, a credential change, a disconnect, an
|
|
855
|
+
invalidation) now retires the catalog at once. What remains is a revocation behind a
|
|
856
|
+
connector that has not answered since: tool names and schemas that caller was
|
|
857
|
+
already shown, marked stale, granting nothing, for at most a day. The first answer
|
|
858
|
+
ends it.
|
|
859
|
+
|
|
804
860
|
Host-only provenance carries each catalog's original fetch time, TTL and expiry,
|
|
805
861
|
and whether it was a fallback, through classification and request-local copies.
|
|
806
862
|
Zero TTL and stale fallbacks cannot authorize post-entry auth recovery.
|
|
@@ -808,8 +864,8 @@ Zero TTL and stale fallbacks cannot authorize post-entry auth recovery.
|
|
|
808
864
|
A manifest is published by CAS only after every UTF-8 chunk is written. It
|
|
809
865
|
records the original fetch time, absolute expiry, revision, fingerprint, and
|
|
810
866
|
monotonic stamp. Readers verify all chunks and fingerprint before accepting it.
|
|
811
|
-
Freshness expires at the bounded TTL; tool facts remain for the
|
|
812
|
-
|
|
867
|
+
Freshness expires at the bounded TTL; tool facts remain for the 24-hour stale
|
|
868
|
+
listing limit, of which calls may use five minutes, and generations expire after 48 hours; invalidation
|
|
813
869
|
rotates the generation, and old chunks expire without becoming reachable
|
|
814
870
|
again. Remote clients pin that generation before listing, including SDK cache
|
|
815
871
|
refreshes. Each new listing pins the current generation, so a live client can
|
|
@@ -1352,15 +1408,15 @@ guide (G) violations before writing. It derives `drift.json` version 2's
|
|
|
1352
1408
|
`generated` block from kit operations and evidence digests, and the guide's
|
|
1353
1409
|
`## Conventions` from definition declarations into `skill.generated.ts`.
|
|
1354
1410
|
The generator requires `fixtures.ts` to export `createAuthored()` with a
|
|
1355
|
-
nonempty offline `staticTools` catalog
|
|
1356
|
-
|
|
1357
|
-
tools
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
reports a nonzero explicit refusal count with singular/plural
|
|
1362
|
-
points to the actual prefixed details tool with method/path lookup;
|
|
1363
|
-
embeds the refusal table. Unreviewed operations remain refused.
|
|
1411
|
+
nonempty offline `staticTools` catalog, which the installed inventory lists.
|
|
1412
|
+
Conventions render only provider-specific declarations: the vendor envelope
|
|
1413
|
+
root, the hatch's tools and refusal count, the `idempotencyKey` rule where a
|
|
1414
|
+
provider has one, and the Errors line. Paging, `fields`, withheld-value and
|
|
1415
|
+
write-once rules live at the agent's point of use, and
|
|
1416
|
+
`test/provider-conventions.test.ts` asserts each paged tool's continuation there.
|
|
1417
|
+
The Hatch line reports a nonzero explicit refusal count with singular/plural
|
|
1418
|
+
wording and points to the actual prefixed details tool with method/path lookup;
|
|
1419
|
+
it never embeds the refusal table. Unreviewed operations remain refused.
|
|
1364
1420
|
`ProviderContext.usageGuide` selects a guide's `<!-- when: <key> -->` blocks
|
|
1365
1421
|
with the definition's `blocks`. Maintained drift records use version 2;
|
|
1366
1422
|
`providers:check` retains version-1 endpoint/manual record parsing for historical
|
|
@@ -1720,9 +1776,13 @@ a table.
|
|
|
1720
1776
|
Remote MCP tools classify from server annotations and deployment policy.
|
|
1721
1777
|
Only `readOnlyHint === true` with `destructiveHint !== true` establishes a
|
|
1722
1778
|
source read; missing or contradictory hints mean write. Deployment overrides
|
|
1723
|
-
in `ConnectaConfig.classification` choose effective `read`, `write`, or `hidden
|
|
1724
|
-
|
|
1725
|
-
|
|
1779
|
+
in `ConnectaConfig.classification` choose effective `read`, `write`, or `hidden`,
|
|
1780
|
+
except that `read` cannot change a tool the source explicitly declares a write:
|
|
1781
|
+
a host may resend a read-annotated call, so that tool stays a write unless the
|
|
1782
|
+
deployment accepts that with `read-repeatable`. They cannot
|
|
1783
|
+
relax source replay safety. Effective or source write makes a call
|
|
1784
|
+
write-like on every resend and recovery path. Source facts, including whether a
|
|
1785
|
+
source write was declared or only silent, travel in
|
|
1726
1786
|
private metadata with host-owned tool clones and stay out of persisted catalogs.
|
|
1727
1787
|
A leftover `classify` throws and names `ConnectaConfig.classification`.
|
|
1728
1788
|
There are no hosted review maps, pins, drift comparisons or review UI fields.
|
|
@@ -2013,18 +2073,18 @@ storage is the root's namespaced to its principal.
|
|
|
2013
2073
|
|
|
2014
2074
|
### What runs on Effect
|
|
2015
2075
|
|
|
2016
|
-
| Area | Shape
|
|
2017
|
-
| ---------------------------------------------------------------------- |
|
|
2018
|
-
| Requests (`src/server.ts`, `src/routes/mcp.ts`, `src/routes/oauth.ts`) | One fiber per `fetch`, tied to `request.signal`. An `/mcp` request's admission permit and every `McpServer` it builds live in a Scope the response carries out, closed when the body ends, fails, or is cancelled, when the signal aborts, when its admitted-request lifetime ends in that request, or at once if the handler fails first. If workerd runs none of those callbacks, the next request reclaims only the expired admission record; it cannot close another request's Scope. The OAuth callback is uninterruptible: a single-use code's exchange and catalog invalidation are one commitment.
|
|
2019
|
-
| 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.
|
|
2020
|
-
| 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`.
|
|
2021
|
-
| `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. Public executor methods stay Promise-shaped; registered built-in execution composes directly as an Effect under the signal and `execute.watchdogMs`. Queued acquisition retains its Promise request handoff. Each guest host call is a fiber of its own.
|
|
2022
|
-
| 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; unscoped search
|
|
2023
|
-
| Registry (`registry.ts`) | The result stash is a program over `Storage`. Same-request catalog loads share one read the way discovery's do. Remote MCP caching uses the SDK ResponseCacheStore adapter over SQL-backed KV, with no cross-request catalog flight.
|
|
2024
|
-
| Remote MCP (`connectors/remote-mcp.ts`, `connectors/remote-mcp/`) | 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.
|
|
2025
|
-
| Downstream OAuth (`auth/downstream-oauth*`, `auth/static-oauth.ts`) | A refresh flight is a Deferred. Before the first durable coordinator write (claim CAS) is submitted, call/request closure stops preparation without a CAS, token request or state change. Submission transfers renewal to the storage coordinator under its own 35-second preparation deadline and 20-second token-request/body deadline, followed by the fenced rotation commit through the runtime deferred-work hook. A closed caller promptly fails without consuming the result, marking its ledger renewed or sending/resending a resource request.
|
|
2026
|
-
| Operator and activity data (`routes/operator.ts`) | Each JSON route is one program run by `serveOperator` behind the Promise `handle()`. Reads run under the request's signal; writes do not, so a vault write or OAuth disconnect that started reaches its cache invalidation.
|
|
2027
|
-
| QuickJS pool (`executors/quickjs.ts`, Node only) | Each child is a scoped resource whose release sends SIGTERM, then SIGKILL after a second; crash respawn backoff is a `Schedule`.
|
|
2076
|
+
| Area | Shape |
|
|
2077
|
+
| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
2078
|
+
| Requests (`src/server.ts`, `src/routes/mcp.ts`, `src/routes/oauth.ts`) | One fiber per `fetch`, tied to `request.signal`. An `/mcp` request's admission permit and every `McpServer` it builds live in a Scope the response carries out, closed when the body ends, fails, or is cancelled, when the signal aborts, when its admitted-request lifetime ends in that request, or at once if the handler fails first. If workerd runs none of those callbacks, the next request reclaims only the expired admission record; it cannot close another request's Scope. The OAuth callback is uninterruptible: a single-use code's exchange and catalog invalidation are one commitment. |
|
|
2079
|
+
| 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. |
|
|
2080
|
+
| 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`. |
|
|
2081
|
+
| `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. Public executor methods stay Promise-shaped; registered built-in execution composes directly as an Effect under the signal and `execute.watchdogMs`. Queued acquisition retains its Promise request handoff. Each guest host call is a fiber of its own. |
|
|
2082
|
+
| 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, which an unscoped search or native listing applies only to connectors it must ask; those add a cached-catalog peek and stale listing, and unscoped search the discovery budget and health record (`discovery-health.ts`). |
|
|
2083
|
+
| Registry (`registry.ts`) | The result stash is a program over `Storage`. Same-request catalog loads share one read the way discovery's do. Remote MCP caching uses the SDK ResponseCacheStore adapter over SQL-backed KV, with no cross-request catalog flight. |
|
|
2084
|
+
| Remote MCP (`connectors/remote-mcp.ts`, `connectors/remote-mcp/`) | 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. |
|
|
2085
|
+
| Downstream OAuth (`auth/downstream-oauth*`, `auth/static-oauth.ts`) | A refresh flight is a Deferred. Before the first durable coordinator write (claim CAS) is submitted, call/request closure stops preparation without a CAS, token request or state change. Submission transfers renewal to the storage coordinator under its own 35-second preparation deadline and 20-second token-request/body deadline, followed by the fenced rotation commit through the runtime deferred-work hook. A closed caller promptly fails without consuming the result, marking its ledger renewed or sending/resending a resource request. |
|
|
2086
|
+
| Operator and activity data (`routes/operator.ts`) | Each JSON route is one program run by `serveOperator` behind the Promise `handle()`. Reads run under the request's signal; writes do not, so a vault write or OAuth disconnect that started reaches its cache invalidation. |
|
|
2087
|
+
| QuickJS pool (`executors/quickjs.ts`, Node only) | Each child is a scoped resource whose release sends SIGTERM, then SIGKILL after a second; crash respawn backoff is a `Schedule`. |
|
|
2028
2088
|
|
|
2029
2089
|
### Native registry and request services (contributor)
|
|
2030
2090
|
|
|
@@ -2344,8 +2404,11 @@ late publication from an invalidated request unreachable. Missing chunks,
|
|
|
2344
2404
|
expired entries, storage errors, or a failed page trigger a fresh complete
|
|
2345
2405
|
listing. A transient first-page tool-listing failure may use a complete catalog
|
|
2346
2406
|
within five minutes of expiry in the same auth partition. Partial walks and
|
|
2347
|
-
auth or permission failures refuse fallback
|
|
2348
|
-
|
|
2407
|
+
auth or permission failures refuse fallback, and a refusal fences later fallback
|
|
2408
|
+
until a listing succeeds; resources require unexpired inventories, and stale
|
|
2409
|
+
fallback cannot authorize post-entry auth recovery.
|
|
2410
|
+
Discovery's [stale listing](#storage-credentials-and-connectors) reads the same
|
|
2411
|
+
entries and answers no call.
|
|
2349
2412
|
|
|
2350
2413
|
**Repeated reads share one request's memo.** `src/runtime/request-reads.ts`
|
|
2351
2414
|
gives each request owner a memo of storage reads, emptied when the request
|
|
@@ -2604,11 +2667,9 @@ this declaration keep parameter-based continuation.
|
|
|
2604
2667
|
An explicit initialAnchor adapter on parameter-based paging can retain a numeric
|
|
2605
2668
|
timestamp as a first-page time-window anchor; opaque string continuations still
|
|
2606
2669
|
require a signed token.
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
`page.in` specifies. `kit.hatch.paging` still validates declared mode values as
|
|
2611
|
-
a sanity check; it does not select the generated continuation instruction.
|
|
2670
|
+
A read tool's input schema states how to continue: tools that take `cursor`
|
|
2671
|
+
restore `page.nextCursor` as that top-level argument; tools without it restore
|
|
2672
|
+
the token in `query[page.param]` or `body[page.param]`, as `page.in` specifies.
|
|
2612
2673
|
An unsafe continuation keeps the safe page data with `hasMore: true` and no
|
|
2613
2674
|
`nextCursor`.
|
|
2614
2675
|
A reviewed `HatchBinding` can supply vendor encoding/origin routing through
|
|
@@ -2657,7 +2718,8 @@ contracts, `Summary` annotations and lenient success schemas become the
|
|
|
2657
2718
|
index's parameters, bodies, summaries and declared fields (optional
|
|
2658
2719
|
`OpenApiData.fields`). Kit facts carry the vendor's declared
|
|
2659
2720
|
`VendorErrors.meanings` and an `idempotencyKey` checked against the write
|
|
2660
|
-
builders at construction
|
|
2721
|
+
builders at construction; the generated Conventions state the key rule only where
|
|
2722
|
+
a provider has one, and the Errors line always.
|
|
2661
2723
|
|
|
2662
2724
|
Hatch request paths and query keys/values refuse any containment of the shared
|
|
2663
2725
|
`[redacted]` marker, including decoded spellings. Body fields and kit write
|
|
@@ -2668,8 +2730,7 @@ Admission iteratively visits keys and values before the write program runs;
|
|
|
2668
2730
|
it checks agent arguments, not whole bodies later composed from internal
|
|
2669
2731
|
reads. The kit connection also checks actual typed path parameters and query
|
|
2670
2732
|
values before transport. Legacy non-kit tools retain their link-only admission;
|
|
2671
|
-
arbitrary binary/base64 bytes are not decoded by this scalar rule.
|
|
2672
|
-
sets `refusesWithheld` for the standard hatch, enabling the generated convention.
|
|
2733
|
+
arbitrary binary/base64 bytes are not decoded by this scalar rule.
|
|
2673
2734
|
Declared error-code facts use the closed `ConnectorCallErrorCode` set at
|
|
2674
2735
|
construction (INV-11); classification callbacks are not run at construction.
|
|
2675
2736
|
|
|
@@ -3370,7 +3431,7 @@ compiling and configuring the real thing.
|
|
|
3370
3431
|
is not a declared refusal standing for the whole call makes the call
|
|
3371
3432
|
unknown, never retryable, with no wait and no resend or key-reuse advice;
|
|
3372
3433
|
while any such 401 is unrenewed the verdict keeps an authorization code,
|
|
3373
|
-
so invocation keeps the
|
|
3434
|
+
so invocation keeps the authorization handoff and picks its
|
|
3374
3435
|
reconcile-first words. Its code and words are the failure's when that is an
|
|
3375
3436
|
authorization verdict, else the 401's own recorded verdict, else
|
|
3376
3437
|
`auth_required`, with a sentence saying the write was sent before the
|
|
@@ -3926,7 +3987,8 @@ physical-attempt and body/admission suites remain compatibility evidence.
|
|
|
3926
3987
|
|
|
3927
3988
|
The standard hatch privately registers bounded vendor operation text against its
|
|
3928
3989
|
ApiTool identity; `apiConnector` transfers those facts to the constructed
|
|
3929
|
-
connector
|
|
3990
|
+
connector, and a provider that wraps that connector carries them to its outer
|
|
3991
|
+
object with `carryConnectorVocabulary`. `operation-vocabulary.ts` contains only pure lexical/cache logic and
|
|
3930
3992
|
types. Catalog discovery consumes those facts after loading and classifying a
|
|
3931
3993
|
complete visible catalog. It never imports a provider or an operation index,
|
|
3932
3994
|
parses operation contracts, invokes a handler, or authenticates. Matching requires
|
package/documentation/auth.md
CHANGED
|
@@ -980,8 +980,8 @@ admitted.
|
|
|
980
980
|
| Scope | `/mcp/<pool>` and `/agent/<pool>` serve exactly the pool's `tools` (connector ids or exact `connector.tool` addresses), with the pool's `readOnly`, to its member people and member agents respectively. Plain `/mcp` serves the whole catalog to maintainers and the unique member pool to other people. Neither intersects with or widens anything. |
|
|
981
981
|
| Refusal | A person with zero or several memberships at `/mcp`, a non-member, and an undeclared pool get an identical 404 status, body, and headers, after auth. The operator log records `not_maintainer` (zero memberships), `ambiguous_pool` (several memberships), `not_pool_member`, or `pool_not_declared`. Names are not anonymously enumerable; valid credentials cannot distinguish these cases by content. |
|
|
982
982
|
| Agents | An agent member reaches its pools only at `/agent/<pool>`, with its `cta_` token. Human endpoints refuse `cta_`; see [agent endpoints](#agent-endpoints). |
|
|
983
|
-
| Surface | From 0.35.0, optional `surface`: `"meta"` (default) serves the
|
|
984
|
-
| Description | Optional `description`: plain text telling the model what this endpoint is for. `/mcp/<pool>` and `/agent/<pool>`
|
|
983
|
+
| Surface | From 0.35.0, optional `surface`: `"meta"` (default) serves the meta-tools; `"native"` lists each of the pool's downstream tools directly, for clients with their own tool search, with optional `executeCode: false` to replace `execute_code` with `read_result` (invalid on meta pools). Calls take the same paths either way. See the [native tool surface](./meta-tools.md#native-tool-surface). |
|
|
984
|
+
| Description | Optional `description`: plain text telling the model what this endpoint is for. `/mcp/<pool>` and `/agent/<pool>` place the description before the rest of the MCP instructions; a maintainer at plain `/mcp` and a pool without one serve none. A routed person receives their pool description. It grants, hides, and refuses nothing, and changes no tool or tool description. |
|
|
985
985
|
| Construction | Malformed names, unknown options or connectors, empty pools, empty or missing `members`, mixed people and agent membership (a `ConfigError` naming the pool), unresolvable member references, a non-boolean `readOnly`, an `owner` that is not a Workspace email address, a `description` that is not 1 to 500 UTF-16 code units of one-line text after trimming, a retired `trust` or `grant`, and addresses absent from an `api()` static catalog throw. Remote catalogs are checked at lazy load. |
|
|
986
986
|
| Discovery | Clerk's pool 401 names `/.well-known/oauth-protected-resource/mcp/<pool>` with the pool URL as `resource`, matching RFC 9728. Cloudflare Managed OAuth is application-level and needs no pool setup. |
|
|
987
987
|
| Drift | No wildcards; pool tools are exact names. Missing live tools are unreachable and warned once while in a bounded 1,024-entry FIFO. |
|
|
@@ -1442,7 +1442,7 @@ longer live; one left behind cannot complete and expires with its link.
|
|
|
1442
1442
|
`authorize_connector` issues the same browser link, with `force` carried in the
|
|
1443
1443
|
signed handoff. On hosts without URL elicitation, direct and program auth
|
|
1444
1444
|
failures and search/describe catalog failures include the same authorization
|
|
1445
|
-
handoff. The admitted registry and management permission govern issuance.
|
|
1445
|
+
handoff. A declined or cancelled URL elicitation returns it too. The admitted registry and management permission govern issuance.
|
|
1446
1446
|
Each call or program run shares at most one handoff per connector; errors never
|
|
1447
1447
|
request a forced restart. Signed links retain their fifteen-minute expiry,
|
|
1448
1448
|
initiating-user and owner binding, origin check, and single-use browser nonce.
|
|
@@ -1681,7 +1681,7 @@ If the refresh fails instead, whatever it met (a dead grant, a waiter
|
|
|
1681
1681
|
deadline, the caller leaving), or hands back the token the API just
|
|
1682
1682
|
rejected, the 401's verdict stays:
|
|
1683
1683
|
`downstream_oauth_required`, never retryable, with `reconciliationRequired`
|
|
1684
|
-
and `writeOutcome: "unknown"`, and
|
|
1684
|
+
and `writeOutcome: "unknown"`, and a handoff that says
|
|
1685
1685
|
to re-read the target before any fresh call. A read is refreshed and sent
|
|
1686
1686
|
once more. MCP sends a tool only once when either its effective classification
|
|
1687
1687
|
or its original server hints say write. A deployment write-to-read override
|
|
@@ -2012,9 +2012,14 @@ any registered form is still withheld, however short.
|
|
|
2012
2012
|
Identifiers sent beside a credential are matched whole only, because consent
|
|
2013
2013
|
URLs and vendor metadata carry their parts by design: a confidential OAuth
|
|
2014
2014
|
client's ID, a Basic username beside a password of at least eight characters,
|
|
2015
|
-
|
|
2016
|
-
`username`, `login`, or `email`, or starting with `public`)
|
|
2017
|
-
|
|
2015
|
+
credential-slot fields named as identifiers (ending in `id`, `user`,
|
|
2016
|
+
`username`, `login`, or `email`, or starting with `public`), and the two headers
|
|
2017
|
+
maintained providers declare public from their vendors' contracts: Cloudflare's
|
|
2018
|
+
Global API Key `X-Auth-Email` and Overflow's `x-client-id`. A value in either
|
|
2019
|
+
that holds whitespace, as an auth scheme's framing does, is a credential. Any
|
|
2020
|
+
other header or query parameter is never public by name: `remoteMcp()` and
|
|
2021
|
+
`api()` auth headers under any name are credentials, fully matched. A Basic
|
|
2022
|
+
username beside a shorter or empty password is the credential itself and is fully
|
|
2018
2023
|
matched. Query parameters named as continuation cursors (`pageToken`,
|
|
2019
2024
|
`nextPageToken`, `next_token`, `continuationToken`, `syncToken`) are public
|
|
2020
2025
|
result state and are not registered. Header values a connector adds to token
|
|
@@ -2610,6 +2615,30 @@ survives every round. Rotation invalidates outstanding states.
|
|
|
2610
2615
|
Each state admits one retry through a connector-scoped storage CAS, including
|
|
2611
2616
|
decline and cancel. Concurrent or repeated consumption is refused before
|
|
2612
2617
|
dispatch, so retries cannot fork into sibling forced connection links.
|
|
2618
|
+
A decline or cancel returns one fresh link, issued only after that state verifies
|
|
2619
|
+
and the same management check as the prompt passes, so a refusal offers nothing
|
|
2620
|
+
this caller could not already request. The link is sealed like the prompt's and
|
|
2621
|
+
keeps the prompt's force.
|
|
2622
|
+
|
|
2623
|
+
URL `accept` is consent to the interaction, not evidence that a browser navigated:
|
|
2624
|
+
the retry normally reaches Connecta before any visit does. So every link a flow
|
|
2625
|
+
issues stays live until it is visited or expires, whichever round or refusal
|
|
2626
|
+
issued it. A `force: true` flow resets once, through one atomic claim on an identity
|
|
2627
|
+
its links share:
|
|
2628
|
+
|
|
2629
|
+
| A verified visit finds the reset | The visit |
|
|
2630
|
+
| -------------------------------- | ------------------------------------------------------------------------------- |
|
|
2631
|
+
| Unclaimed | Claims it and runs `startAuth({ force: true })` |
|
|
2632
|
+
| Still running in another visit | Returns 409 and spends nothing, so reloading the same link retries |
|
|
2633
|
+
| Started | Spends its link and runs `startAuth({ force: false })`, continuing that consent |
|
|
2634
|
+
| Failed | Claims it again and starts afresh, so a failure never reuses the old grant |
|
|
2635
|
+
|
|
2636
|
+
A failed link is spent, but the flow is not: its other links, or the next prompt's,
|
|
2637
|
+
take over. The record outlives every link the flow can issue. If the process holding
|
|
2638
|
+
the claim dies, nothing settles it, so the flow's links keep returning 409 for up to
|
|
2639
|
+
thirty minutes, and that response says to start a new connection request. A new
|
|
2640
|
+
`force: true` call is a new flow with its own reset, so it recovers at once. Each link still works
|
|
2641
|
+
once. An unforced flow's links stay independent and never reset.
|
|
2613
2642
|
The host records each entered call's classification, catalog freshness and
|
|
2614
2643
|
classification digest in private invocation state, along with auth failure origins. Writes may elicit only
|
|
2615
2644
|
when Connecta's own credential resolution fails before invoking the connector:
|
|
@@ -2644,14 +2673,11 @@ reads this key; connector contexts and guest APIs cannot reach the vault.
|
|
|
2644
2673
|
Explicit authorization retries require a completed browser start (including
|
|
2645
2674
|
its reset and consent binding), valid stored credential fields where declared,
|
|
2646
2675
|
and a healthy passive connector status where available before completing.
|
|
2647
|
-
An in-progress start cannot complete against an old grant
|
|
2648
|
-
|
|
2649
|
-
Failed starts remain single-use but do not block a later
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
A Continue link issued during a pending forced start is bound to that
|
|
2653
|
-
predecessor and cannot be consumed unless the predecessor starts successfully.
|
|
2654
|
-
If it fails, the next prompt requests a fresh restart instead of reusing the old grant.
|
|
2676
|
+
An in-progress start cannot complete against an old grant: only a link whose start
|
|
2677
|
+
finished counts toward completion, and the next prompt carries the same flow, so it
|
|
2678
|
+
cannot restart pending consent. Failed starts remain single-use but do not block a later
|
|
2679
|
+
successful attempt. A link visited during the completion check continues the
|
|
2680
|
+
attempt that check is judging, and cannot reset it.
|
|
2655
2681
|
|
|
2656
2682
|
The `/connect` link issued by the elicitation boundary encrypts its browser
|
|
2657
2683
|
handoff with the vault's `seal`/`open` methods before signing it. The URL carries
|
|
@@ -2720,12 +2746,13 @@ still serve configured credentials. Both routes accept only GET, otherwise retur
|
|
|
2720
2746
|
`form_post` is unsupported. Clerk's session-only policy applies regardless of method.
|
|
2721
2747
|
|
|
2722
2748
|
The fifteen-minute signed handoff carries connector ownership, initiating principal, deployment origin, nonce,
|
|
2723
|
-
restart mode, and
|
|
2749
|
+
restart mode, and, for a forced flow's later links, the flow they share a reset with. New links use versioned, length-prefixed fields instead of
|
|
2724
2750
|
JSON, with unpadded base64url payload and signature segments. Ordinary handoffs use an opaque wire encoding,
|
|
2725
2751
|
not encryption; URL elicitation also seals those fields as described above. Copy the complete URL unchanged.
|
|
2726
2752
|
The signature authenticates the exact version and payload text. Tampered or re-encoded links return
|
|
2727
2753
|
`Invalid or expired connection link. Request a new link from connecta.` Pre-upgrade signed JSON and sealed
|
|
2728
|
-
v2 links still verify against their original text and expire after their original fifteen minutes.
|
|
2754
|
+
v2 links still verify against their original text and expire after their original fifteen minutes. A link from
|
|
2755
|
+
an earlier version that names a predecessor link, in any format, returns the same refusal, and a new link replaces it.
|
|
2729
2756
|
|
|
2730
2757
|
Configure `vault: encryptedCredentialVault(storage, key)`, which derives its HMAC key with HKDF. Custom vaults need `signOAuthHandoff` and `verifyOAuthHandoff`; without a signing vault, links cannot be
|
|
2731
2758
|
issued or accepted. A link grants no identity. After identity and permissions pass, its nonce is consumed
|
|
@@ -2962,12 +2989,12 @@ owner's access token as `Authorization: Bearer` with these rules:
|
|
|
2962
2989
|
- Whatever that recovery meets, a write's failure is never retryable and its
|
|
2963
2990
|
outcome is unknown. Until a refresh succeeds the 401's verdict stands:
|
|
2964
2991
|
`downstream_oauth_required`, which the invocation marks
|
|
2965
|
-
`reconciliationRequired`, with the
|
|
2992
|
+
`reconciliationRequired`, with the authorization handoff, whether
|
|
2966
2993
|
the refresh was [dead](#refresh-failures), lost after it was sent, raced
|
|
2967
2994
|
by a disconnect or reset, failed in storage or contention, or handed back
|
|
2968
2995
|
the token the API just rejected, which renews nothing.
|
|
2969
2996
|
- No grant, a second 401, or a [dead refresh](#refresh-failures) means
|
|
2970
|
-
`downstream_oauth_required`,
|
|
2997
|
+
`downstream_oauth_required`, carrying the authorization handoff. Refresh waiter deadlines
|
|
2971
2998
|
return retryable `unavailable` for a read. Latch second 401s for the request scope,
|
|
2972
2999
|
preventing repeated refreshes by later program calls.
|
|
2973
3000
|
- Handlers can name no storage, sealing, or owner partition; the registry owns them.
|