@zackbart/connecta 0.32.0 → 0.34.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 (158) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/README.md +95 -116
  3. package/SECURITY.md +1 -1
  4. package/bin/connecta.mjs +24 -6
  5. package/bin/version.mjs +1 -1
  6. package/dist/access-tokens.d.ts +167 -23
  7. package/dist/access-tokens.js +676 -140
  8. package/dist/activity.d.ts +3 -0
  9. package/dist/agent-identity.js +47 -0
  10. package/dist/auth/cloudflare-access.js +13 -5
  11. package/dist/branding.d.ts +7 -0
  12. package/dist/branding.js +76 -9
  13. package/dist/call-admission-context.js +15 -0
  14. package/dist/call-admission.js +30 -43
  15. package/dist/call-recovery.js +6 -2
  16. package/dist/catalog-service.js +15 -1
  17. package/dist/config-value-sources.js +16 -9
  18. package/dist/config.d.ts +48 -75
  19. package/dist/config.js +144 -36
  20. package/dist/connector-access.d.ts +4 -34
  21. package/dist/connector-access.js +15 -99
  22. package/dist/connector-caller.d.ts +7 -1
  23. package/dist/connector-caller.js +4 -2
  24. package/dist/connectors/api-connector.d.ts +27 -4
  25. package/dist/connectors/api-connector.js +2 -0
  26. package/dist/connectors/remote-mcp.js +2 -0
  27. package/dist/describe-config.d.ts +31 -7
  28. package/dist/describe-config.js +8 -17
  29. package/dist/execute.js +1 -11
  30. package/dist/fix-prompt.js +2 -2
  31. package/dist/index.d.ts +4 -3
  32. package/dist/index.js +30 -10
  33. package/dist/invocation.js +5 -3
  34. package/dist/membership.d.ts +53 -0
  35. package/dist/membership.js +121 -0
  36. package/dist/module-contracts.d.ts +23 -6
  37. package/dist/oauth-callback-outcome.js +2 -2
  38. package/dist/operator-record.d.ts +2 -2
  39. package/dist/operator-record.js +2 -4
  40. package/dist/operator-ui/agents.d.ts +74 -0
  41. package/dist/operator-ui/agents.js +104 -0
  42. package/dist/operator-ui/config-summary.js +562 -0
  43. package/dist/operator-ui/connection-map.js +375 -0
  44. package/dist/operator-ui/contract.d.ts +149 -28
  45. package/dist/operator-ui/endpoints.js +221 -0
  46. package/dist/operator-ui/fix-prompts.js +5 -5
  47. package/dist/operator-ui/generated.js +5 -4
  48. package/dist/operator-ui/member-page.js +97 -0
  49. package/dist/operator-ui/model.d.ts +14 -6
  50. package/dist/operator-ui/overview.js +134 -0
  51. package/dist/operator-ui/view.d.ts +107 -37
  52. package/dist/operator-ui/view.js +163 -86
  53. package/dist/page-styles.js +5 -3
  54. package/dist/provider.d.ts +61 -8
  55. package/dist/provider.js +46 -7
  56. package/dist/providers/_shared/google/workspace.d.ts +56 -17
  57. package/dist/providers/_shared/google/workspace.js +34 -8
  58. package/dist/providers/_shared/rest/tools.js +2 -2
  59. package/dist/providers/_shared/rest/value-safety.d.ts +50 -0
  60. package/dist/providers/_shared/rest/value-safety.js +699 -7
  61. package/dist/providers/ccb/index.js +2 -2
  62. package/dist/providers/cloudflare/index.js +3 -3
  63. package/dist/providers/cloudflare/rest.js +25 -3
  64. package/dist/providers/cloudflare/skill.generated.js +1 -1
  65. package/dist/providers/cloudflare/value-safety.js +32 -0
  66. package/dist/providers/docs/index.js +4 -4
  67. package/dist/providers/docs/skill.generated.js +2 -2
  68. package/dist/providers/drive/index.js +4 -4
  69. package/dist/providers/drive/skill.generated.js +2 -2
  70. package/dist/providers/forms/index.js +4 -4
  71. package/dist/providers/forms/skill.generated.js +2 -2
  72. package/dist/providers/gmail/index.js +11 -9
  73. package/dist/providers/gmail/skill.generated.js +2 -2
  74. package/dist/providers/mixpanel/index.js +1 -1
  75. package/dist/providers/mixpanel/skill.generated.js +3 -3
  76. package/dist/providers/notion/index.d.ts +7 -0
  77. package/dist/providers/notion/index.js +100 -11
  78. package/dist/providers/notion/rest.js +45 -5
  79. package/dist/providers/notion/skill.generated.js +2 -2
  80. package/dist/providers/notion/value-safety.js +30 -5
  81. package/dist/providers/overflow/index.js +2 -2
  82. package/dist/providers/overflow/skill.generated.js +2 -2
  83. package/dist/providers/planning-center/index.js +2 -2
  84. package/dist/providers/planning-center/skill.generated.js +4 -4
  85. package/dist/providers/revenuecat/skill.generated.js +2 -2
  86. package/dist/providers/sheets/index.d.ts +3 -0
  87. package/dist/providers/sheets/index.js +7 -4
  88. package/dist/providers/sheets/skill.generated.js +2 -2
  89. package/dist/providers/slides/index.js +4 -4
  90. package/dist/providers/slides/skill.generated.js +2 -2
  91. package/dist/providers/stripe/rest.js +9 -2
  92. package/dist/providers/stripe/skill.generated.js +1 -1
  93. package/dist/providers/stripe/value-safety.js +22 -0
  94. package/dist/providers/tithely/index.js +1 -1
  95. package/dist/providers/vercel/index.js +1 -1
  96. package/dist/providers/vercel/rest.js +20 -2
  97. package/dist/providers/vercel/skill.generated.js +2 -2
  98. package/dist/providers/vercel/value-safety.js +22 -0
  99. package/dist/registry.d.ts +2 -3
  100. package/dist/registry.js +23 -9
  101. package/dist/routes/access-tokens.js +82 -75
  102. package/dist/routes/activity-disclosure.js +17 -18
  103. package/dist/routes/activity.js +34 -54
  104. package/dist/routes/actor-labels.js +33 -4
  105. package/dist/routes/agent-path.js +43 -0
  106. package/dist/routes/connect.js +16 -12
  107. package/dist/routes/credentials.js +16 -40
  108. package/dist/routes/mcp.js +161 -104
  109. package/dist/routes/oauth-management.js +11 -5
  110. package/dist/routes/oauth.js +5 -11
  111. package/dist/routes/operator.js +24 -23
  112. package/dist/routes/shared.d.ts +60 -20
  113. package/dist/routes/shared.js +187 -96
  114. package/dist/routes/ui-config.js +437 -97
  115. package/dist/routes/ui-data.js +2 -2
  116. package/dist/routes/ui.js +9 -16
  117. package/dist/runtime/call-admission.js +9 -0
  118. package/dist/server.js +5 -0
  119. package/dist/skills.js +10 -0
  120. package/dist/storage/keys.js +9 -1
  121. package/dist/storage/sql.js +5 -1
  122. package/dist/types.d.ts +29 -7
  123. package/dist/ui.d.ts +1 -1
  124. package/dist/ui.js +23 -16
  125. package/dist/usage-guide.js +1 -1
  126. package/dist/version.d.ts +1 -1
  127. package/dist/version.js +1 -1
  128. package/dist/workspace-address.js +9 -0
  129. package/dist/workspace-subject.js +23 -0
  130. package/documentation/README.md +47 -23
  131. package/documentation/architecture.md +371 -61
  132. package/documentation/auth.md +897 -249
  133. package/documentation/code-mode.md +37 -16
  134. package/documentation/deploying.md +126 -25
  135. package/documentation/integrating.md +97 -23
  136. package/documentation/meta-tools.md +50 -31
  137. package/documentation/migration-0.33.md +352 -0
  138. package/documentation/migration-0.34.md +67 -0
  139. package/documentation/operating.md +3 -1
  140. package/documentation/operator-tests.md +147 -34
  141. package/documentation/operator-ui.md +519 -64
  142. package/documentation/provider-auth-migration.md +37 -23
  143. package/documentation/provider-migration-0.29.md +2 -2
  144. package/examples/worker/AGENTS.md +37 -9
  145. package/examples/worker/README.md +127 -61
  146. package/examples/worker/src/connecta.config.ts +40 -23
  147. package/examples/worker/wrangler.jsonc +4 -1
  148. package/package.json +19 -18
  149. package/templates/node/.env.example +3 -0
  150. package/templates/node/AGENTS.md +33 -6
  151. package/templates/node/Dockerfile +2 -2
  152. package/templates/node/README.md +80 -52
  153. package/templates/node/docker-compose.yml +3 -2
  154. package/templates/node/package.json +4 -3
  155. package/templates/node/src/adopt-token.ts +23 -0
  156. package/templates/node/src/connecta.config.ts +28 -12
  157. package/templates/node/src/rotate-token.ts +17 -0
  158. package/templates/node/src/provision-token.ts +0 -17
@@ -43,14 +43,14 @@ mode returns native `content`. Default-mode successes declare
43
43
  `_meta["dev.connecta/format"]: "json" | "text"`; value mode places the value in
44
44
  `data` and declares `format` in its complete result envelope.
45
45
 
46
- | Tool | Arguments | Returns |
47
- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
48
- | `execute_code` | `code`, `diagnostics?` | `{ result?, error?, hostCalls }`, plus diagnostics when asked |
49
- | `search_tools` | `query?`, `connector?`, `safety?`, `limit?`, `offset?`, `fullDescriptions?`, `includeSchemas?: "compact" \| "json" \| "typescript"` | `{ catalogErrors, tools, total, offset, limit, hasMore }`, plus `queryAnalysis` on a partial or failed search |
50
- | `call_tool` | `address`, `args?`, `resultMode?: "mcp" \| "value"`, `timeoutMs?`, `diagnostics?` | the downstream result, bounded as [result representation](#result-representation) describes |
51
- | `call_destructive_tool` | the same, plus `reason?` | the same |
52
- | `authorize_connector` | `connector`, `force?` | the class-specific handoff in [authorization recovery](#authorization-recovery) |
53
- | `skills` | `name?` | the listing when `name` is absent, that skill's markdown when it is present |
46
+ | Tool | Arguments | Returns |
47
+ | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
48
+ | `execute_code` | `code`, `diagnostics?` | `{ result?, error?, hostCalls }`, plus `logs`, `emitted`, and `emittedDiscarded` when present and diagnostics when asked |
49
+ | `search_tools` | `query?`, `connector?`, `safety?`, `limit?`, `offset?`, `fullDescriptions?`, `includeSchemas?: "compact" \| "json" \| "typescript"` | `{ catalogErrors, tools, total, offset, limit, hasMore }`, plus `nextOffset` when more remain, `absence` when the query names a service with no configured connector, and `queryAnalysis` on a partial, failed, unsearchable, or absent-service search |
50
+ | `call_tool` | `address`, `args?`, `resultMode?: "mcp" \| "value"`, `timeoutMs?`, `diagnostics?` | the downstream result, bounded as [result representation](#result-representation) describes |
51
+ | `call_destructive_tool` | the same, plus `reason?` | the same |
52
+ | `authorize_connector` | `connector`, `force?` | the class-specific handoff in [authorization recovery](#authorization-recovery) |
53
+ | `skills` | `name?` | the listing when `name` is absent, that skill's markdown when it is present |
54
54
 
55
55
  `limit` defaults to 8 and is capped at 100, as is one `connecta.describe` batch.
56
56
  `connecta.result`'s `offset` is a whole number of bytes ≥ 0 defaulting to 0 and
@@ -66,8 +66,8 @@ refuse a consequential call.
66
66
 
67
67
  `diagnostics: true` adds compact request-local timing and serialized-size
68
68
  aggregates for a caller measuring a workflow: a `diagnostics` block from
69
- `execute_code`, a `timing` block on a call response that is already structured
70
- (value mode, or a failure carrying recovery). Normal calls pay nothing for it,
69
+ `execute_code`, a `timing` block on every structured call response: value-mode successes
70
+ and every `{ ok: false }` failure. Normal calls pay nothing for it,
71
71
  and the measurements never contain program source, arguments, values, addresses,
72
72
  credentials, logs, or raw error text.
73
73
 
@@ -194,8 +194,7 @@ discovery made the same bet).
194
194
  ```
195
195
 
196
196
  It is something to read, not something that runs: programs stay JavaScript,
197
- and `execute_code` accepts no erasable TypeScript
198
- ([decision history](https://github.com/zackbart/connecta/blob/main/decisions/0001-ethos-verdict-table.md)). The output half is the provider's declared
197
+ and `execute_code` accepts no erasable TypeScript. The output half is the provider's declared
199
198
  schema, else an `S9` observation that opens with the
200
199
  `/* observed, not declared */` marker and keeps the `outputSchemaSource`
201
200
  field, else `unknown` — runtime evidence never reads as a contract, even when
@@ -222,7 +221,7 @@ declared schema exactly as they do for compact.
222
221
  A connector may attach a deployment-owned guide as markdown, keeping the
223
222
  original `usageGuide: string` configuration, or as
224
223
  `{ content, summary?, required? }` — which registers no connector and creates no
225
- shared runtime template. The registry prepends Agent Skills frontmatter to
224
+ shared runtime template. The skills catalog prepends Agent Skills frontmatter to
226
225
  `content` and preserves the guide body, including its whitespace.
227
226
  `summary` is normalized and refuses construction over 120 characters; absent,
228
227
  Connecta derives the same bounded fallback the skills listing uses from the
@@ -254,14 +253,22 @@ A connector-scoped lexical miss keeps that connector's guide metadata under
254
253
  contains no endpoint vocabulary: a required guide stays discoverable instead of
255
254
  disappearing with the zero-tool page.
256
255
 
257
- Two built-in skills are byte-identical across deployments. `usage` says to read
258
- it at most once per task and owns program selection detail, examples, runtime
256
+ Two built-in skills are byte-identical across deployments. `usage` is fetched
257
+ only for missing guidance or program repair, and owns program selection detail, examples, runtime
259
258
  differences, and repair guidance; `investigate` is on-demand guidance for
260
259
  purchase verification, experiment checks, and customer or deployment
261
260
  investigations — shared guidance, not a saved workflow and not a source of
262
261
  deployment-specific ids. The always-loaded instructions and tool definitions keep
263
262
  route selection, the fail-closed boundary, and the minimum guest syntax, so a
264
- client that never fetches a skill can still write a valid first program. Connector
263
+ client that never fetches a skill can still write a valid first program.
264
+ The instructions are the same on every endpoint except one addition: at
265
+ `/mcp/<pool>` and `/agent/<pool>`, a pool that sets a
266
+ [`description`](./auth.md#pools) has it appended after `This endpoint: `, so
267
+ the model learns that endpoint's scope, for example
268
+ `This endpoint: Team tier: shared tools only. For someone's own mail or files, ask them to message you one-on-one.`
269
+ Plain `/mcp` never carries one. The description changes no tool or tool
270
+ description, and `initialize` and `server/discover` stay `cacheScope: "private"`,
271
+ so a client keeps what it learned per endpoint. Connector
265
272
  guides stay scoped to the deployment that listed them even when two deployments
266
273
  use identical content, and a deployment with no connector guides receives none of
267
274
  the short conditional guide pointers in its tool definitions.
@@ -282,7 +289,7 @@ is its slug; other IDs use `connector-` followed by a stable SHA-256 prefix.
282
289
  The suffix makes each
283
290
  document a valid Agent Skills file; the roots `skill://connecta/usage` and
284
291
  `skill://connecta/connectors/<connector-id>` also resolve. Provider guides are
285
- built from the generated `src/providers/*/SKILL.md` content with the existing
292
+ built from maintained provider guides with the existing
286
293
  connection context and deployment instructions. Local manifests contain a
287
294
  SHA-256 digest and UTF-8 byte size of the complete document. `usage` and
288
295
  `investigate` remain compatibility names; `connector:<id>` remains a
@@ -387,7 +394,11 @@ including dependencies inherited from pages read during the run. Downstream skil
387
394
  resource content requires a whole-connector grant on every page. Local guides and catalog
388
395
  metadata require the contributing connector or tool to remain visible, including
389
396
  caught catalog-load errors, unknown-tool failures, and recovery messages that
390
- list visible connectors or local guides. Returns with no downstream calls still
397
+ list visible connectors or local guides. No-match search guidance also retains
398
+ the connectors it names or summarizes by count, including matches to connector
399
+ titles; scoped guides and summaries, absence rosters, and catalog errors retain
400
+ their contributors. Search counts, ranking, and term analysis also retain every
401
+ searched connector, including matches outside the returned page. Returns with no downstream calls still
391
402
  require matching identity, origin and endpoint, current auth, and pool membership. A page must
392
403
  match those bindings and pass current auth, connector/tool grants, pool membership,
393
404
  and read-only checks. Old entries lacking bindings fail closed. A random UUID is a
@@ -395,7 +406,7 @@ handle, never an access grant.
395
406
 
396
407
  New entries store UTF-8 bytes in a base64 envelope split across storage keys,
397
408
  48 KiB of result text per chunk — widening past roughly 1.5 MB so no result
398
- occupies more than 33 keys, because every chunk is also a write. `connecta.result`
409
+ occupies more than 32 keys, because every chunk is also a write. `connecta.result`
399
410
  reads and decodes only the chunks a page covers plus a few boundary bytes, so
400
411
  paging a 1.2 MB result costs the same per page as paging a 300 KB one; it
401
412
  neither encodes nor reads the whole result per page. Entries without the required identity and invocation bindings fail closed,
@@ -427,7 +438,7 @@ JSON; everything after the first newline is the preview. `structuredContent`
427
438
  is absent, so the notice cannot hide the preview in clients that prefer it:
428
439
 
429
440
  ```text
430
- {"truncated":true,"resultId":"…","totalBytes":161420,"hint":"This write already ran: do not call it again to see its result. Bytes 0-24000 of 161420 follow; page the rest with connecta.result using nextAction.","nextOffset":24000,"nextAction":{"tool":"execute_code","arguments":{"code":"async () => await connecta.result(\"…\", { offset: 24000 })"}}}
441
+ {"truncated":true,"resultId":"…","totalBytes":161420,"hint":"This write already ran: do not call it again to see its result. Bytes 0-24000 of 161420 follow; page the rest with connecta.result using nextAction.","nextOffset":24000,"nextAction":{"tool":"execute_code","arguments":{"code":"async () => await connecta.result(\"…\", { offset: 24000, maxBytes: 3829 })"}}}
431
442
  {"ts":"2026-09-16T21:31:11.759Z","actor":"sam.ortiz@example.com",…
432
443
  ```
433
444
 
@@ -459,7 +470,7 @@ characters. One that rejects outright — Claude Code with
459
470
  `MAX_MCP_OUTPUT_TOKENS` set below 12,500 — needs a cap under twice its limit.
460
471
 
461
472
  The preview is the head of what `connecta.result` pages, cut on a character
462
- boundary, so `nextAction.arguments.offset` continues exactly where it stops. A
473
+ boundary, so the `offset` in `nextAction.arguments.code` continues exactly where it stops. A
463
474
  lone text block — including the one synthesized from `structuredContent` — is
464
475
  measured, stashed, previewed, and paged as its text: wrapping it in a content
465
476
  envelope added nothing but a second layer of JSON escaping, so a JSON payload
@@ -598,8 +609,8 @@ same when no visible connector ID or title names that service. This service-name
598
609
  vocabulary is query interpretation, not an inventory of configured providers.
599
610
  For other service names, use the explicit `connector` scope. An unavailable
600
611
  catalog is a catalog failure, never an absence. Absence records and catalog
601
- errors use only the endpoint/pool and grant intersection; a connector hidden by
602
- that intersection is indistinguishable from an unconfigured one.
612
+ errors use only the endpoint's tools; a connector the endpoint does not serve
613
+ is indistinguishable from an unconfigured one.
603
614
 
604
615
  If no tool covers every search term, wider any-term fallback remains available
605
616
  for ordinary action/object searches and the page carries `matchMode: "partial"`.
@@ -683,8 +694,8 @@ replay. Downstream payloads and input responses are capped at 64 KiB, with at
683
694
  most 16 inputs and 256 characters per downstream key. The combined private
684
695
  state history is also capped at 64 KiB. Sealed wire state is
685
696
  capped at 128 KiB. `input_required_invalid`, `input_required_limit`,
686
- `input_required_unsupported`, and `input_required_round_limit` are non-retryable
687
- typed failures.
697
+ and `input_required_unsupported` are non-retryable typed failures;
698
+ `input_required_round_limit` is an untyped failure with `retryable: false`.
688
699
 
689
700
  Programs and in-process meta-tools return `input_required_unsupported`, with a
690
701
  `nextAction` naming the equivalent direct call and bounded original arguments.
@@ -933,9 +944,13 @@ list connectors from another pool or identity's grants.
933
944
  A write that times out after dispatch returns `write_outcome_unknown` with
934
945
  `retryable: false` and an agent-only `uncertainCall: { address, args?, argsOmitted?, argsRedacted?, recovery? }`.
935
946
  No write is automatically replayed. `recovery` carries facts the connector
936
- recorded before dispatch (`src/call-recovery.ts`), such as the `idempotencyKey`
937
- a REST connector generated; a deliberate retry that reuses it with the exact
938
- original arguments cannot repeat the write while the vendor retains the key
947
+ recorded before dispatch, such as a REST connector's
948
+ generated or caller-supplied `idempotencyKey`. Recovery accepts keys up to 255
949
+ characters, matching the REST write schema. Other recovery values remain capped
950
+ at 128 characters, with at most four facts per call and camelCase names of at
951
+ most 32 characters. Values outside these bounds are dropped whole. A deliberate
952
+ retry that reuses the key with the exact original arguments cannot repeat the
953
+ write while the vendor retains the key
939
954
  (Stripe may prune v1 keys after 24 hours). After that, look the object up before
940
955
  retrying. When a connector recorded such facts, any other dispatched write whose
941
956
  outcome is unknown (a lost response body, a 5xx) also carries `uncertainCall`
@@ -954,18 +969,22 @@ SDK timeout classes, and runtime network codes. Untyped prose is non-retryable:
954
969
  name, or URL never changes the verdict. JSON-RPC `ProtocolError.data` is dropped
955
970
  at the downstream boundary, even when the agent may see the error's message.
956
971
 
957
- ## Source and tests
972
+ ## Contributor reference
973
+
974
+ ### Source and tests
958
975
 
959
976
  Handlers and schemas live in [meta-tools.ts](https://github.com/zackbart/connecta/blob/main/src/meta-tools.ts).
960
977
  [Catalog service](https://github.com/zackbart/connecta/blob/main/src/catalog-service.ts)
961
978
  and [invocation](https://github.com/zackbart/connecta/blob/main/src/invocation.ts)
962
979
  serve both direct tools and programs. [Skills](https://github.com/zackbart/connecta/blob/main/src/skills.ts)
963
- owns the supplied usage instructions and connector guides.
980
+ owns the supplied usage instructions, each endpoint's instructions
981
+ (`endpointInstructions`), and connector guides.
964
982
 
965
983
  [Schema tests](https://github.com/zackbart/connecta/blob/main/test/meta-tool-schemas.test.ts),
966
984
  [search tests](https://github.com/zackbart/connecta/blob/main/test/meta-tools-search.test.ts),
967
985
  [call tests](https://github.com/zackbart/connecta/blob/main/test/meta-tools-call.test.ts),
968
986
  [skills tests](https://github.com/zackbart/connecta/blob/main/test/skills-extension.test.ts),
969
- and [surface tests](https://github.com/zackbart/connecta/blob/main/test/code-first-surface.test.ts)
987
+ [surface tests](https://github.com/zackbart/connecta/blob/main/test/code-first-surface.test.ts),
988
+ and [pool description tests](https://github.com/zackbart/connecta/blob/main/test/pool-description.test.ts)
970
989
  check the published contract. Guest parity and clause evidence are in
971
990
  [code-mode verification](./code-mode.md#verification).
@@ -0,0 +1,352 @@
1
+ # Migration to 0.33
2
+
3
+ Use this upgrade guide for an existing deployment moving to 0.33.
4
+ [Deployment upgrades](./deploying.md#upgrade-an-existing-deployment) cover the
5
+ general procedure; [the agent index](./README.md) routes other work.
6
+
7
+ 0.33 makes pools the access model.
8
+ Every deployment with inbound auth changes its configuration. Each removed
9
+ option fails construction with a message naming its replacement, so a
10
+ deployment that starts has no leftover option silently ignored. Work through
11
+ the sections in order, then [verify](#verify).
12
+
13
+ ## Old option to new option
14
+
15
+ | 0.32 | 0.33 |
16
+ | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
17
+ | `identity.connectorAccess` | A pool per audience, with `tools` and `members`. A guarded `{ tool, requireReadOnly: true }` grant becomes a pool with `readOnly: true` |
18
+ | Top-level `grant` | `maintainers` |
19
+ | Pool `grant` | Pool `members` |
20
+ | `describeConfig().hasGrant` (root and pools) | `describeConfig().maintainers` and each pool's `members` |
21
+ | `identity.credentialAdministration` | `maintainers` (interactive) |
22
+ | `identity.personalConnection` | Pool `members` for personal connectors in their pools; maintainers for their own |
23
+ | `identity.accessTokenManagement` | `maintainers` (interactive) |
24
+ | `identity.activityAccess` | `maintainers`, still narrowed by `activity.readGate` |
25
+ | The `identity` section, even empty | Removed; delete it |
26
+ | `ConnectaIdentityConfig`, `ConnectorPermission` types | Removed |
27
+ | Tokens created or named in the UI, human-bound tokens | `agents: ["<name>"]`, one token slot each |
28
+ | `accessTokens(storage, { maxActive })` | `accessTokens(storage)` |
29
+ | `describeConfig().modules.accessTokens.maxActive` | `describeConfig().agents` |
30
+ | Node template `provision-token` | `npm run --silent rotate-token -- <agent>`; existing tokens: `npm run adopt-token` |
31
+ | A machine at `/mcp` or `/mcp/<pool>` with `cta_` | `/agent/<pool>`, with `"agent:<name>"` in the pool's `members` |
32
+ | Access service token plus `cta_` behind Cloudflare Access | `cta_` alone, through an `/agent/*` Access Bypass application |
33
+ | Grant callbacks matching token record ids | Membership by agent name; `identity.agent` in custom code |
34
+ | A Workspace `subject` mapping that answered for machine tokens | The pool's `owner` |
35
+ | One Workspace connector per person (`gmail_alice`) | One connector with a mapping `subject`, plus pool owners |
36
+ | `POST`/`DELETE /ui/access-tokens/<agent>` without a body | `{ "expect": "<revision>" }` from `GET /ui/access-tokens` |
37
+ | Operator contract `schemaVersion: 1`, `you.grants`, `you.permissions`, and `hasGrant` | `schemaVersion: 2` with `role`, `root`, `pools`, `agents`, `matrix`, and `connectors`; `OperatorUiContract` names v2 |
38
+ | `OperatorViewer`, `OperatorPool` types | Removed; `OperatorPoolV2`, `OperatorRootEndpoint`, `OperatorMemberConnector` and the other v2 types |
39
+ | `/ui/data`, `/ui/connectors/<id>` | Maintainers only; members read their view from `/ui/api/config` |
40
+ | A connector that connects with `startAuth` alone, or disconnects only alongside it | Connect needs `startAuth`, `finishAuth`, and `verifyState`; disconnect needs only `disconnectAuth` |
41
+ | `DELETE /ui/credentials/<id>` on an empty slot answering 204 | 409 `{ "problem": "credential_required" }` |
42
+ | The `agent_not_maintainer` denial reason | Removed; agents are refused at human endpoints before admission |
43
+
44
+ ## Maintainers and pool members
45
+
46
+ 0.32 combined an identity resolver, a top-level `grant`, and pool `grant`
47
+ callbacks. 0.33 reads access from two static lists in code:
48
+
49
+ - `maintainers` names the people who reach plain `/mcp`, which serves the
50
+ whole catalog, and every operator route. It is required whenever an inbound
51
+ provider that admits people is configured.
52
+ - Each pool's `members` names who reaches it: people at `/mcp/<pool>`, agents
53
+ as `"agent:<name>"` at `/agent/<pool>`. A pool serves exactly its `tools`,
54
+ intersected with nothing. A maintainer opens no pool implicitly.
55
+
56
+ ```ts
57
+ // 0.32
58
+ createConnecta({
59
+ auth: clerkAuth({ publishableKey, secretKey }),
60
+ grant: ({ principal }) => principal?.id === "user_alice",
61
+ identity: { connectorAccess, credentialAdministration, activityAccess },
62
+ pools: {
63
+ support: { tools: ["tickets", "crm.search"], grant: ({ principal }) => support.has(principal?.id ?? "") },
64
+ },
65
+ // …
66
+ });
67
+
68
+ // 0.33
69
+ createConnecta({
70
+ auth: clerkAuth({ publishableKey, secretKey }),
71
+ maintainers: ["user_alice"],
72
+ pools: {
73
+ support: { tools: ["tickets", "crm.search"], members: [...support] },
74
+ },
75
+ // …
76
+ });
77
+ ```
78
+
79
+ A reference is a bare principal id when the deployment has exactly one
80
+ interactive provider, `{ namespace, id }` otherwise, or `"agent:<name>"`.
81
+ Rewrite a predicate as the list it computed; ordinary code at construction
82
+ (`team.map(...)`) can build it. [Principals and operators](./auth.md#principals-visibility-and-operators)
83
+ owns the rules. A non-maintainer at `/mcp`, a non-member, and an undeclared
84
+ pool get the same 404.
85
+
86
+ ### Preserve effective access
87
+
88
+ Before deleting `identity.connectorAccess`, compute what each audience could
89
+ actually use: its endpoint's tools intersected with that identity's old
90
+ connector permissions, including read-only guards. Write that effective set
91
+ into the new pool. Copying an old pool's broader `tools` verbatim would widen
92
+ access because 0.33 has no second intersection. A person previously admitted
93
+ to plain `/mcp` with restricted connector permissions needs a pool and a new
94
+ `/mcp/<pool>` client URL, not a maintainer entry. Maintainers have the whole
95
+ root catalog and administrative authority.
96
+
97
+ For example, suppose Alice administered the deployment, while Bob's old team
98
+ pool listed `crm` and `tickets` but his `connectorAccess` allowed only
99
+ `{ tool: "tickets.search", requireReadOnly: true }`. The new configuration is:
100
+
101
+ ```ts
102
+ maintainers: ["user_alice"],
103
+ pools: {
104
+ team: { tools: ["tickets.search"], members: ["user_bob"], readOnly: true },
105
+ },
106
+ // Delete identity and all grant callbacks; Bob connects at /mcp/team.
107
+ ```
108
+
109
+ A read-only guard applies to the whole new endpoint. If an audience previously
110
+ had both writable tools and guarded reads, separate those into explicit pools
111
+ with the appropriate `readOnly` choice rather than dropping the guard. Then
112
+ update clients to the intended endpoints.
113
+
114
+ ## Management
115
+
116
+ The four management resolvers are gone, and the `identity` section with them.
117
+ [Management permissions](./auth.md#management-permissions) has the table:
118
+ interactive maintainers administer shared credentials and OAuth grants and
119
+ issue, rotate, and revoke agent tokens; maintainers read activity, within an
120
+ optional `readGate`; a pool member connects and disconnects their own account
121
+ on a personal connector in one of their pools. Agents manage nothing.
122
+
123
+ ## Agents and their tokens
124
+
125
+ Machine clients are agents declared by name. Each has one token slot that a
126
+ maintainer issues, rotates, and revokes; nothing outside code adds, renames,
127
+ or rebinds one, and removing a name stops its token on the next request.
128
+
129
+ 1. Add `agents: ["<name>", …]` and keep `accessTokens(storage)` on the same
130
+ storage and key namespace. Drop `maxActive`. A Node template deployment
131
+ from before 0.33 also copies `src/rotate-token.ts` and `src/adopt-token.ts`
132
+ from `node_modules/@zackbart/connecta/templates/node/src/`, deletes
133
+ `src/provision-token.ts`, and replaces the `provision-token` and `doctor`
134
+ scripts with the installed template's `rotate-token`, `adopt-token`, and
135
+ `doctor` (`connecta doctor --pool <pool>`) scripts.
136
+ 2. Keep each machine's existing secret by adopting its token once:
137
+ `npm run adopt-token` lists the tokens that can be adopted, and
138
+ `npm run adopt-token -- <token-id> <agent>` binds one (Node template), or
139
+ call `AccessTokenManager.adopt(tokenId, agent)` from trusted provisioning.
140
+ Until then the token refuses.
141
+ 3. A human-bound token, issued from the UI before 0.33, cannot be adopted and
142
+ stays refused. Issue that agent a new token with
143
+ `npm run --silent rotate-token -- <agent>` or from the Tokens page.
144
+ 4. Clients that call the token routes send the slot's `revision`:
145
+ `POST /ui/access-tokens/<agent>` (issue, rotate) and `DELETE` (revoke) take
146
+ `{ "expect": "<revision>" }` from `GET /ui/access-tokens`, and a slot that
147
+ changed answers 409 `slot_changed`. A missing `expect` is 400.
148
+ `AccessTokenManager` takes the same optional `expect`.
149
+
150
+ ### Programmatic token management
151
+
152
+ Trusted provisioning code that constructs its own `AccessTokenManager` must
153
+ pass the declared agents. The deployment's top-level `agents` does not bind a
154
+ separately constructed manager, so `new AccessTokenManager(storage)` knows no
155
+ agent and every `rotate` and `adopt` fails with `unknown_agent`.
156
+
157
+ | 0.32 | 0.33 |
158
+ | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- |
159
+ | `new AccessTokenManager(storage, { maxActive })` | `new AccessTokenManager(storage, { agents })`, with the same names as the deployment's `agents` |
160
+ | `create(name, createdBy)` | `rotate(agent, issuedBy?, options?)`, returning `{ token, agent }`; the first rotation issues the slot's token |
161
+ | `rename(id, name)` | Removed; agents are named only in code |
162
+ | `revoke(tokenId, revokedBy)` | `revoke(agent, options?)`, returning the slot |
163
+ | `list()` of token records | `list()` of one slot per declared agent: `{ name, state, issuedAt?, revokedAt?, revision }` |
164
+ | (none) | `adopt(tokenId, agent, options?)` and `unboundTokens()` for tokens issued before agents |
165
+
166
+ `options` is `{ expect?, signal?, deadline? }`. `expect` is the slot
167
+ `revision` read from `list()`; when given, a slot that changed since refuses
168
+ with an `AccessTokenError` whose `code` is `slot_changed`, and nothing is
169
+ minted or written. Without it the calls behave as before.
170
+
171
+ ```ts
172
+ import { AccessTokenManager } from "@zackbart/connecta/auth/access-tokens";
173
+
174
+ const manager = new AccessTokenManager(storage, { agents: ["assistant"] });
175
+ const slot = (await manager.list()).find((agent) => agent.name === "assistant")!;
176
+ const { token } = await manager.rotate("assistant", "provisioning", { expect: slot.revision });
177
+ ```
178
+
179
+ Rotation is an immediate cutover: the old secret refuses on its next request.
180
+ An agent has no principal, so personal connectors are absent for it.
181
+ [Agent tokens](./auth.md#agent-tokens-and-upgrading-from-earlier-tokens) owns
182
+ the slot, revision, and adoption contracts.
183
+
184
+ ## Agent endpoints
185
+
186
+ Agents move from `/mcp/<pool>` to `/agent/<pool>`, and every human route
187
+ (`/mcp`, `/mcp/<pool>`, the operator pages and APIs, `/connect`, OAuth starts
188
+ and callbacks) refuses a `cta_` token before reading token storage. Agents are
189
+ never maintainers, so a machine that used plain `/mcp` moves to a pool that
190
+ holds its tools.
191
+
192
+ 1. List `"agent:<name>"` in the `members` of each pool the agent uses.
193
+ 2. Point its client at `https://<host>/agent/<pool>` with
194
+ `Authorization: Bearer cta_…` as its only credential.
195
+ 3. Behind Cloudflare Access, add one self-hosted Access application per agent
196
+ hostname for `<host>/agent/*` with a single Bypass policy, beside the
197
+ Worker-level application, then drop the agent's `CF-Access-Client-Id` and
198
+ `CF-Access-Client-Secret` headers. The
199
+ [deploying guide](./deploying.md#agent-endpoints-behind-cloudflare-access)
200
+ has the steps and the checks to repeat until the change settles. Never
201
+ bypass any other path.
202
+ 4. Run `connecta doctor --pool <pool>` with the agent's token; doctor checks
203
+ `/agent/<pool>` for a `cta_` token and requires `--pool`.
204
+
205
+ [Agent endpoints](./auth.md#agent-endpoints) owns the path, auth, and
206
+ membership contract.
207
+
208
+ ## Google Workspace pool owners
209
+
210
+ In 0.32 a `cta_` token reached a Workspace connector's `subject` mapping,
211
+ which usually answered nothing for a caller without a principal. In 0.33 an
212
+ agent never reaches the mapping. It acts as the `owner` of the pool it came
213
+ through; in a pool without one the connector is absent for it, as a personal
214
+ connector is. People always act
215
+ as themselves through the mapping, in any pool. A fixed `subject` address
216
+ still applies to every caller.
217
+
218
+ If your mapping returned an address for a machine token, set that address as
219
+ the `owner` of the agent's pool. Replace per-person connectors such as
220
+ `gmail_alice` and `gmail_bob` with one `gmail()` whose `subject` is a mapping
221
+ function, and give each person's pool its `owner`.
222
+ [Pool owners](./auth.md#pool-owners-for-google-workspace) owns the rules.
223
+
224
+ ## Operator contract v2
225
+
226
+ `GET /ui/api/config` serves `OperatorUiContractV2` (`schemaVersion: 2`) and
227
+ is the operator page's only gate; the v1 contract is removed:
228
+
229
+ - A signed-out caller gets 401 with the auth challenge.
230
+ - A `cta_` agent token gets the machine-only 401 Bearer challenge, as on every
231
+ human route, before token storage is read.
232
+ - A signed-in person who is neither a maintainer nor in any pool's `members`
233
+ gets the private JSON 404.
234
+ - A maintainer, or a person in a pool's `members`, gets 200 with their `role`.
235
+
236
+ The maintainer view carries `config`, `configSources`, `root`, every pool
237
+ with its owner and labelled members, every declared agent with its token slot
238
+ and `lastSeenAt`, and the connector × endpoint `matrix`. The member view
239
+ carries only that member's pools, without owner or members, and the
240
+ connectors in them, with their own connection and a `manage` naming the
241
+ actions they may take. `/ui/data` and `/ui/connectors/<id>` are
242
+ maintainer-only and give anyone else the same 404. Code that read v1's
243
+ `hasGrant`, `you.grants`, or `you.permissions` reads v2's `role`, `root`,
244
+ `pools`, `agents`, `matrix`, and `connectors` instead;
245
+ [the operator contract](./operator-ui.md#operator-data-contract) documents
246
+ every field. Read the published declarations in
247
+ `dist/operator-ui/contract.d.ts` inside the installed package for the exact
248
+ typed shape; `you.interactive` remains in v2. `OperatorUiContract` now names v2, and `OperatorViewer` and
249
+ `OperatorPool` are removed. `connecta doctor --config` reads v2 and prints a
250
+ maintainer's `config`; a member's view has none.
251
+
252
+ The connector auth routes accept what `manage` lists, with one recovery
253
+ exception: a stored credential that cannot be read can still be cleared,
254
+ although `manage` lists only `save` in that state.
255
+ `/ui/oauth/<id>` gates each verb on its own hooks: connecting needs
256
+ `startAuth`, `finishAuth`, and `verifyState`, and disconnecting needs only
257
+ `disconnectAuth`. A custom connector that offered connect with `startAuth`
258
+ alone adds the other two, without which its callback never completed. A
259
+ connector provisioned out of band, with `disconnectAuth` and no `startAuth`,
260
+ can now be disconnected. `DELETE /ui/credentials/<id>` refuses to clear a slot
261
+ that holds nothing, the caller's own partition for a personal connector, with
262
+ 409 `{ "error": "no credential is stored to clear", "problem": "credential_required" }`.
263
+ The bundled operator UI needs no change.
264
+
265
+ ## Security fix: pre-auth listen buffering
266
+
267
+ Versions 0.29.0 through 0.32.0 buffered the whole body of an unauthenticated
268
+ `POST` carrying `Mcp-Method: subscriptions/listen` to `/mcp` or `/mcp/<pool>`,
269
+ with no byte cap, before authentication and request admission. An anonymous
270
+ client could hold unbounded memory per request without taking an admission
271
+ permit. No credential or data was exposed; the risk is memory exhaustion.
272
+
273
+ 0.33 authenticates first, then checks endpoint admission, before reading any
274
+ body on `/mcp`, `/mcp/<pool>`, and `/agent/<pool>`. A pre-dispatch read takes
275
+ at most 64 KiB; past that, connecta answers 413 with JSON-RPC code `-33006`
276
+ and stops reading. The new `/agent/*` Access bypass therefore does not reopen
277
+ the issue.
278
+
279
+ What operators should know:
280
+
281
+ - The fix ships in 0.33.0 only; there is no patch release for 0.29–0.32.
282
+ Upgrade.
283
+ - No configuration change is needed for the fix itself.
284
+ - A Worker behind a Worker-level Access application received only requests
285
+ Access admitted. A deployment reachable without edge authentication, such
286
+ as Node behind a public URL, was exposed. If you cannot upgrade at once,
287
+ cap request body size at your proxy or refuse unauthenticated requests that
288
+ carry `Mcp-Method: subscriptions/listen` there.
289
+
290
+ ## Moving to the agent pool layout
291
+
292
+ A 0.32 agent usually reached `/mcp/<pool>` through a pool `grant` matching
293
+ its token, often with one connector per person and a second connection or
294
+ pool for each person's accounts. Move it to
295
+ [connecta's agent pool layout](./auth.md#organizing-an-agents-pools): one
296
+ connection; a base pool named after the agent with shared connectors and no
297
+ owner; one `<agent>-<user>` pool per person with their own connectors and
298
+ their Workspace address as `owner`.
299
+
300
+ ```ts
301
+ // 0.32
302
+ pools: {
303
+ assistant: {
304
+ tools: ["crm", "tickets", "notes_alice", "gmail_alice", "notes_bob", "gmail_bob"],
305
+ grant: ({ actor }) => actor.kind === "access_token" && actor.id === assistantTokenId,
306
+ },
307
+ },
308
+
309
+ // 0.33
310
+ agents: ["assistant"],
311
+ pools: {
312
+ assistant: { tools: base, members },
313
+ "assistant-alice": { tools: [...base, "notes_alice", "gmail"], owner: "alice@example.com", members },
314
+ "assistant-bob": { tools: [...base, "notes_bob", "gmail"], owner: "bob@example.com", members },
315
+ },
316
+ // with const base = ["crm", "tickets"], members = ["agent:assistant"];
317
+ ```
318
+
319
+ 1. Declare the agent in `agents` and adopt its token (see
320
+ [Agents and their tokens](#agents-and-their-tokens)).
321
+ 2. Define the base pool's tool list once with the shared connectors, and
322
+ spread it into the base pool `<agent>` and one `<agent>-<user>` pool per
323
+ person, adding that person's connectors and `owner`. Each pool lists
324
+ `members: ["agent:<agent>"]`. If group chats should get different tools
325
+ depending on who is present, add tiers as the layout describes.
326
+ 3. Collapse per-person Workspace connectors into one connector with a mapping
327
+ `subject`. Keep other per-person accounts as their own shared-auth
328
+ connector ids (`notes_alice`) and list each only in its person's pool.
329
+ 4. Change the agent's client to one connection whose URL it sets per turn:
330
+ `/agent/<agent>-<user>` in a one-on-one with that user, otherwise the
331
+ layout's tier for who is present (`/agent/<agent>` with a single tier). Remove any second Connecta connection.
332
+ 5. Add this line to your deployment's own AGENTS.md (or its CLAUDE.md
333
+ equivalent), so agents find all operator procedures after the upgrade:
334
+
335
+ ```text
336
+ For every Connecta operator task, start at node_modules/@zackbart/connecta/documentation/README.md#operator-tasks
337
+ ```
338
+
339
+ ## Verify
340
+
341
+ 1. Start the deployment; construction errors name each removed option's
342
+ replacement.
343
+ 2. Sign in as a maintainer and confirm plain `/mcp` and the operator UI work.
344
+ 3. Run `connecta doctor --pool <pool>` with each agent's token, and confirm
345
+ the same token at `/mcp` is refused.
346
+ 4. Confirm a pool member sees exactly that pool's tools at `/mcp/<pool>`.
347
+ 5. For Workspace, call a read in an agent's personal pool and confirm it acts
348
+ as the pool's owner. In the base pool, confirm Workspace connectors are
349
+ absent: search finds none of their tools and a call fails
350
+ `unknown_address`. The same holds in any pool without an owner, even one
351
+ that lists a Workspace connector with a subject mapping.
352
+ 6. Confirm each `<agent>-<user>` pool lists no other person in `members`.
@@ -0,0 +1,67 @@
1
+ # Migration to 0.34
2
+
3
+ Use this guide when upgrading an existing deployment from 0.33 to 0.34.
4
+ [Deployment upgrades](./deploying.md#upgrade-an-existing-deployment) cover the
5
+ general procedure; [the agent index](./README.md) routes other work. Upgrades
6
+ from earlier versions must also apply the intervening migrations.
7
+
8
+ No storage migration, token rotation or required configuration field is added.
9
+ Review the following changes before updating the package pin.
10
+
11
+ ## Operator links and browser titles
12
+
13
+ Maintainers now land on Overview at `/`. Endpoints is at `/endpoints`, and
14
+ `/access` redirects to `/endpoints`. Update saved links and browser automation
15
+ that expected Endpoints at `/`, including row links: `/#mcp/support` becomes
16
+ `/endpoints#mcp/support`. Members still get their member page on every operator
17
+ page path. MCP client URLs (`/mcp`, `/mcp/<pool>` and `/agent/<pool>`) do not change.
18
+
19
+ The default `branding.pageTitle` joins `productName` and `ownerName` with a
20
+ middle dot, for example `Assistant · Example team`. A title set explicitly in
21
+ `branding.pageTitle` is unchanged. If automation requires the previous title,
22
+ set that exact title explicitly; otherwise update its expectation. With no
23
+ owner name, the default remains the product name alone.
24
+
25
+ Activity links can use `failed` or `succeeded` for the outcome filter. Older
26
+ exact outcome values in saved links still work. See the
27
+ [operator UI guide](./operator-ui.md) for the new pages and controls.
28
+
29
+ ## Workspace connectors in agent pools
30
+
31
+ An agent in a pool without an `owner` no longer sees a Google Workspace
32
+ connector whose `subject` is a mapping function. Previously the connector
33
+ appeared but its calls failed `auth_required`; now discovery, search, describe,
34
+ skills and code mode omit it, and a call by address fails `unknown_address`.
35
+ Paging a saved program return also rechecks visibility, including contributions
36
+ to search counts and guidance.
37
+
38
+ Keep a shared or group pool owner-less when it must not act as one person.
39
+ If an agent needs to act as Alice, put that access in her personal pool and set
40
+ `owner: "alice@example.com"` there, using the Workspace account she is intended
41
+ to use. Confirm the pool's tools and members before adding an owner. Follow
42
+ [the agent pool layout](./auth.md#organizing-an-agents-pools) for the full
43
+ configuration. People in the same pool, agents in owned pools and connectors
44
+ with a fixed `subject` address keep their behavior.
45
+
46
+ ## Optional settings and development tooling
47
+
48
+ Pool `description` is optional; omit it to preserve the existing MCP
49
+ instructions. A custom activity store can optionally return
50
+ `describe().optionSources.retentionDays` as `"config"` or `"default"` to mark
51
+ whether code explicitly set retention. Existing implementations need no
52
+ change; Settings marks retention as explicitly set only when the store
53
+ reports `"config"`.
54
+
55
+ The repository now uses pnpm for development and CI. Installed deployments and
56
+ the Node template continue to use npm; keep their npm commands and lockfiles.
57
+ Deployments without the operator UI or mapping-subject Workspace connectors in
58
+ owner-less agent pools can skip the UI and discovery steps above.
59
+
60
+ ## Verify
61
+
62
+ After upgrading, open `/` as a maintainer and confirm Overview loads, then open
63
+ `/endpoints` and a saved endpoint row link. Check any automation that reads the
64
+ browser title. With an agent's existing credentials, discover tools in its base
65
+ and personal pools and perform a bounded Workspace read in the owned pool.
66
+ Owner-less pools must omit mapping-subject Workspace connectors. Run
67
+ `connecta doctor` as described in [deployment upgrades](./deploying.md#upgrade-an-existing-deployment).
@@ -126,7 +126,9 @@ mean the provider applied it; inspect state or ask for the missing decision
126
126
  before issuing it again. [Retry semantics](./code-mode.md#retry-semantics) and
127
127
  [writes](./code-mode.md#writes) describe program outcomes and write accounting.
128
128
 
129
- ## Implementation and evidence
129
+ ## Contributor reference
130
+
131
+ ### Implementation and evidence
130
132
 
131
133
  The endpoint's built-in [usage skill](https://github.com/zackbart/connecta/blob/main/src/skills.ts)
132
134
  is maintained alongside [meta-tools](https://github.com/zackbart/connecta/blob/main/src/meta-tools.ts)