@zackbart/connecta 0.18.1 → 0.18.3

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 (38) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +4 -0
  3. package/dist/catalog-service.d.ts +4 -0
  4. package/dist/catalog-service.js +22 -6
  5. package/dist/connectors/remote-mcp.d.ts +49 -2
  6. package/dist/connectors/remote-mcp.js +302 -7
  7. package/dist/invocation.js +9 -3
  8. package/dist/providers/linear.d.ts +7 -1
  9. package/dist/providers/linear.js +12 -2
  10. package/dist/providers/mixpanel.d.ts +5 -1
  11. package/dist/providers/mixpanel.js +13 -2
  12. package/dist/providers/revenuecat.d.ts +3 -1
  13. package/dist/providers/revenuecat.js +12 -3
  14. package/dist/providers/stripe.d.ts +9 -3
  15. package/dist/providers/stripe.js +24 -5
  16. package/dist/registry.d.ts +7 -0
  17. package/dist/registry.js +9 -0
  18. package/dist/result-shapes.d.ts +13 -0
  19. package/dist/result-shapes.js +331 -0
  20. package/dist/skills.js +3 -3
  21. package/dist/version.d.ts +1 -1
  22. package/dist/version.js +1 -1
  23. package/documentation/architecture.md +5 -2
  24. package/documentation/code-mode.md +6 -6
  25. package/documentation/connectors.md +35 -0
  26. package/documentation/linear.md +25 -4
  27. package/documentation/meta-tools.md +22 -3
  28. package/documentation/mixpanel.md +19 -0
  29. package/documentation/operations.md +3 -1
  30. package/documentation/provider-conventions.md +37 -21
  31. package/documentation/revenuecat.md +23 -1
  32. package/documentation/storage-and-credentials.md +55 -0
  33. package/documentation/stripe.md +22 -2
  34. package/documentation/upgrading.md +32 -4
  35. package/ethos.md +3 -3
  36. package/examples/worker/src/index.ts +5 -0
  37. package/package.json +1 -1
  38. package/templates/node/package.json +1 -1
@@ -78,21 +78,35 @@ headed to `call_tool` or generated code; `safety: "approvalRequired"` finds the
78
78
  complementary set that must cross `call_destructive_tool`. Omitting `safety`,
79
79
  or setting it to `"all"`, preserves the complete configured catalog. This is
80
80
  only a discovery filter: it neither grants authority nor changes invocation admission.
81
- `includeSchemas: "compact"` adds each match's input and any declared output
82
- shape. Bounded plain-object schemas also expose `inputKeys`,
81
+ `includeSchemas: "compact"` adds each match's input and any provider-declared
82
+ output shape. When the provider declared none but an earlier successful call
83
+ learned one, the same field carries the open observed schema beside
84
+ `outputSchemaSource: "observed"`. That marker matters: observed fields and broad
85
+ JSON types are routing evidence, not a provider contract, and every object field
86
+ remains optional and open to unseen names. A provider declaration always wins.
87
+ Bounded plain-object schemas also expose `inputKeys`,
83
88
  `requiredInputKeys`, and `outputKeys`; a zero-input object keeps
84
89
  `requiredInputKeys: []`, while an output object with no declared properties
85
90
  omits `outputKeys`. A truncated shape omits its corresponding list rather than
86
91
  repeating a large partial inventory. Matches carry declared
87
92
  behavior annotations. Lexical rank is only one signal: select a candidate whose
88
93
  required inputs are available, whose schema is complete enough for the call,
89
- and whose safety and declared outputs fit the work. A reducer uses `outputKeys`
94
+ and whose safety and available outputs fit the work. A reducer uses `outputKeys`
90
95
  before inspecting the value; it does not assume a collection is named `items`
91
96
  or `results`. When that shape is sufficient, call the returned address directly. Reserve schema
92
97
  expansion through `connecta.describe` for a search without schemas, an
93
98
  ambiguous compact shape, or exact
94
99
  constraints that require `format: "json"`.
95
100
 
101
+ Observed schemas originate no provider traffic. A successful explicitly
102
+ read-only call the user already made contributes names and broad types after
103
+ Connecta unwraps the result. Arguments, scalar values, raw results, code,
104
+ credentials, and errors are not retained, though property names may themselves
105
+ be user-authored. Shapes merge in a 256-entry runtime cache for 24 hours under
106
+ the exact tool definition that produced them. A changed definition, process
107
+ restart, or Worker isolate eviction starts cold. Observation cannot fail the
108
+ call, and the declared catalog remains the fallback.
109
+
96
110
  Compact search is deliberately a routing view, not a second copy of connector
97
111
  documentation. Tool purposes are capped at 160 characters, connector
98
112
  descriptions and property prose are omitted, required input fields render
@@ -310,6 +324,11 @@ handoff:
310
324
  declared credential label and field names/guidance; or
311
325
  - `unavailable`: an honest deployment/configuration message.
312
326
 
327
+ The class follows what the connector declares, not how it was authored: a
328
+ `remoteMcp()` connection using `auth: { type: "credential" }` declares a slot
329
+ and no OAuth flow, so it lands in `operator_config` beside every `api()`
330
+ credential.
331
+
313
332
  The tool accepts no secret. `force` applies only to OAuth and may discard its
314
333
  stored grant before restarting consent. Static credential values are written
315
334
  only through the same-origin, Clerk-operator credential route. After OAuth
@@ -50,6 +50,25 @@ password, not ordinary configuration. Mixpanel currently labels service-account
50
50
  MCP authentication beta. Prefer OAuth unless the deployment is intentionally
51
51
  headless.
52
52
 
53
+ The same service account can arrive from `/credentials` instead, and there the
54
+ operator pastes the readable pair rather than an encoded blob:
55
+
56
+ ```ts
57
+ mixpanel("automation_analytics", {
58
+ purpose: "Headless release-health reporting",
59
+ auth: { type: "credential" },
60
+ });
61
+ ```
62
+
63
+ The slot renders as "Service account" and takes `username:secret`. Connecta
64
+ base64-encodes it and sends Mixpanel's documented `Bearer Basic` framing, so the
65
+ operator never has to encode anything by hand. **The two paths take different
66
+ strings:** the `headers` example above wants the already-encoded blob
67
+ (`echo -n "username:secret" | base64`), and this one wants the plaintext pair.
68
+ Migrating from one to the other means decoding, not copying. Until a value is
69
+ saved the connector is present and reports `auth_required`. See
70
+ [storage and credentials](./storage-and-credentials.md#a-remote-mcp-connectors-static-credential).
71
+
53
72
  ## Conditional input contracts
54
73
 
55
74
  Mixpanel's hosted descriptions enforce three cross-field conditions that its
@@ -75,7 +75,7 @@ optional.
75
75
  | `connectors` | — (required) | the connector set ([connectors](./connectors.md)) |
76
76
  | `executor` | — (required) | the sandbox `execute_code` runs in ([code mode](./code-mode.md#what-an-executor-must-implement)) |
77
77
  | `auth?` | none ⇒ open (dev only) | one `InboundAuth` or an array; bearer providers are checked before Clerk ([inbound auth](./auth.md)) |
78
- | `storage?` | `memoryStorage()` | the one state seam ([storage](./storage-and-credentials.md)) |
78
+ | `storage?` | `memoryStorage()` | the one state seam for catalogs, result paging, credentials, and access tokens ([storage](./storage-and-credentials.md)) |
79
79
  | `publicUrl?` | per-request origin | public base URL; an HTTPS value also redirects inbound HTTP |
80
80
  | `logger?` | `console`, prefixed `[connecta]` | `{ debug, info, warn, error }` |
81
81
  | `branding?` | neutral Connecta defaults | operator-page and OAuth result-page labels and marks |
@@ -247,8 +247,10 @@ in.
247
247
  | `provider-conventions.test.ts` | the conventions a test can hold: hand-written providers refusing schemas they cannot enforce (H5), their compact discovery schemas staying complete (H7), Cloudflare stating its second pagination convention in the schema (H10), and Notion saying it has no escape hatch (H14) |
248
248
  | `registry.test.ts` | construction and id validation, startup warnings, address resolution, catalog TTL/persistence/completeness, agent-only stale-while-revalidate with cross-request single-flight shared with blocking reads in both start orders, owned teardown, invalidation/fingerprint guards, blocking diagnostics, and broken-connector isolation |
249
249
  | `remote-mcp.test.ts` | `remoteMcp()` against an in-process server through the `_transportFactory` seam: passthrough, downstream `isError`, Workers-safe output-schema validation, request-scoped client reuse and at-most-once scope close; plus the real transport's manual redirect policy, destination guard, credential containment, and downstream session termination |
250
+ | `remote-mcp-credential.test.ts` | `remoteMcp()` drawing a static key from `/credentials`: the declared slot and its refusal of named fields and bad header names, header framing (bearer, bare, and the two `Basic` forms) observed on the wire, an empty slot failing as `auth_required` rather than reaching the downstream, a value carrying a control character refused before framing and absent from every surface — `call_tool`, `status`, the Test result, `lastError`, and the thrown error — rotation replacing the cached client and a connect already in flight while a wiped value fails the next call, the Test action's catalog probe and scope close, the cleartext-destination warning, and the vault and `authorize_connector` handoff end to end |
250
251
  | `remote-mcp-pagination.test.ts` | the `tools/list` cursor chain in both directions — exact cursor handoff, first-wins dedup, a failed later page rejecting rather than returning its prefix, the runaway backstops, the tool-metadata re-prime across pages, and paginated catalogs reaching the discovery path |
251
252
  | `request-admission.test.ts` | `/mcp` bounded before auth, the stable 503 and `Retry-After`, health and operator responsiveness under saturation, payload-free counters, queued cancellation, shutdown rejection while active work drains, and the separate fallback code pool |
253
+ | `result-shapes.test.ts` | passive output-shape learning: value-free bounded inference, merging, 256-entry LRU eviction, 24-hour expiry, runtime isolation, read-only admission, declared-schema precedence, definition-change invalidation, discovery provenance, and failure isolation |
252
254
  | `revenuecat-provider.test.ts` / `revenuecat-registry.test.ts` | the RevenueCat proxy's per-project key scoping and account-wide OAuth guides, its purpose-bearing summary, the argued borderline verdicts in its digest-free manifest, and the deliberately unclassified `render-paywall-screenshot`; then two project-scoped keys as two connectors in a real deployment |
253
255
  | `server.test.ts` | end-to-end `/mcp` (401 → compact initialize instructions → seven compact definitions with bounded connector inventory → complete usage skill → `call_tool`), conditional guide pointers, open routes, Clerk `.well-known` metadata without network, code mode, and deferred catalog reads through both discovery surfaces |
254
256
  | `server-route-contracts.test.ts` | the route contracts `server.ts` must keep byte-identical: every built-in answered ahead of connector routes inside the security wrapper, open data-free shells with framing denied, per-route auth and same-origin requirements with exact 401/403/405 bodies, and OAuth `verifyState`-before-`finishAuth` ordering |
@@ -412,33 +412,49 @@ retries.
412
412
 
413
413
  OAuth per connector instance, stored in connector-scoped storage, is the
414
414
  default. The provider's own headless credential — a personal API key, a
415
- restricted key, a service account — is supported through explicit `headers`
416
- auth, documented as a secret rather than configuration, and paired with the
417
- narrowest mode the deployment can use. `requireHttps` is set. Recovery from an
418
- expired authorization is the ordinary `auth_required` → `authorize_connector`
419
- route.
415
+ restricted key, a service account — is supported two ways: explicit `headers`
416
+ auth, documented as a secret rather than configuration, and `{ type:
417
+ "credential" }`, which declares an operator slot and takes the same secret from
418
+ `/credentials` instead. Either way it is paired with the narrowest mode the
419
+ deployment can use, and the framing matches the provider's *published* contract
420
+ for the MCP endpoint — not a convention borrowed from that provider's other
421
+ APIs, and not this repository's earlier example, which is the same claim wearing
422
+ a circle. `requireHttps` is set. Recovery from an expired authorization is the
423
+ ordinary `auth_required` → `authorize_connector` route, which returns the
424
+ consent URL for OAuth and the `/credentials` handoff for a declared slot.
420
425
 
421
426
  *Why:* one route back from an expired credential is what keeps a failed call
422
427
  from becoming an abandoned task. *Cost:* wrong-tool selection.
423
428
 
424
- ### P10 — There is no credential test; the equivalent check happens at construction
429
+ ### P10 — Nothing probes a credential unasked; a declared slot may be tested on request
425
430
 
426
- A proxy declares no operator credential slot and implements neither
427
- `testCredential` nor `testCredentials`. `remoteMcp()` has no `credential`
428
- option, and neither shape of proxy credential is vault-managed: OAuth lives in
431
+ A proxy declares an operator credential slot exactly when its auth is `{ type:
432
+ "credential" }`, and then it inherits H12 whole
433
+ ([#439](https://github.com/zackbart/connecta/issues/439)). The other two shapes
434
+ declare no slot and hold nothing for the credentials page: OAuth lives in
429
435
  connector-scoped storage and is exercised by the authorization flow itself,
430
- while a headless key arrives as deployment configuration in `headers`, so there
431
- is nothing for the operator credentials page to hold or test. H12's guarantee is
432
- still owed, and a proxy pays it in two other places: construction throws when a
433
- recognizable credential contradicts the declared mode (P4), and a dead or
434
- revoked credential fails loudly at use as `auth_required` with the
435
- `authorize_connector` route attached (P9). Connecta never probes a downstream to
436
- see whether a credential is still alive — that shape is `removed` in the ethos
437
- ([#179](https://github.com/zackbart/connecta/issues/179)). A provider that later
438
- does take a vault-managed secret inherits H12 whole.
436
+ while a `headers` key arrives as deployment configuration. H12 is owed in every
437
+ shape, and a proxy pays it in two places that do not depend on a slot:
438
+ construction throws when a recognizable credential contradicts the declared mode
439
+ (P4) a check a vault-managed key cannot get, because there is nothing in the
440
+ deployment file to read and a dead, revoked, or absent credential fails loudly
441
+ at use as `auth_required` with the `authorize_connector` route attached (P9).
442
+
443
+ `testCredential` exists only behind the operator-pressed Test action on
444
+ `/credentials`, and only for a declared slot. It connects with the stored value
445
+ and reports how many tools the downstream served, which is the whole honest
446
+ check for a proxy: which account, project, or mode a key reaches is the
447
+ provider's answer, not Connecta's. That is not the shape
448
+ [#179](https://github.com/zackbart/connecta/issues/179) removed. What was
449
+ removed is the *unasked* probe — a liveness call every deployment pays on a
450
+ schedule or at startup to answer a question only a misconfigured one has. A
451
+ human clicking Test has asked, `api()` has had that button since the vault
452
+ existed, and nothing here probes on its own: no timer, no warmup, no check on
453
+ the read path.
439
454
 
440
455
  *Why:* an unasked-for liveness probe spends a call on every deployment to answer
441
- a question only a misconfigured one has. *Cost:* result size.
456
+ a question only a misconfigured one has; a requested one spends a call the
457
+ person requesting it chose. *Cost:* result size.
442
458
 
443
459
  ### P11 — Connecta classifies the transport; the downstream owns the tool error
444
460
 
@@ -666,8 +682,8 @@ than by reading:
666
682
  | P4 | endpoint or mode option exists, with the documented default (or no default, where none is safe) |
667
683
  | P5 | reads and writes are named lists; an unlisted tool resolves to not-read-only; a reviewed destructive name beats a contradictory `readOnlyHint: true` |
668
684
  | P6, P8 | the guide contains the catalog-varies note and the id-resolution rule |
669
- | P9 | `auth` defaults to OAuth and `requireHttps` is set |
670
- | P10 | no `credential`, `testCredential`, or `testCredentials` on the wrapper; the mode/key contradiction throws at construction instead |
685
+ | P9 | `auth` defaults to OAuth and `requireHttps` is set; a credential-auth shape frames the key the way the provider's MCP documentation does |
686
+ | P10 | a `credential` slot exactly when auth is `{ type: "credential" }`; `testCredential` runs only from the operator's Test action, never on a timer or a read path; the mode/key contradiction still throws at construction |
671
687
  | P11 | an authorization failure surfaces as `auth_required`; a downstream tool error is returned unchanged, with no code chosen from its prose |
672
688
  | P12 | a declared budget matches a citable documented limit, or the absence is justified in the guide |
673
689
  | P13 | classification lists are maintained in one place per provider and built into the manifest the wrapper classifies from, so the drift check compares against the same fact the caller is served |
@@ -80,6 +80,26 @@ connectors: [
80
80
  ]
81
81
  ```
82
82
 
83
+ Neither key has to be a runtime secret. Declare the slot instead and each
84
+ connector's key is pasted, tested, and rotated on `/credentials`:
85
+
86
+ ```ts
87
+ connectors: [
88
+ revenuecat("bepresent_ios", {
89
+ purpose: "Subscription state for the BePresent iOS project",
90
+ auth: { type: "credential", credential: { label: "API v2 secret key" } },
91
+ }),
92
+ revenuecat("biblescroll", {
93
+ purpose: "Subscription state for the BibleScroll project",
94
+ auth: { type: "credential", credential: { label: "API v2 secret key" } },
95
+ }),
96
+ ]
97
+ ```
98
+
99
+ Two ids, two slots, two single-project catalogs — the `credential` option is
100
+ optional, and omitting it gives the same "API v2 secret key" label. See
101
+ [storage and credentials](./storage-and-credentials.md#a-remote-mcp-connectors-static-credential).
102
+
83
103
  That is config-as-code doing what an account model would otherwise do: one
84
104
  credential per connector, each with its own catalog, storage namespace, health,
85
105
  and admission counters. The two share a title, because Connecta cannot know
@@ -116,7 +136,9 @@ Keys are prefixed `sk_`, are issued read-only or write-enabled, and can be
116
136
  revoked at any time by a project Admin. RevenueCat's setup guidance is to "use
117
137
  a write-enabled key if you plan to create/modify resources"; "a read-only key
118
138
  works if you only need to view data". Keep the key in the runtime's secret
119
- store, never in the deployment file.
139
+ store, never in the deployment file — or declare
140
+ `auth: { type: "credential" }` and let the operator hold it in the vault
141
+ instead, which is the shape the two-project example above uses.
120
142
 
121
143
  **Connecta does not filter writes for a read-only key.** It has no way to tell
122
144
  which kind a key is without spending a call, so every write in the catalog is
@@ -34,6 +34,61 @@ Credential mutation is intentionally narrower than MCP access:
34
34
  The vault is read for each call. Once an operator saves a replacement,
35
35
  the agent can retry immediately without restarting or redeploying Connecta.
36
36
 
37
+ ## A remote MCP connector's static credential
38
+
39
+ `remoteMcp()` accepts a third auth shape beside OAuth and literal headers:
40
+
41
+ ```ts
42
+ remoteMcp("revenuecat_bepresent", {
43
+ url: "https://mcp.revenuecat.ai/mcp",
44
+ auth: { type: "credential", credential: { label: "API v2 secret key" } },
45
+ });
46
+ ```
47
+
48
+ The connector, its endpoint, and the credential *slot* stay declared in code;
49
+ only the secret arrives through `/credentials`. That is the same boundary
50
+ `api()` has always had, and the reason a project-wide key no longer has to be a
51
+ Worker secret or an environment variable
52
+ ([#439](https://github.com/zackbart/connecta/issues/439)).
53
+
54
+ `header` defaults to `Authorization` and `scheme` to `Bearer`. `scheme: null`
55
+ sends the stored value verbatim, which is what Linear's personal API keys
56
+ expect. A scheme whose last token is `Basic` declares HTTP Basic credentials, so
57
+ the stored `user:secret` is base64-encoded first — `"Basic"` produces
58
+ `Basic <base64>`, and Mixpanel's documented `"Bearer Basic"` produces
59
+ `Bearer Basic <base64>`. There is one reserved `value` field and no multi-field
60
+ header composition: named `credential.fields` are refused at construction.
61
+
62
+ A stored value is checked before anything frames it: a line break or other
63
+ control character — what a key pasted across two lines leaves behind — is
64
+ refused as `auth_required` with a message naming the problem and never the
65
+ value. That check exists because the runtime that rejects such a header quotes
66
+ the whole offending value back in its `TypeError`, and that message would
67
+ otherwise reach the agent, the operator page, and the activity log. Behind it,
68
+ any error whose message quotes the credential or the header it became is
69
+ discarded whole and replaced; nothing is masked or truncated, because a
70
+ redaction that keeps part of a secret is still a leak.
71
+
72
+ An empty slot is not a boot failure and not a silently absent connector. The
73
+ connector is present, its status reads `auth_required`, calls fail with the same
74
+ typed error a missing OAuth grant produces, and `authorize_connector` returns
75
+ the `/credentials` handoff. With no vault configured at all, the failure names
76
+ `credentials.encryptionKey`, and Connecta already warned at startup.
77
+
78
+ The vault is read before any cached downstream client is trusted, so a rotation
79
+ lands on the next call rather than the next deploy. Connecta compares a SHA-256
80
+ digest of the value the cached client connected with; a different digest closes
81
+ that client and reconnects. The plaintext lives in the connect attempt's local
82
+ scope, never on connector state, never in a log, and never in a status or error
83
+ message. A cleartext `http://` destination warns at construction here exactly as
84
+ it does for literal headers — who owns the secret changed, not what the wire
85
+ carries.
86
+
87
+ `/credentials`' Test action connects with the stored value and reports how many
88
+ tools the downstream served. That is the whole honest check for a proxy: which
89
+ account, project, or mode the key reaches is the provider's answer, not
90
+ Connecta's.
91
+
37
92
  ## Downstream OAuth
38
93
 
39
94
  `remoteMcp()` stores dynamic client registration, tokens, PKCE material, state,
@@ -95,12 +95,32 @@ Use a restricted key, not a secret key, and scope it to the operations the
95
95
  agent actually needs; Stripe's own guidance is to "limit your agent's access to
96
96
  exactly the functionality it requires". Keep it in the runtime's secret store.
97
97
 
98
+ The same key can come from `/credentials` instead:
99
+
100
+ ```ts
101
+ stripe("stripe_sandbox", {
102
+ mode: "sandbox",
103
+ purpose: "Automated billing rehearsal",
104
+ auth: { type: "credential" },
105
+ });
106
+ ```
107
+
108
+ `mode` is required either way — a static key answers for exactly one
109
+ environment and cannot report which. The literal-header form is checked against
110
+ the key's `_live_`/`_test_` prefix at construction; an operator-managed key is
111
+ not in the deployment file to read, so the declared mode stands alone and a key
112
+ pointed at the other environment fails at Stripe. Declare the mode carefully:
113
+ that check is the one guard Connecta can offer, and this shape does not get it.
114
+ See
115
+ [storage and credentials](./storage-and-credentials.md#a-remote-mcp-connectors-static-credential).
116
+
98
117
  Organization accounts in one OAuth session are not Stripe Connect connected
99
118
  accounts. Connect platforms can act as a connected account with
100
119
  `connectedAccount`, which adds Stripe's documented `Stripe-Account` header at
101
120
  connector construction. Stripe does not support OAuth for connected-account
102
- calls, so this requires a restricted key through `headers` auth and throws
103
- otherwise:
121
+ calls, and `Stripe-Account` is a second header beside the credential's own,
122
+ which the operator-managed shape does not assemble — so this requires a
123
+ restricted key through `headers` auth and throws otherwise:
104
124
 
105
125
  ```ts
106
126
  stripe("merchant_42", {
@@ -57,7 +57,7 @@ exist so far:
57
57
  | --- | --- | --- |
58
58
  | **pre-template** | before 0.10.2 | no `connecta init` existed; hand-written, or copied from the retired `examples/node` |
59
59
  | **A** | 0.10.2 – 0.15.1 | `.env.example`, `.gitignore`, `AGENTS.md`, `CLAUDE.md`, `README.md`, `package.json`, `src/index.ts`, `tsconfig.json` |
60
- | **B** | 0.16.0 – 0.18.1 | adds `.dockerignore`, `Dockerfile`, `docker-compose.yml`, and `src/file-activity.ts`; `src/index.ts` grows the four commented operator blocks; `.env.example` ships `CONNECTA_TOKEN=` empty |
60
+ | **B** | 0.16.0 – 0.18.3 | adds `.dockerignore`, `Dockerfile`, `docker-compose.yml`, and `src/file-activity.ts`; `src/index.ts` grows the four commented operator blocks; `.env.example` ships `CONNECTA_TOKEN=` empty |
61
61
 
62
62
  Generation A is a decade in template years and identifying it precisely does
63
63
  not matter, because you are about to reconstruct it exactly rather than guess
@@ -106,7 +106,7 @@ know what to preserve, once to know what to re-verify at the end.
106
106
  ### Bump the pin and install
107
107
 
108
108
  ```sh
109
- npm pkg set dependencies.@zackbart/connecta=0.18.1
109
+ npm pkg set dependencies.@zackbart/connecta=0.18.3
110
110
  npm install
111
111
  ```
112
112
 
@@ -130,7 +130,7 @@ Generate the *current* template beside the base you already made, into the same
130
130
  `$SCRATCH`:
131
131
 
132
132
  ```sh
133
- (cd "$SCRATCH" && npx @zackbart/connecta@0.18.1 init current)
133
+ (cd "$SCRATCH" && npx @zackbart/connecta@0.18.3 init current)
134
134
  ```
135
135
 
136
136
  You now have a three-way merge with a real base: `$SCRATCH/base` is what this
@@ -186,7 +186,7 @@ A deployment older than 0.10.2 has no base to diff against. Do not try to
186
186
  manufacture one. Instead:
187
187
 
188
188
  1. `SCRATCH=$(mktemp -d)`, then
189
- `(cd "$SCRATCH" && npx @zackbart/connecta@0.18.1 init current)` — there is no
189
+ `(cd "$SCRATCH" && npx @zackbart/connecta@0.18.3 init current)` — there is no
190
190
  `base` leg here, only the current template to read from.
191
191
  2. Copy `$SCRATCH/current` into the deployment file by file, **skipping
192
192
  `src/index.ts`**.
@@ -207,6 +207,34 @@ first, so cross them bottom-up: start at the oldest one still above this
207
207
  deployment's pin and work back up the page, because each boundary assumes the
208
208
  older ones are already done.
209
209
 
210
+ ### 0.18.2 → 0.18.3
211
+
212
+ Nothing throws for an existing deployment, and the version bump alone crosses
213
+ this boundary. Successful explicitly read-only calls whose provider declares
214
+ no output schema now teach later discovery the result's field names and broad
215
+ JSON types. The open optional-field shape is labeled
216
+ `outputSchemaSource: "observed"`, lives only in a bounded runtime cache, and
217
+ starts cold after 24 hours, a process restart, or Worker isolate eviction. No
218
+ configuration or storage migration is involved, and discovery still never
219
+ executes a tool.
220
+
221
+ ### 0.18.1 → 0.18.2
222
+
223
+ Nothing throws for an existing deployment, and the version bump alone crosses
224
+ it. The release adds a third `auth` shape to `remoteMcp()` and every maintained
225
+ hosted connection — `{ type: "credential" }` — under which the connector
226
+ declares an operator slot on `/credentials` and reads the pasted value on each
227
+ request. A deployment carrying a static key as a runtime secret
228
+ (`auth: { type: "headers", headers: { Authorization: env.KEY } }`) keeps
229
+ working unchanged; moving it behind `/credentials` is an edit to the connector's
230
+ `auth` and one paste on the operator page, and needs `credentials.encryptionKey`
231
+ configured — a deployment without a vault gets a startup warning and
232
+ `recovery: "unavailable"` at use for that connector, not a boot failure. Two
233
+ Linear notes: the `headers` example in `documentation/linear.md` now shows
234
+ `Bearer ${key}` (Linear's MCP server documents that framing), and the credential
235
+ shape sends `Bearer` by default; a `headers` connector already sending a bare
236
+ key is untouched.
237
+
210
238
  ### 0.18.0 → 0.18.1
211
239
 
212
240
  Nothing throws, no option moves, and every deployment crosses this on the
package/ethos.md CHANGED
@@ -111,11 +111,11 @@ proposing one without a new argument is not.
111
111
  | View-initiated read calls from program UI | accepted | named bindings materially improve refresh, cursor pagination, and drill-down without persistence or a new tool; the trusted shell delegates only to the existing fail-closed `call_tool`, and the one-string UI remains display-only ([evidence](./documentation/program-ui-read-calls.md), [#287](https://github.com/zackbart/connecta/issues/287), [#289](https://github.com/zackbart/connecta/issues/289)) |
112
112
  | View-initiated mutation calls from program UI | gated | live-read utility says nothing about write consent: a click is not approval, stale/replayed effects need a host-tested story, and the ordinary destructive path keeps the action in the transcript ([#287](https://github.com/zackbart/connecta/issues/287)) |
113
113
  | Result sampling on the catalog surface (`sample` / `dryRun`) | refused | sampling is execution and cannot ride a catalog read; most tools carry required arguments no sampler can invent, and undeclared `outputSchema` (measured 0/30 and 3/30 on real deployments) is a real gap that is not a sampleable one — a program that checks the shape before rendering already hands back the first record inside the run it was going to make anyway, at zero new surface ([#282](https://github.com/zackbart/connecta/issues/282)) |
114
+ | Passive observed output schemas | accepted | the sampling refusal stands: discovery originates no call and invents no arguments; instead, a successful explicitly read-only call whose provider declared no `outputSchema` records field names and broad JSON types only, under strict depth, breadth, property-name, node, and byte bounds, then merges that open optional-field shape in a 256-entry process-local cache for 24 hours; property names may be user-authored, search and describe label the shape `outputSchemaSource: "observed"`, a materially changed tool definition cannot inherit it, a provider declaration always wins, and no argument, scalar value, raw result, code, credential, or error is retained; this is new evidence rather than a rewrite of #282's facts: Blacksmith measured a value-free warm shape cache cutting one Linear code-mode task from 116.6 s / $1.91 to 56.6 s / $1.06, while live Connecta catalogs measured 246/378 missing on BePresent and 0/90 missing on OneMany ([study](https://www.blacksmith.sh/blog/code-smith-code-mode), [#442](https://github.com/zackbart/connecta/issues/442)) |
114
115
  | Legacy embedded `UIResource` delivery | refused | superseded upstream and rendered by none of the clients connecta faces; per-request minted URIs also fight the caching the Apps spec assumes ([#266](https://github.com/zackbart/connecta/issues/266)) |
115
116
  ## Invariants
116
117
 
117
- One line each; the enforcing tests live beside the subsystem documentation.
118
- Breaking one is not a bug fix — it is a design change wearing a disguise.
118
+ One line each; the enforcing tests live beside the subsystem documentation. Breaking one is not a bug fix — it is a design change wearing a disguise.
119
119
 
120
120
  - **Fail-closed read-only.** A missing, false, or contradictory annotation
121
121
  never gets the benefit of the doubt.
@@ -131,6 +131,7 @@ Breaking one is not a bug fix — it is a design change wearing a disguise.
131
131
  is never cached, persisted, or served as if it were small.
132
132
  - **Activity is payload-free by construction.** The event type has nowhere to
133
133
  put arguments, results, code, or raw error text.
134
+ - **An observed shape is never a declaration.** It contains field names and broad JSON types only, remains open and optional, is labeled on discovery, and disappears behind any provider-declared output schema.
134
135
  - **Credentials never leave the host.** Encrypted at rest, readable only by
135
136
  the owning connector, never rendered by any surface.
136
137
  - **Import-graph purity.** Nothing reachable from the root entry imports a
@@ -145,5 +146,4 @@ Breaking one is not a bug fix — it is a design change wearing a disguise.
145
146
  - **Structural mistakes throw at construction.** A deployment that boots into
146
147
  the wrong shape is worse than one that refuses to boot.
147
148
 
148
- ---
149
149
  Connecta began as a radical simplification of [executor](https://github.com/UsefulSoftwareCo/executor); the table above is the record of that simplification holding.
@@ -123,6 +123,11 @@ function build(env: Env) {
123
123
  auth: {
124
124
  type: "headers",
125
125
  headers: { Authorization: `Bearer ${env.DOWNSTREAM_TOKEN}` },
126
+ // The vault-backed alternative for a downstream that authenticates
127
+ // with a static key: the operator pastes it at /credentials and
128
+ // rotates it there, so no Worker secret holds it.
129
+ // type: "credential",
130
+ // credential: { label: "Notion internal integration token" },
126
131
  },
127
132
  }),
128
133
  api("echo", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zackbart/connecta",
3
- "version": "0.18.1",
3
+ "version": "0.18.3",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "One MCP to rule them all — a single MCP endpoint aggregating many downstream connectors behind a code-first surface of seven meta-tools.",
@@ -15,7 +15,7 @@
15
15
  "typecheck": "tsc --noEmit"
16
16
  },
17
17
  "dependencies": {
18
- "@zackbart/connecta": "0.18.1",
18
+ "@zackbart/connecta": "0.18.3",
19
19
  "quickjs-emscripten": "0.32.0"
20
20
  },
21
21
  "devDependencies": {