@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.
Files changed (148) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/bin/connecta.mjs +26 -17
  3. package/bin/meta-tool-names.d.mts +3 -1
  4. package/bin/meta-tool-names.mjs +4 -1
  5. package/bin/version.mjs +1 -1
  6. package/dist/activity.d.ts +2 -1
  7. package/dist/activity.js +5 -5
  8. package/dist/advertised-schema.js +15 -0
  9. package/dist/agent-routes.js +31 -5
  10. package/dist/auth/downstream-oauth/refresh-coordinator.js +4 -1
  11. package/dist/auth/downstream-oauth.js +3 -0
  12. package/dist/auth/static-oauth.js +26 -18
  13. package/dist/auth-elicitation.js +118 -61
  14. package/dist/authorization-handoff.js +36 -3
  15. package/dist/catalog-cache.js +292 -24
  16. package/dist/catalog-freshness.js +12 -0
  17. package/dist/catalog-service.js +373 -70
  18. package/dist/catalog.js +147 -58
  19. package/dist/config-defaults.d.ts +2 -1
  20. package/dist/config-defaults.js +4 -1
  21. package/dist/config.d.ts +43 -17
  22. package/dist/config.js +26 -10
  23. package/dist/connectors/remote-mcp/catalog.js +52 -28
  24. package/dist/connectors/remote-mcp/connection.js +62 -5
  25. package/dist/connectors/remote-mcp.js +10 -1
  26. package/dist/describe-config.d.ts +3 -2
  27. package/dist/discovery-health.js +26 -11
  28. package/dist/errors.d.ts +52 -3
  29. package/dist/errors.js +86 -3
  30. package/dist/execute.js +476 -200
  31. package/dist/executor-admission.js +2 -0
  32. package/dist/executor-contract.js +6 -0
  33. package/dist/executor-result.js +2 -2
  34. package/dist/executors/quickjs-child.js +13 -4
  35. package/dist/executors/quickjs-runtime.d.ts +2 -0
  36. package/dist/executors/quickjs-runtime.js +36 -19
  37. package/dist/executors/quickjs.js +23 -6
  38. package/dist/guest-functions.js +5 -4
  39. package/dist/guest-runtime.js +37 -1
  40. package/dist/guest-types.d.ts +18 -0
  41. package/dist/http/client.js +7 -2
  42. package/dist/http/errors.js +9 -4
  43. package/dist/http/fields.js +17 -5
  44. package/dist/http/hatch.js +9 -3
  45. package/dist/http/paging.js +1 -1
  46. package/dist/http/provider.js +2 -6
  47. package/dist/http/vendor.js +6 -1
  48. package/dist/index.d.ts +1 -1
  49. package/dist/index.js +5 -0
  50. package/dist/invocation.js +98 -34
  51. package/dist/meta-output.js +24 -18
  52. package/dist/meta-tool-names.d.ts +1 -1
  53. package/dist/meta-tool-names.js +1 -1
  54. package/dist/meta-tools.js +259 -60
  55. package/dist/native-recovery.js +4 -1
  56. package/dist/native-tools.js +166 -51
  57. package/dist/node.js +3 -1
  58. package/dist/oauth-handoff.js +53 -43
  59. package/dist/operation-activity.js +2 -1
  60. package/dist/operation-vocabulary.js +6 -0
  61. package/dist/operator-record.d.ts +1 -1
  62. package/dist/operator-record.js +1 -0
  63. package/dist/operator-ui/generated.js +2 -2
  64. package/dist/program-result.js +2 -0
  65. package/dist/program-source.js +1 -1
  66. package/dist/program-writes.js +112 -19
  67. package/dist/provider.d.ts +0 -13
  68. package/dist/provider.js +0 -3
  69. package/dist/providers/_shared/google/kit.js +2 -2
  70. package/dist/providers/breeze/api.js +2 -2
  71. package/dist/providers/breeze/skill.generated.js +1 -1
  72. package/dist/providers/ccb/api.js +2 -2
  73. package/dist/providers/ccb/skill.generated.js +1 -1
  74. package/dist/providers/cloudflare/api.js +2 -2
  75. package/dist/providers/cloudflare/index.js +2 -0
  76. package/dist/providers/cloudflare/skill.generated.js +1 -1
  77. package/dist/providers/docs/skill.generated.js +1 -1
  78. package/dist/providers/drive/skill.generated.js +1 -1
  79. package/dist/providers/forms/index.js +1 -0
  80. package/dist/providers/forms/skill.generated.js +1 -1
  81. package/dist/providers/github/api.js +98 -6
  82. package/dist/providers/github/index.js +7 -2
  83. package/dist/providers/github/lib/auth.d.ts +3 -1
  84. package/dist/providers/github/lib/auth.js +43 -12
  85. package/dist/providers/github/lib/discovery.js +11 -1
  86. package/dist/providers/github/lib/operations.js +8 -0
  87. package/dist/providers/github/safety.js +39 -0
  88. package/dist/providers/github/skill.generated.js +1 -1
  89. package/dist/providers/github/tools.js +12 -5
  90. package/dist/providers/gmail/index.js +1 -0
  91. package/dist/providers/gmail/skill.generated.js +1 -1
  92. package/dist/providers/granola/skill.generated.js +1 -1
  93. package/dist/providers/overflow/index.js +2 -0
  94. package/dist/providers/overflow/skill.generated.js +1 -1
  95. package/dist/providers/planning-center/index.js +11 -5
  96. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  97. package/dist/providers/planning-center/skill.generated.js +1 -1
  98. package/dist/providers/quickbooks/index.js +2 -5
  99. package/dist/providers/quickbooks/skill.generated.js +1 -1
  100. package/dist/providers/sheets/index.js +1 -0
  101. package/dist/providers/sheets/skill.generated.js +1 -1
  102. package/dist/providers/slides/index.js +2 -2
  103. package/dist/providers/slides/skill.generated.js +1 -1
  104. package/dist/providers/stripe/skill.generated.js +1 -1
  105. package/dist/providers/tithely/index.js +1 -0
  106. package/dist/providers/tithely/skill.generated.js +1 -1
  107. package/dist/providers/vercel/api.js +2 -2
  108. package/dist/providers/vercel/skill.generated.js +1 -1
  109. package/dist/registry.d.ts +36 -4
  110. package/dist/registry.js +127 -27
  111. package/dist/result-preview.js +210 -0
  112. package/dist/result-seal.js +168 -0
  113. package/dist/route-names.js +13 -4
  114. package/dist/routes/connect.js +21 -13
  115. package/dist/routes/mcp.js +55 -26
  116. package/dist/routes/shared.d.ts +6 -0
  117. package/dist/runtime/call.js +15 -10
  118. package/dist/runtime/request-scope.js +36 -1
  119. package/dist/runtime/run.js +4 -1
  120. package/dist/sent-secrets.d.ts +20 -0
  121. package/dist/sent-secrets.js +49 -1
  122. package/dist/skills.js +100 -33
  123. package/dist/storage/keys.js +9 -6
  124. package/dist/storage/sql-native.js +1 -1
  125. package/dist/tool-safety.d.ts +33 -0
  126. package/dist/tool-safety.js +51 -15
  127. package/dist/types.d.ts +31 -0
  128. package/dist/usage-guide.js +14 -13
  129. package/dist/validate.js +29 -10
  130. package/dist/version.d.ts +1 -1
  131. package/dist/version.js +1 -1
  132. package/dist/worker.js +28 -8
  133. package/documentation/architecture.md +104 -42
  134. package/documentation/auth.md +46 -19
  135. package/documentation/code-mode.md +378 -116
  136. package/documentation/deploying.md +10 -9
  137. package/documentation/hosted-mcp-migration.md +9 -3
  138. package/documentation/integrating.md +27 -5
  139. package/documentation/meta-tools.md +655 -153
  140. package/documentation/operating.md +5 -3
  141. package/documentation/providers.md +18 -6
  142. package/examples/worker/README.md +3 -3
  143. package/guides/how-it-works.md +2 -1
  144. package/guides/safety-and-trust.md +1 -3
  145. package/package.json +1 -1
  146. package/templates/node/AGENTS.md +3 -3
  147. package/templates/node/README.md +1 -1
  148. 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
- // Serialization makes the validation cover exactly what the agent receives.
505
- const example = JSON.parse(JSON.stringify(exampleCandidate(schema, schema)));
506
- if (validator.validate(example).valid) {
507
- minimizeExample(example, validator);
508
- if (validator.validate(example).valid)
509
- return { example };
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.41.0";
1
+ export declare const CONNECTA_VERSION = "0.42.0";
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // Generated from package.json by scripts/generate-version.mjs.
2
2
  // Workers-safe: no runtime filesystem read or deployment override.
3
- export const CONNECTA_VERSION = "0.41.0";
3
+ export const CONNECTA_VERSION = "0.42.0";
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
- // One pool per executor, so per isolate. A queued run may wait out one
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: 60_000,
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 = slice(err, 0, 1000);
201
+ if (typeof err === "string") message = prefix(err, 1000);
183
202
  else if (err) {
184
- if (typeof err.name === "string") name = slice(err.name, 0, 64);
185
- if (typeof err.message === "string") message = slice(err.message, 0, 1000);
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: ${options.timeout ?? 60_000}, deadlineMs: ${options.timeout ?? 60_000} } } : {})
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
- ...(options.timeout !== undefined ? { timeout: options.timeout } : {}),
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 six meta-tools on a fresh `McpServer`
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`, native `tools/list` and tool calls keep the full
753
- `discovery.probeTimeoutMs`. A probe that times out on its own deadline, or a
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. Resources require unexpired inventories. There is no deferred refresh.
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 five-minute
812
- fallback window, and generations expire after 48 hours; invalidation
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. Its input/output schemas derive one
1356
- paging convention, including `get_*`, hatch search and details, excluding
1357
- tools whose `page.hasMore` is always false. The convention distinguishes
1358
- top-level `cursor` from `query[page.param]` or `body[page.param]` continuation
1359
- and tells agents to return `page.nextCursor` before the call that would exceed
1360
- the per-program call limit, then continue in the next program. The Hatch line
1361
- reports a nonzero explicit refusal count with singular/plural wording and
1362
- points to the actual prefixed details tool with method/path lookup; it never
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
- They cannot relax source replay safety. Effective or source write makes a call
1725
- write-like on every resend and recovery path. Source classifications travel in
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 adds the discovery budget and health record (`discovery-health.ts`). |
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; resources require unexpired
2348
- inventories, and stale fallback cannot authorize post-entry auth recovery.
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
- The generated Paging convention follows the read tool's input schema: tools
2608
- that take `cursor` restore `page.nextCursor` as that top-level argument; tools
2609
- without it restore the token in `query[page.param]` or `body[page.param]`, as
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, which the generated Conventions state.
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. The kit
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 `authorize_connector` handoff and picks its
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. `operation-vocabulary.ts` contains only pure lexical/cache logic and
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
@@ -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 six meta-tools; `"native"` lists each of the pool's downstream tools directly, for clients with their own tool search, with optional `executeCode: false` to replace `execute_code` with `read_result` (invalid on meta pools). Calls take the same paths either way. See the [native tool surface](./meta-tools.md#native-tool-surface). |
984
- | Description | Optional `description`: plain text telling the model what this endpoint is for. `/mcp/<pool>` and `/agent/<pool>` prefix the MCP instructions with `Use skills. ` and the description before routing rules; a maintainer at plain `/mcp` and a pool without one serve the shared instructions unchanged. A routed person receives their pool description. It grants, hides, and refuses nothing, and changes no tool or tool description. |
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 an `authorize_connector` handoff that says
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
- and credential-slot fields named as identifiers (ending in `id`, `user`,
2016
- `username`, `login`, or `email`, or starting with `public`). A Basic username
2017
- beside a shorter or empty password is the credential itself and is fully
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. A forced flow switches to
2648
- Continue after that visit, so another prompt cannot restart pending consent.
2649
- Failed starts remain single-use but do not block a later successful attempt.
2650
- Before checking completion, unspent links in the retry flow are atomically
2651
- retired so an older forced link cannot begin a reset during the status check.
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 any predecessor link binding. New links use versioned, length-prefixed fields instead of
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 `authorize_connector` handoff, whether
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`, directing `authorize_connector`. Refresh waiter deadlines
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.