@zackbart/connecta 0.29.0 → 0.31.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 (116) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/README.md +126 -134
  3. package/bin/connecta.mjs +13 -2
  4. package/bin/version.mjs +1 -1
  5. package/dist/argument-redaction.js +230 -0
  6. package/dist/auth-elicitation.js +14 -9
  7. package/dist/authorization-handoff.js +111 -0
  8. package/dist/call-recovery.js +25 -0
  9. package/dist/catalog-cache.js +2 -2
  10. package/dist/catalog-service.js +103 -63
  11. package/dist/config-schema.d.ts +14 -7
  12. package/dist/config-schema.js +29 -10
  13. package/dist/config.d.ts +2 -0
  14. package/dist/config.js +12 -7
  15. package/dist/connectors/guarded-fetch.d.ts +9 -1
  16. package/dist/connectors/guarded-fetch.js +17 -5
  17. package/dist/connectors/negotiation-cache.js +1 -1
  18. package/dist/downstream-input-context.js +9 -0
  19. package/dist/downstream-input.js +13 -11
  20. package/dist/errors.d.ts +38 -12
  21. package/dist/errors.js +19 -9
  22. package/dist/execute.js +375 -43
  23. package/dist/executor-result.js +11 -9
  24. package/dist/executors/quickjs-child.js +12 -5
  25. package/dist/executors/quickjs-protocol.js +5 -6
  26. package/dist/executors/quickjs-runtime.js +6 -1
  27. package/dist/executors/quickjs.js +20 -4
  28. package/dist/guest-runtime.js +52 -2
  29. package/dist/guest-types.d.ts +47 -5
  30. package/dist/index.d.ts +1 -1
  31. package/dist/invocation.js +72 -14
  32. package/dist/meta-output.js +0 -15
  33. package/dist/meta-tools.js +166 -95
  34. package/dist/oauth-handoff.js +88 -11
  35. package/dist/operator-ui/generated.js +2 -2
  36. package/dist/operator-ui/view.d.ts +9 -0
  37. package/dist/operator-ui/view.js +11 -0
  38. package/dist/program-result.js +5 -0
  39. package/dist/program-writes.js +7 -48
  40. package/dist/provider.d.ts +8 -6
  41. package/dist/provider.js +3 -4
  42. package/dist/providers/_shared/google/workspace.d.ts +1 -1
  43. package/dist/providers/_shared/request-token.js +83 -0
  44. package/dist/providers/_shared/rest/dispatch.js +41 -0
  45. package/dist/providers/_shared/rest/operation-index.d.ts +133 -0
  46. package/dist/providers/_shared/rest/operation-index.js +527 -0
  47. package/dist/providers/_shared/rest/tools.d.ts +141 -0
  48. package/dist/providers/_shared/rest/tools.js +569 -0
  49. package/dist/providers/_shared/rest/value-safety.d.ts +151 -0
  50. package/dist/providers/_shared/rest/value-safety.js +580 -0
  51. package/dist/providers/cloudflare/graphql.js +385 -0
  52. package/dist/providers/cloudflare/index.d.ts +47 -51
  53. package/dist/providers/cloudflare/index.js +188 -1997
  54. package/dist/providers/cloudflare/openapi.generated.js +3347 -0
  55. package/dist/providers/cloudflare/rest.d.ts +37 -0
  56. package/dist/providers/cloudflare/rest.js +1056 -0
  57. package/dist/providers/cloudflare/skill.generated.js +7 -4
  58. package/dist/providers/cloudflare/value-safety.js +1247 -0
  59. package/dist/providers/infisical/index.d.ts +7 -0
  60. package/dist/providers/infisical/index.js +713 -0
  61. package/dist/providers/infisical/skill.generated.js +8 -0
  62. package/dist/providers/notion/index.d.ts +35 -31
  63. package/dist/providers/notion/index.js +60 -1499
  64. package/dist/providers/notion/openapi.generated.js +75 -0
  65. package/dist/providers/notion/rest.d.ts +24 -0
  66. package/dist/providers/notion/rest.js +1067 -0
  67. package/dist/providers/notion/skill.generated.js +5 -4
  68. package/dist/providers/notion/value-safety.js +31 -0
  69. package/dist/providers/stripe/index.d.ts +26 -20
  70. package/dist/providers/stripe/index.js +103 -130
  71. package/dist/providers/stripe/openapi.generated.js +649 -0
  72. package/dist/providers/stripe/rest.d.ts +14 -0
  73. package/dist/providers/stripe/rest.js +395 -0
  74. package/dist/providers/stripe/skill.generated.js +7 -5
  75. package/dist/providers/stripe/value-safety.js +705 -0
  76. package/dist/providers/vercel/index.d.ts +31 -22
  77. package/dist/providers/vercel/index.js +96 -853
  78. package/dist/providers/vercel/openapi.generated.js +456 -0
  79. package/dist/providers/vercel/rest.d.ts +20 -0
  80. package/dist/providers/vercel/rest.js +1002 -0
  81. package/dist/providers/vercel/skill.generated.js +4 -5
  82. package/dist/providers/vercel/value-safety.js +328 -0
  83. package/dist/registry.d.ts +8 -6
  84. package/dist/registry.js +134 -61
  85. package/dist/resource-read.js +4 -1
  86. package/dist/routes/mcp.js +11 -2
  87. package/dist/sent-secrets.d.ts +54 -13
  88. package/dist/sent-secrets.js +770 -231
  89. package/dist/skills.js +26 -16
  90. package/dist/sqlite.js +21 -6
  91. package/dist/storage/keys.js +20 -5
  92. package/dist/storage/memory.js +9 -4
  93. package/dist/storage/sql.js +18 -9
  94. package/dist/types.d.ts +17 -3
  95. package/dist/usage-guide.js +49 -40
  96. package/dist/version.d.ts +1 -1
  97. package/dist/version.js +1 -1
  98. package/dist/worker.js +2 -1
  99. package/dist/write-outcome.js +47 -0
  100. package/documentation/README.md +64 -0
  101. package/documentation/architecture.md +241 -97
  102. package/documentation/auth.md +138 -5
  103. package/documentation/code-mode.md +224 -107
  104. package/documentation/deploying.md +146 -0
  105. package/documentation/integrating.md +170 -0
  106. package/documentation/meta-tools.md +142 -30
  107. package/documentation/operating.md +135 -0
  108. package/documentation/operator-tests.md +4 -0
  109. package/documentation/operator-ui.md +132 -0
  110. package/documentation/provider-auth-migration.md +307 -0
  111. package/documentation/provider-migration-0.29.md +22 -5
  112. package/examples/worker/README.md +9 -2
  113. package/package.json +9 -2
  114. package/templates/node/README.md +9 -2
  115. package/templates/node/package.json +1 -1
  116. package/dist/providers/vercel/mcp-ownership.js +0 -212
@@ -9,6 +9,26 @@ first; this guide explains the shape and where each subsystem lives. The surface
9
9
  [meta-tools](./meta-tools.md), [code mode](./code-mode.md), and
10
10
  [inbound auth](./auth.md).
11
11
 
12
+ ## Task routes
13
+
14
+ [Agent index](./README.md) routes endpoint use, deployment, integration, and
15
+ repository maintenance. Read this guide when changing the core or checking an
16
+ implementation boundary. Use these sections for the affected subsystem.
17
+
18
+ | Change or question | Sections |
19
+ | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
20
+ | Request handling, transport, cleanup | [Two lifetimes](#the-two-lifetimes), [request lifecycle](#request-lifecycle), [admission](#admission-in-two-places) |
21
+ | Discovery, invocation, classification | [Layers below meta-tools](#layers-below-the-meta-tools), [providers and classification](#providers-and-reviewed-classification) |
22
+ | Storage, credentials, catalogs, paging | [Storage and connectors](#storage-credentials-and-connectors), [errors and records](#errors-and-records) |
23
+ | Config validation and optional features | [Configuration](#configuration), [modules](#optional-deployment-modules), [operator contract](./operator-ui.md#operator-data-contract) |
24
+ | Imports, executors, or Effect | [Purity](#import-graph-purity), [Effect](#effect-inside), [sharp edges](#sharp-edges) |
25
+ | Find code and deployment evidence | [Source map](#where-else-to-look), [sharp edges](#sharp-edges), [repository policy](https://github.com/zackbart/connecta/blob/main/AGENTS.md) |
26
+
27
+ Source paths and test names in each section identify its implementation and
28
+ regression evidence. Recorded production probes and bundle measurements are
29
+ historical observations tied to their stated date or version, not current
30
+ benchmarks or proof that planned replacements have shipped.
31
+
12
32
  ## The two lifetimes
13
33
 
14
34
  Almost every bug in this codebase is a lifetime mistake, so the split is worth
@@ -207,6 +227,13 @@ same credential resolution, and the same fail-closed read-only check.
207
227
  `test/execute.test.ts` asserts the parity directly, because a sandbox path that
208
228
  quietly diverges is how generated code would mint a capability.
209
229
 
230
+ Program result paging shares `meta-tools.ts`'s direct-call stash envelope and
231
+ reader. `program-result.ts` owns executor-independent byte limits;
232
+ `execute.ts` issues bounded transfer cursors, and `guest-runtime.ts` reconstructs
233
+ values through provider preludes. Cursors hold only an ID and offset for the
234
+ current run. Result bytes, expiry, authority and capacity charges stay in the
235
+ existing storage and ledger; no executor-specific store or storage key is added.
236
+
210
237
  ## Admission, in two places
211
238
 
212
239
  Request admission (`src/executor-admission.ts`, applied in `src/routes/mcp.ts`)
@@ -324,11 +351,20 @@ marker refuses stale copies afterward.
324
351
 
325
352
  `KVStorage` is `get`/`set`/`delete`/`list(prefix)`/`compareAndSet`, all
326
353
  required, and `createConnecta` refuses storage missing one (INV-11).
354
+ Adapters must also declare `capabilities: { absoluteExpiry: true }`.
355
+ Construction rejects missing or false capability declarations with an error
356
+ naming `capabilities.absoluteExpiry`. The memory, SQLite, and D1 adapters carry
357
+ this marker. Custom adapters may declare it only after implementing absolute
358
+ expiry on both writes; a legacy `ttlSeconds`-only adapter is rejected by the
359
+ published types and at runtime.
327
360
  `compareAndSet(key, expected, next, options?)` is an atomic claim: `null` means
328
361
  absent (expired counts) on the way in and delete on the way out, and a
329
- successful write takes the same optional `ttlSeconds` as `set`. SQL TTLs are
330
- created and checked by the database clock inside each statement, so isolate
331
- clock skew cannot expire a live holder's record. Each SQL claim
362
+ successful write takes the same expiry options as `set`. Relative `ttlSeconds`
363
+ is created and checked by the database clock inside each statement, so isolate
364
+ clock skew cannot expire a live holder's record. Absolute `expiresAtMs` is a
365
+ safe-integer epoch-millisecond deadline stored verbatim and checked against the
366
+ storage clock. Supply at most one option. Custom adapters must implement both;
367
+ a delayed absolute-expiry write must not restart a relative TTL at commit time. Each SQL claim
332
368
  is one statement, which SQLite executes atomically and D1 serializes on its
333
369
  primary. Everything that must claim a key exactly once relies on it with no
334
370
  read-then-write fallback: access-token issuance and
@@ -350,7 +386,8 @@ State-file import validates all keys before writing. The `connecta_kv` table
350
386
  keeps its existing TEXT keys, including compatibility with the 0.28 schema.
351
387
  Storage writes no log lines. A refused import names the file and an entry's
352
388
  position, never a key, a value, or the JSON parser's account, which quotes
353
- the file (INV-6).
389
+ the file (INV-6). Database failures report only a fixed import step and an
390
+ entry's position, including errors raised by target-database triggers.
354
391
 
355
392
  Remote MCP catalogs use the SDK `ResponseCacheStore` over this same SQL KV.
356
393
  The `response-cache:v1:` family binds connector id, a hash of its configured
@@ -433,12 +470,53 @@ Result paging stores each oversized result for 15 minutes, chunked so a page
433
470
  reads only what it covers. Its bounds (`results.maxStashBytes`,
434
471
  `results.maxStashEntries`) are the deployment's, not an isolate's: every charge
435
472
  is a row in one ledger record, booked by compare-and-set before any chunk is
436
- written, so isolates and processes sharing the store see one count. A lost swap
437
- backs off and re-reads; only a full ledger refuses. Each chunk's TTL is what
438
- remains of its charge's deadline, so no chunk outlives its charge. A full
439
- stash returns the successful call's preview and a paging-unavailable notice
440
- rather than a result id. The shared storage cases live in
441
- `test/storage-contract.ts` and `test/sql-storage-contract.ts`.
473
+ written, so isolates and processes sharing the store see one count. Every
474
+ reservation starts with a finite deadline of booking time plus the 30-second
475
+ write budget plus the chunk TTL, normally 15 minutes. Pending writes retain
476
+ that charge until its deadline. Booking retries recognize their own reservation
477
+ ID and reuse its stored deadline, including a final read after an ambiguous
478
+ last CAS. A lost swap or transient storage error backs off and re-reads, up to
479
+ 32 attempts. A full ledger or unconfirmed exhausted booking returns no result id.
480
+
481
+ Caller time only nominates charges for pruning. Before discarding one, the
482
+ ledger writes a probe with that charge's absolute deadline and reads it back.
483
+ Only an absent probe proves storage has passed the deadline; a live probe keeps
484
+ the charge, and a storage error cannot free capacity. Each deadline has its own
485
+ key in `stashExpiryKeys`, so concurrent probes cannot overwrite a different
486
+ deadline's proof. Probes expire at the deadline they check and need no cleanup.
487
+ This uses the same absolute-expiry contract on memory, SQLite and D1 without a
488
+ storage API or schema change. An ahead caller clock cannot admit a second stash
489
+ while the first remains readable.
490
+
491
+ The whole chunk-write phase has a 30-second timeout measured from booking, with
492
+ a clock check before and after each write. Trailing chunks precede the header;
493
+ failure or timeout returns no result id and stops the write loop. Chunks use an
494
+ absolute expiry at booking time plus their TTL, so slow writes shorten the
495
+ remaining paging window. Storage promises cannot guarantee cancellation of an
496
+ already-dispatched database operation, and a rejected client response does not
497
+ prove that operation cannot commit later. Even a commit after timeout or process
498
+ interruption keeps that absolute expiry, rather than starting another TTL.
499
+
500
+ Settlement and release only shorten or remove a reservation. Successful writes
501
+ attempt to shorten the charge to the fixed chunk expiry. Cleanup releases it
502
+ only after all deletions succeed and every dispatched write returned confirmed
503
+ success. Failed cleanup or exhausted release attempts finite settlement when
504
+ no write is unconfirmed. A rejected or timed-out write keeps its original
505
+ reservation because it may still commit after deletion. Each completion path
506
+ has one 15-second budget covering deletion, release, fallback settlement, and
507
+ their storage waits and retries. Successful-write settlement has the same
508
+ 15-second budget. A stalled operation stops being awaited when the budget
509
+ expires; its late response starts no further I/O, and no background cleanup
510
+ fiber is started. The caller returns its write outcome even if these
511
+ optimizations cannot finish. Ledger updates retry within the same 32-attempt
512
+ budget. If they fail, time out, crash, or exhaust,
513
+ capacity may be over-held until the booked deadline, at most 15 minutes and
514
+ 30 seconds for the normal TTL. The ledger row itself has an absolute expiry at
515
+ its latest live reservation deadline. There are no completion receipts or
516
+ separate recovery rows; expiry probes carry no completion state. Expired data reads as absent and normal storage sweeps
517
+ reclaim the physical rows. A full stash returns the successful call's preview
518
+ and a paging-unavailable notice rather than a result id. Shared failure cases
519
+ live in `test/stash-charge-contract.ts`, exercised by the memory and SQL suites.
442
520
 
443
521
  `src/credentials.ts` is the AES-GCM vault behind the root-exported
444
522
  `CredentialVault` contract, selected through the `vault` slot. It binds connector
@@ -488,6 +566,14 @@ credentials produce one payload-free warning per connector; use secrets of
488
566
  at least eight characters. Encoded and JSON-escaped echoes of registered
489
567
  credentials follow the same redaction rules. Connecta's own messages
490
568
  never quote a credential.
569
+ Core uncertainty and retry envelopes share one schema-filtered argument snapshot,
570
+ captured before dispatch and byte budgeting. `writeOnly: true` omits values of
571
+ every length, including nested properties, array items and prefixes, local
572
+ `$ref`/`$defs`, and `allOf`. `oneOf`/`anyOf` must agree on sensitivity. Unresolved
573
+ references, sensitive dynamic-property schemas or other unsupported applicators,
574
+ and traversal limits withhold the entire echo. A private array element withholds
575
+ its containing array to preserve indices. `argsRedacted: true` marks a partial
576
+ or withheld echo as reconciliation context; retries require the original arguments.
491
577
  OAuth discovery and consent URLs are refused as a typed, non-retryable failure
492
578
  when they contain a known credential, before persistence or reuse. This guard
493
579
  checks the request's sent credentials and the current grant's credentials,
@@ -560,7 +646,7 @@ only: an array or a class instance there is refused by path, unread. String
560
646
  maps (static `headers`, `api()`'s `authorizationParams` and
561
647
  `tokenRequestHeaders`) are copied the same way and each value must be a
562
648
  string. A discriminated union (`remoteMcp()`'s `auth.type`,
563
- a provider's `surface`) with a missing or unrecognized discriminant throws
649
+ a provider's `auth.type`) with a missing or unrecognized discriminant throws
564
650
  with the valid values. Only plain data is copied. Connectors, modules,
565
651
  executors, storage, loggers, handlers, and schemas are opaque and never
566
652
  entered. An `api()` tool carries its handler, so it is checked in place (own
@@ -604,8 +690,29 @@ switched by the environment, and keep their entries under 30 lines.
604
690
 
605
691
  ### Providers and reviewed classification
606
692
 
693
+ Infisical separates explicit value reads from identifier results. `get_secret`
694
+ and `list_secrets` with `includeValues: true` return secret values and comments.
695
+ All other results, including imported secrets and pending approvals, allow only
696
+ checked IDs, keys, environment and tag slugs, paths, timestamps, positive versions
697
+ and enum fields. They omit project slugs derived from free-text names, comments,
698
+ descriptions, free-form names, reminder notes and arbitrary nested objects.
699
+ `metadataOmitted: true` reports withheld or unavailable metadata.
700
+ This assumes an honest server and protects against careless human entry in free
701
+ text. A malicious server encoding values into valid identifiers is out of scope.
702
+ Known, accepted residual risk: environment slugs are returned as required
703
+ routing identifiers. Infisical's UI derives them from environment names by
704
+ default, so secrets entered in those names can appear in returned slugs.
705
+ Operators must not put secrets in environment names or slugs. Secrets placed in
706
+ identifiers or key names are out of scope.
707
+ Write input schemas mark secret values, secret comments and folder descriptions
708
+ as `writeOnly`, so recovery argument echoes omit them without changing dispatch.
709
+ Submitted values never join the request's sent-credential set, preserving later
710
+ program arguments. Client credentials and tokens still join that set on every
711
+ call. The [provider guide](https://github.com/zackbart/connecta/blob/main/src/providers/infisical/SKILL.md) owns routing and
712
+ value-read conventions.
713
+
607
714
  A maintained provider is one `defineProvider()` call (`src/provider.ts`): a
608
- name, title, kind (`"mcp"`, `"api"`, or `"composed"`), a maintained skill, an
715
+ name, title, kind (`"mcp"`, `"api"`, `"composed"`, or `"dual"`), a maintained skill, an
609
716
  optional reviewed classification, a closed `options` shape declared with
610
717
  `optionsOf<O>()`, and a synchronous `create`. The root exports the option-shape
611
718
  combinators and `PROVIDER_COMMON` so provider authors use the same validation
@@ -621,12 +728,124 @@ live in their own folders. The eight hosted implementations, including the
621
728
  MCP branches of Notion, Vercel, and Cloudflare, use reviewed presets over
622
729
  `remoteMcp({ classify })`. The eleven API-only factories retain an internal
623
730
  adapter to the same definition and description-stamping path. GitHub composes hosted tools with a scope-enforced REST complement.
624
- Notion, Vercel and Cloudflare default to hosted MCP and select explicit REST
625
- complements with `surface: "api"`. Configure both under distinct connector ids
626
- with independent credentials when both are needed. Notion preserves its
627
- internal-integration identity under `integration_*` names; Vercel and Cloudflare
628
- retain documented REST gaps rather than duplicate hosted operations. See the
629
- [provider migration guide](./provider-migration-0.29.md).
731
+ A `"dual"` provider lets `auth` select the implementation
732
+ ([decision 0005](https://github.com/zackbart/connecta/blob/main/decisions/0005-auth-selects-implementation.md)):
733
+ `variants(["auth", "type"], …)` closes each case's options, and
734
+ `byAuth()` in `src/providers/_shared/rest/dispatch.ts` sends OAuth to
735
+ `hostedOAuth()` (`remoteMcp()`, OAuth only, the reviewed classification) and a
736
+ key to Connecta's REST connector. Stripe, Notion, Cloudflare, and Vercel are dual; `auth` is required.
737
+ The REST connector (`src/providers/_shared/rest/`) serves
738
+ `<vendor>_api_search`, `_api_details`, `_api_read`, and `_api_write` over an
739
+ `OperationIndex` that `npm run providers:spec` compiles offline from the
740
+ provider's pinned `openapi.source.json` into `openapi.generated.ts`.
741
+ Each call is matched to an operation and checked against its parameters before
742
+ transport; `_api_read` admits GET and the vendor's reviewed read-only POSTs in
743
+ its handler, since classification is per tool name. Vendors configure only a
744
+ transport, failure mapper, scope, framing, cursor extraction, a reviewed
745
+ value-safety table (required; see below), argument-dependent refusals, and an
746
+ idempotency header, plus optional hooks: `path` (fill default ids before
747
+ matching), `admit` (an awaited check in `callRest` every tool passes, such as
748
+ Cloudflare's account and zone pin), `result` (unwrap a vendor envelope;
749
+ redaction runs after it and before cursors and `select`), a reviewed
750
+ request-header allowlist, text bodies, and index `slashParams` that admit an
751
+ encoded `/` in a final key segment (`test/rest-connector.test.ts`).
752
+ Large documents opt into generator options that drop restated operation ids
753
+ and plain path parameters and cap one operation's details (`opBudget`).
754
+ `check:providers-generated` checks each index header against its source record
755
+ offline; `providers:check` reports a pin that differs from the live document.
756
+ No maintained provider selects an implementation with `surface` any more. See
757
+ the
758
+ [provider auth migration](./provider-auth-migration.md). Notion's token
759
+ connector keeps its `integration_*` projections and authoring helpers beside
760
+ the generic REST tools.
761
+
762
+ #### Value safety for REST vendors
763
+
764
+ The bar ([decision 0005](https://github.com/zackbart/connecta/blob/main/decisions/0005-auth-selects-implementation.md)):
765
+ no REST tool response, success or error, returns a credential that grants
766
+ access to the vendor or a third party, or a stored secret value, unless a
767
+ reviewed named tool exists to return it. The threat model is Infisical's
768
+ above: an honest server, with secrets people type into free text out of
769
+ scope. Field-by-field redaction over a whole vendor API does not converge, so
770
+ every vendor ships one reviewed table through one mechanism
771
+ (`src/providers/_shared/rest/value-safety.ts`):
772
+
773
+ 1. **Candidates from the pinned spec.** `scripts/value-safety.mjs` flags every
774
+ operation whose success response names or describes a credential, keyed
775
+ value, header, environment, or binding container, or whose path, id,
776
+ summary, or description names a secret family (tokens, secrets, keys,
777
+ signing, webhooks, drains, transfer, claim, and invite codes). It is the
778
+ union of the detectors Cloudflare and Vercel shipped first, and
779
+ deliberately over-inclusive. `providers:spec` writes
780
+ `value-safety.candidates.json` beside the index when the source record sets
781
+ `options.valueSafety` (vendor `operationWords`; Stripe's `expansions: false`
782
+ skips id-or-object expansions, which are their own operations' candidates),
783
+ stamped with the pinned digest, the detector format, and its options. The
784
+ file is test evidence and never ships.
785
+ 2. **One reviewed verdict per candidate**, in the vendor's `value-safety.ts`:
786
+ `refuse` (the generic tools refuse it before transport, naming the
787
+ operation and the reason), `redact` (reviewed field paths), or `safe`, each
788
+ with a reason. Paths are dot paths into the data a tool returns: a list on
789
+ the way is traversed, `[]` and `{}`/`*` address list items and map values,
790
+ `@keys` replaces secret map keys, `[?env]`, `[?credential]`, and
791
+ `[?header]` filter list items, `url:` sanitizes a URL, and `origin:` (or a
792
+ `#url` suffix) keeps only scheme and host. `keep` names reviewed metadata
793
+ the heuristic leaves (children are still checked). A vendor whose objects
794
+ repeat across operations (Stripe) reviews a field once in `fields`,
795
+ `redact` or `keep`, wherever it appears, and states `resources`: rules
796
+ keyed on the object's type discriminator (Stripe's `object`) that apply to
797
+ every such object wherever a response embeds it (an expansion, a list, an
798
+ event's `data.object` and its `previous_attributes`). A resource rule can
799
+ return a reviewed payer-facing URL verbatim only while a declarative guest
800
+ condition holds (deny by default: a Checkout page only when every field
801
+ that can bind the session to a Customer, an Account, or saved payment
802
+ methods is empty and the session only takes a payment) and withhold it
803
+ otherwise. `providers:spec` derives the binding fields from the pinned
804
+ schema, and the harness fails until the condition lists each one. Argument-dependent refusals (Stripe expansions,
805
+ Vercel `decrypt`) stay in the vendor's `refuse` hook.
806
+ 3. **Redaction on every success body**, in `callRest` after `result` and
807
+ before cursors and `select`, so named tools, HEAD data, and uploads pass
808
+ through it too: the operation's reviewed paths, then the key-name
809
+ heuristic as defense in depth. The heuristic is the union of the former
810
+ vendor engines: a credential word anywhere in a non-metadata name removes
811
+ the value whole, a credential suffix removes the strings below it,
812
+ labelled or typed secret records lose their value, environment containers
813
+ lose their values, header, cookie, and query rules lose theirs, external
814
+ destinations keep their origin, and every URL loses userinfo and
815
+ credential-named query parameters or fragments. Exemptions are never
816
+ shared; each vendor states its own.
817
+ 4. **Errors.** A reviewed operation, or an unreviewed one whose request
818
+ accepts a credential-named field, answers failures with the vendor's codes
819
+ and status but not its text, unless its verdict is wrapped in
820
+ `vendorErrors()` because a review found its errors cannot echo a stored
821
+ secret. Credentials Connecta sent are always removed by the sent-secrets
822
+ matcher.
823
+
824
+ `providers:spec` (run under tsx) also checks the table against the pinned
825
+ spec: every `redact` path and `keep` must name something the operation's
826
+ response schema can return (through `options.valueSafety.dataRoot` for an
827
+ envelope such as Cloudflare's `result`), and every resource rule path
828
+ something the resource's schema can return. It stamps the paths that resolve
829
+ into the candidates file and fails on the rest, unless
830
+ `value-safety.absent.json` acknowledges them (shared verdicts applied to
831
+ operations whose schema lacks a path, or fields a schema leaves undeclared;
832
+ such paths still apply at runtime).
833
+
834
+ `test/fixtures/value-safety.ts` is the shared harness each vendor's
835
+ `value-safety.node.test.ts` runs; `reviewProblems` is pure, and
836
+ `test/value-safety-harness.node.test.ts` proves wrong paths fail it. It fails
837
+ on candidates from another pin, format, or option set; an unreviewed
838
+ candidate; a verdict for an operation the index lacks; a table path the stamp
839
+ lacks (unresolved, or edited since `providers:spec`); a flagged field no path,
840
+ keep, field review, or scope map covers; a flagged field, built in the shape
841
+ the pinned schema gives it, that survives redaction or that no redact path
842
+ reaches; a keep that exempts anything below it or a vendor-wide keep of a
843
+ credential name; a redact path or resource rule that leaves its value at the
844
+ top level, in a list, an expansion, or an event; a refusal that reaches
845
+ transport; a reviewed operation that echoes vendor error text; and a moved
846
+ verdict count.
847
+ `RestVendor.valueSafety` is required, so a new REST vendor cannot ship without
848
+ a table.
630
849
 
631
850
  `remoteMcp({ classify })` is the public way to declare what a downstream's
632
851
  tools do: `{ tools: { name: "read" | "write" | "destructive" | { verdict,
@@ -1010,82 +1229,7 @@ compiling and configuring the real thing.
1010
1229
 
1011
1230
  ## Operator data contract
1012
1231
 
1013
- `GET /ui/api/config`, owned by `operatorUi()`, returns `OperatorUiContract`
1014
- from `connecta/ui`. `src/operator-ui/contract.ts` is also the browser's type
1015
- source. The response has `schemaVersion: 1`, `config`, `configSources`, `live`, and `you`.
1016
- `config` is the construction-time `describeConfig()` snapshot, filtered to
1017
- visible connectors, granted tools, classification overrides, and admitted
1018
- pools. Pool tools are the intersection with the caller's identity grants.
1019
- `configSources` labels each serialized leaf as `default` or `config`, based on
1020
- option presence rather than equality with the default. Factory descriptions carry
1021
- relative `optionSources` paths so resolved module and transport defaults retain
1022
- their provenance. The snapshot copies only resolved values; the UI consumes
1023
- these presence facts separately. Its paths use connector
1024
- ids and pool names and are filtered after disclosure; hidden values leave no
1025
- provenance keys. It contains no source code or credential material.
1026
- Every response is private, `no-store` JSON. Only GET is allowed, with the
1027
- same auth gate and identity partition as `/ui/data` and `/ui/connectors/:id`.
1028
-
1029
- `live.connectors` carries status and problem codes, complete registry tools
1030
- with their final read/write classification, catalog age in milliseconds, and
1031
- last-call time/outcome. Catalog descriptions and schemas are allowed for an
1032
- authenticated reader; grammar-failing names and addresses become `<withheld>`.
1033
- `auth.registrationPath` reports the selected downstream OAuth client mechanism when known.
1034
- Status prose, error text, credential values or suffixes, arguments, results,
1035
- and code are excluded. Static and unobserved catalogs have a null age;
1036
- persisted catalogs retain their original fetch time. Probes run with bounded
1037
- concurrency and the configured probe deadline, and release their scopes.
1038
-
1039
- Last-call lookup requires the existing activity permission and read gate. It
1040
- scans up to 1,000 recent rows, filters connector/tool grants, and restricts
1041
- personal connectors to the caller's activity identity. Each retained fact
1042
- copies a validated timestamp and outcome only. A null call means unknown or
1043
- absent in that window, not proof of no calls. `live.activity` distinguishes
1044
- available, unconfigured, forbidden, and unavailable history. `you` names no
1045
- identity or auth material; it reports grants, root and admitted-pool trust,
1046
- and effective activity, token, and per-connector auth permissions.
1047
-
1048
- `connecta doctor --config` fetches this authenticated contract and prints only
1049
- `config` as indented JSON. It requires the UI module and the existing doctor
1050
- authentication environment variables; it runs no diagnostic program, follows
1051
- no redirects, and prints no raw HTTP failure body.
1052
-
1053
- The operator shell uses React, TanStack Router/Query/Table, Radix, cmdk and
1054
- Tailwind v4. Inter, CSS, JavaScript and dependency notices are hashed assets
1055
- under `/ui/assets/*`, served identically with immutable cache headers on Node
1056
- and Workers. HTML contains only mount points and escaped inert configuration;
1057
- it remains uncached. `generated.ts` is ignored and generated before build and
1058
- test; `check:operator-ui` detects stale assets and shared page styles. The UI
1059
- stays behind `./ui`, outside the root import graph. The identity-fenced store
1060
- owns authenticated reads and existing mutations. Overview, Connectors, connector
1061
- detail, Tools, Access and Config consume the typed contract. Activity retains
1062
- its authenticated data routes. Connector detail tabs live
1063
- in the URL hash; Activity filters live in the query string and apply to loaded
1064
- history. Both `/ui/activity` and `/ui/api/activity` require Activity access and
1065
- pass the optional activity `readGate`. Interactive operators and machine callers
1066
- admitted by `identity.activityAccess` share the same history disclosure rules.
1067
- Connector/tool grants and grants for the recorded pool filter each event. Shared
1068
- connector history can include other principals' events, with recorded request
1069
- IDs, validated client name/version and package version, and actor kind, ID and
1070
- namespace. Interactive reads may also add directory actor labels; machine reads
1071
- never add them. Personal connector history is owner-only: the event must have a
1072
- checked `principal` actor basis and actor ID/namespace matching the reader's
1073
- admitted principal. Rows without a provable personal owner are withheld. Activity
1074
- carries no arguments, results, code or raw downstream errors. The last-call
1075
- overlay uses the same event filter but returns only timestamp and outcome.
1076
- Calls group only by recorded request ids, with missing ids shown as
1077
- separate calls. Classification and result size are shown only when recorded. The verdict is
1078
- captured after registry resolution, and result size counts UTF-8 bytes of the
1079
- downstream value before paging or truncation. Nullable SQL columns preserve old
1080
- rows. Catalog changes use the same paging envelope with `kind: "catalog_drift"`,
1081
- `source: "catalog_refresh"` and a checked `catalog_changed` fact containing only
1082
- added, removed and changed tool counts. The first complete catalog is a baseline;
1083
- one refresh-publication hook emits each later change. It includes the request's
1084
- id and typed actor when available, otherwise a fresh id and system actor. No
1085
- names, descriptions or schemas enter the event. Private catalog changes require
1086
- a matching principal owner even when the connector uses shared auth. Empty credential declarations
1087
- report `credential_required` before running a connector status or catalog probe.
1088
- The Access adapter reports inbound provider kinds, admitted pools, grants, trust
1089
- and endpoint setup; token controls retain their existing permission gate.
1090
- Configuration remains in deployment code. The UI mutates only credentials,
1091
- OAuth connections and client tokens through the existing routes.
1232
+ The contract now lives in [Operator UI](./operator-ui.md#operator-data-contract),
1233
+ including configuration provenance, live facts, activity disclosure, doctor,
1234
+ and the browser implementation. [Optional deployment modules](#optional-deployment-modules)
1235
+ keeps the module composition and request-lifetime rules here.
@@ -1,5 +1,28 @@
1
1
  # Inbound auth
2
2
 
3
+ ## Task routes
4
+
5
+ [Agent index](./README.md) distinguishes endpoint operation from deployment
6
+ and package maintenance. This guide owns identity, grants, and downstream
7
+ credential contracts; use [deployment setup](./deploying.md) for runnable steps.
8
+
9
+ | Task | Sections |
10
+ | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
11
+ | Connect a machine or migrate an old client | [Managed tokens](#managed-client-tokens-and-upgrading-from-v023), [static bearer migration](#migrating-static-bearer-clients) |
12
+ | Configure caller visibility, trust, or auth ownership | [Principals and operators](#principals-visibility-and-operators), [pools](#pools), [shared and personal auth](#shared-and-personal-auth) |
13
+ | Set up human sign-in | [Access on Workers](#cloudflare-access-on-workers), [Clerk tokens and sessions](#clerk-oauth-tokens-and-operator-sessions), [management](#human-authentication-management) |
14
+ | Enable credential or connection controls | [Management permissions](#management-permissions), [start/restart/disconnect](#starting-restarting-and-disconnecting) |
15
+ | Diagnose consent or token refresh | [State at rest](#downstream-oauth-state-at-rest), [callbacks](#consents-and-callbacks), [refresh failures](#refresh-failures), [safe errors](#what-a-servers-errors-may-say) |
16
+ | Configure a downstream OAuth client or HTTP handler | [Client registration](#downstream-oauth-client-registration), [api() OAuth](#downstream-oauth-on-api), [request-local bearer tokens](#request-local-downstream-bearer-tokens) |
17
+ | Review browser or downstream URL boundaries | [Origins](#origins), [advertised URLs](#urls-a-downstream-advertises), [one server per grant](#one-server-per-grant) |
18
+
19
+ [Credential recognition and admission](#credential-recognition-and-admission)
20
+ defines refusal precedence.
21
+ [Source and tests](#source-and-tests) points to the implementation. Migration
22
+ sections describe upgrade procedures, not additional auth modes to enable.
23
+
24
+ ## Credential recognition and admission
25
+
3
26
  Inbound auth decides who may reach the MCP endpoint. People use `clerkAuth`
4
27
  from `@zackbart/connecta/auth/clerk` or `cloudflareAccessAuth` from
5
28
  `/auth/cloudflare-access`. Machines use `accessTokens(storage)` from
@@ -369,7 +392,14 @@ longer live; one left behind cannot complete and expires with its link.
369
392
  | `DELETE /ui/oauth/<id>` | Disconnects and invalidates the cached catalog, even if the browser leaves. |
370
393
 
371
394
  `authorize_connector` issues the same browser link, with `force` carried in the
372
- signed handoff. Status reads never call `startAuth` and never expose the
395
+ signed handoff. On hosts without URL elicitation, direct and program auth
396
+ failures and search/describe catalog failures include the same authorization
397
+ handoff. The admitted registry and management permission govern issuance.
398
+ Each call or program run shares at most one handoff per connector; errors never
399
+ request a forced restart. Signed links retain their fifteen-minute expiry,
400
+ initiating-user and owner binding, origin check, and single-use browser nonce.
401
+ Minting a link stores no nonce and starts no consent. When an auth error includes
402
+ a handoff, give it to the user or operator. Status reads never call `startAuth` and never expose the
373
403
  provider's consent URL. A UI start answers `{ state: "auth_required",
374
404
  authorizationUrl }`, where the URL belongs to connecta. Continue hands back the
375
405
  latest consent only while it is unclaimed, in the live epoch, written within
@@ -574,7 +604,7 @@ withheld error keeps its original classification, and no connector error keeps
574
604
  a runtime, stream, or parser error as `cause`.
575
605
 
576
606
  The JSON-RPC and 4xx exception preserves diagnostics agents need to correct
577
- their arguments. Core redacts credentials used by that call before these
607
+ their arguments. Core redacts credentials and submitted `writeOnly` argument values before these
578
608
  messages, nested causes/data, or tool results reach an agent or guest program.
579
609
  The memory-only set covers credential-slot values, static auth headers,
580
610
  outbound bearer tokens, and their JSON-escaped, URL-encoded, base64 and
@@ -586,6 +616,85 @@ maintained-provider transports track the final request too. Credential-slot
586
616
  reads cover values sent in headers, queries, or bodies. Echoed sensitive
587
617
  header lines are also withheld.
588
618
 
619
+ Submitted values under JSON Schema `writeOnly: true` join this memory-only
620
+ request set, including nested objects, arrays, and local references. Strings
621
+ and numbers register with the same raw, URL, form, base64, and base64url forms
622
+ as credentials; booleans and null carry no text. Property names inside a
623
+ private object subtree are private values too; array indices are not. Output
624
+ redaction protects the union of private fields across `oneOf`/`anyOf`
625
+ alternatives. If a schema declares private fields but its sensitivity cannot
626
+ be resolved, all submitted values are treated as private. Schemas without a
627
+ `writeOnly` annotation skip this work. Private values never enter stored
628
+ catalogs or operator records. The program's outgoing-argument filter and
629
+ catalog caches use credentials alone, so later calls receive the original
630
+ submitted data and cached catalogs never depend on one request's arguments.
631
+
632
+ Payload rules apply where downstream or guest data enters: downstream results
633
+ and errors, resource reads, connector usage guides and downstream skill files,
634
+ program returns, logs, and emits, and relayed input prompts. JSON objects,
635
+ arrays, and string literals are parsed and walked, whether they are wire text
636
+ or a string inside a result, so nested JSON strings are decoded too. Every
637
+ decoded string leaf and property name is scanned, and a number, boolean, or
638
+ null whose canonical text matches becomes the string `[redacted]`, so the JSON
639
+ stays valid. Text is reserialized
640
+ only when something changed; otherwise its original bytes remain. The
641
+ serialized bytes are then scanned raw, and a literal the walk cannot see, such
642
+ as one spanning tokens, withholds the whole text. Other text is scanned raw and
643
+ through one more layer of JSON string escapes at a time while the last layer
644
+ could still hold an escape. A string is decoded through at most eight layers in
645
+ total, counting the nested JSON strings it was parsed from; text that would
646
+ need more is withheld. Every match is mapped back to the original text,
647
+ overlapping and adjacent spans merge, and each merged span is replaced once, so
648
+ one match can never split another. A span wholly inside an existing
649
+ `[redacted]` placeholder is that placeholder; a private value that contains the
650
+ placeholder text is still redacted. Connecta and MCP envelope field names, tag
651
+ values such as a content `type` or result `format`, and typed facts such as
652
+ `isError` and `retryable` are copied unchanged. The agent boundary around
653
+ Connecta's own envelopes repeats credential and literal private-value
654
+ replacement without the short-value rules, so envelope identifiers, counts, and
655
+ JSON-RPC ids are never rewritten.
656
+
657
+ Empty private strings are exempt because they contain nothing to leak. A
658
+ non-empty private value shorter than eight characters redacts a structured
659
+ string leaf, property name, or scalar only when it equals the value or a
660
+ supported encoded form, including after JSON unescaping. A longer structured
661
+ field that merely contains it, such as `prod-db` for `prod`, is kept: this
662
+ documented tradeoff keeps identifiers and approval state usable. Prose that
663
+ contains a short value or encoded form anywhere is withheld as `[redacted]`:
664
+ plain-text results, `message`, `error`, `text`, `logs`, and `stack` strings,
665
+ string program results, and relayed prompts. Typed error classification,
666
+ retryability, retry delay, and write-outcome accounting remain. Private values
667
+ of at least eight characters are replaced wherever they occur.
668
+
669
+ Matching runs one Aho-Corasick automaton over every registered form, so each
670
+ scanned code unit costs amortized constant work however many values are
671
+ registered. The automaton indexes the first 256 code units of each form,
672
+ credentials first, and at most 512 Ki indexed units for every registration
673
+ together, so a long credential or private value costs no more automaton memory
674
+ than its prefix. Where a longer form's prefix matches, the scanned view's
675
+ rolling hash confirms the rest once its last unit arrives; equal text always
676
+ confirms, so a hash collision could only redact more. Forms that share a prefix
677
+ and a length share each confirmation, and overlapping prefix occurrences in
678
+ self-similar text share one run of confirmations, so periodic text stays exact.
679
+ Each confirmation is charged to the call's work budget before it is queued, and
680
+ one scanned view holds at most 16 Ki runs; a view that needs more is withheld.
681
+ Submitted values register at most 512 KiB of encoded forms and 4,096
682
+ argument nodes. A form that does not fit either limit withholds every string
683
+ at least as long as that form; an argument tree that cannot be walked withholds
684
+ all downstream text.
685
+
686
+ One redaction call has one work budget across parsing and every scanned view:
687
+ 64 Mi units with private values, and 1 Gi for credentials alone, which keeps
688
+ multi-MiB document and skill-file reads. Work that does not fit the remaining
689
+ budget withholds its field, so smaller identifiers can still be checked. The
690
+ same call walks structured values iteratively, at most 512 containers deep and
691
+ 1 Mi entries in total, and copies only containers on a path that changed. A
692
+ container's entries are counted before it is walked. One that does not fit the
693
+ remaining entries is scanned as serialized text instead: kept unchanged when
694
+ nothing registered appears, otherwise withheld. A cycle or a container past the
695
+ depth limit is withheld. No limit throws or exposes a matcher diagnostic, and
696
+ typed success outcomes remain.
697
+
589
698
  Only credentials of at least eight characters enter the redaction and structural
590
699
  matchers. Credentials shorter than 8 characters are not redacted from echoes;
591
700
  use longer secrets. Matching short secrets can corrupt JSON-RPC fields or refuse
@@ -594,7 +703,7 @@ this floor produces one operator warning per connector with the typed code
594
703
  `short_secret_not_redacted` and no credential value. Remote connectors warn at
595
704
  construction; static `api()` OAuth warns on its first provider operation.
596
705
  Connecta's own messages never quote credential values, regardless of length.
597
- One matcher is cached until the secret set changes, and an empty set skips matching. Redaction runs after
706
+ One automaton is cached until the secret set changes, and an empty set skips matching. Redaction runs after
598
707
  JSON unwrapping or joining text blocks and on final serialized text and every
599
708
  structured string, before result paging, emits, and program outputs/errors/logs. See
600
709
  [the agent boundary](./architecture.md#errors-and-records).
@@ -962,8 +1071,14 @@ still serve configured credentials. Both routes accept only GET, otherwise retur
962
1071
  `form_post` is unsupported. Clerk's session-only policy applies regardless of method.
963
1072
 
964
1073
  The fifteen-minute signed handoff carries connector ownership, initiating principal, deployment origin, nonce,
965
- and restart mode. Configure `vault: encryptedCredentialVault(storage, key)`, which derives its HMAC key with
966
- HKDF. Custom vaults need `signOAuthHandoff` and `verifyOAuthHandoff`; without a signing vault, links cannot be
1074
+ restart mode, and any predecessor link binding. New links use versioned, length-prefixed fields instead of
1075
+ JSON, with unpadded base64url payload and signature segments. Ordinary handoffs use an opaque wire encoding,
1076
+ not encryption; URL elicitation also seals those fields as described above. Copy the complete URL unchanged.
1077
+ The signature authenticates the exact version and payload text. Tampered or re-encoded links return
1078
+ `Invalid or expired connection link. Request a new link from connecta.` Pre-upgrade signed JSON and sealed
1079
+ v2 links still verify against their original text and expire after their original fifteen minutes.
1080
+
1081
+ 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
967
1082
  issued or accepted. A link grants no identity. After identity and permissions pass, its nonce is consumed
968
1083
  before OAuth starts, even while consent is pending. Failed starts need new links; nonce markers expire with
969
1084
  their links.
@@ -1145,3 +1260,21 @@ is retained across requests, and rejected writes are never replayed. An omitted
1145
1260
  `app.privateKey` uses the encrypted connector vault's `privateKey` field;
1146
1261
  `describe()` includes neither the key nor the minted tokens. The maintained
1147
1262
  provider skill explains App permissions and installation prerequisites.
1263
+
1264
+ ## Source and tests
1265
+
1266
+ Inbound adapters live in [src/auth](https://github.com/zackbart/connecta/tree/main/src/auth);
1267
+ [auth routes](https://github.com/zackbart/connecta/blob/main/src/routes/oauth.ts)
1268
+ own connection handoffs and callbacks. Check [inbound auth tests](https://github.com/zackbart/connecta/blob/main/test/inbound-auth.test.ts),
1269
+ [Access tests](https://github.com/zackbart/connecta/blob/main/test/cloudflare-access-auth.test.ts),
1270
+ and [identity-scope tests](https://github.com/zackbart/connecta/blob/main/test/identity-scope.test.ts)
1271
+ for credential precedence and disclosure boundaries.
1272
+
1273
+ Downstream OAuth implementation and evidence are in
1274
+ [downstream-oauth.ts](https://github.com/zackbart/connecta/blob/main/src/auth/downstream-oauth.ts),
1275
+ [static-oauth.ts](https://github.com/zackbart/connecta/blob/main/src/auth/static-oauth.ts),
1276
+ [OAuth connect tests](https://github.com/zackbart/connecta/blob/main/test/oauth-connect.test.ts),
1277
+ [callback fencing](https://github.com/zackbart/connecta/blob/main/test/oauth-callback-fencing.test.ts),
1278
+ [refresh leases](https://github.com/zackbart/connecta/blob/main/test/oauth-refresh-lease.test.ts),
1279
+ and [API OAuth tests](https://github.com/zackbart/connecta/blob/main/test/api-oauth.test.ts).
1280
+ Use the test pointers within each section for narrower failure cases.