@zackbart/connecta 0.28.0 → 0.29.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 (320) hide show
  1. package/CHANGELOG.md +961 -5
  2. package/README.md +62 -39
  3. package/bin/connecta.mjs +110 -139
  4. package/bin/meta-tool-names.d.mts +2 -0
  5. package/bin/meta-tool-names.mjs +9 -0
  6. package/bin/version.mjs +3 -0
  7. package/dist/access-tokens.js +53 -63
  8. package/dist/activity-request.js +8 -0
  9. package/dist/activity.d.ts +76 -42
  10. package/dist/activity.js +110 -42
  11. package/dist/auth/clerk-sdk/client.d.ts +65 -0
  12. package/dist/auth/clerk-sdk/client.js +5 -0
  13. package/dist/auth/clerk-sdk/generated.js +1845 -0
  14. package/dist/auth/clerk-transport.js +19 -0
  15. package/dist/auth/clerk.d.ts +17 -7
  16. package/dist/auth/clerk.js +189 -107
  17. package/dist/auth/cloudflare-access.js +25 -26
  18. package/dist/auth/downstream-client-metadata.d.ts +29 -0
  19. package/dist/auth/downstream-client-metadata.js +133 -0
  20. package/dist/auth/downstream-oauth.js +1632 -1509
  21. package/dist/auth/downstream-revocation.js +106 -0
  22. package/dist/auth/oauth-token-response.js +231 -0
  23. package/dist/auth/oauth-v2-migration.js +116 -0
  24. package/dist/auth/static-oauth.js +526 -0
  25. package/dist/auth-elicitation.js +283 -0
  26. package/dist/branding.d.ts +8 -16
  27. package/dist/branding.js +26 -59
  28. package/dist/byte-read-response.js +39 -0
  29. package/dist/call-admission.js +12 -16
  30. package/dist/catalog-cache.js +739 -0
  31. package/dist/catalog-drift.d.ts +50 -15
  32. package/dist/catalog-drift.js +365 -128
  33. package/dist/catalog-fingerprint.js +2 -4
  34. package/dist/catalog-freshness.js +13 -0
  35. package/dist/catalog-limits.js +2 -2
  36. package/dist/catalog-service.js +368 -224
  37. package/dist/catalog.js +43 -84
  38. package/dist/concurrency.js +3 -4
  39. package/dist/config-defaults.d.ts +39 -0
  40. package/dist/config-defaults.js +46 -0
  41. package/dist/config-schema.d.ts +181 -0
  42. package/dist/config-schema.js +435 -0
  43. package/dist/config-value-sources.js +102 -0
  44. package/dist/config.d.ts +631 -0
  45. package/dist/config.js +503 -0
  46. package/dist/connector-access.d.ts +1 -0
  47. package/dist/connector-access.js +13 -15
  48. package/dist/connector-caller.d.ts +8 -0
  49. package/dist/connector-caller.js +15 -6
  50. package/dist/connector-scope.js +1 -1
  51. package/dist/connectors/api-connector.d.ts +220 -0
  52. package/dist/connectors/api-connector.js +214 -0
  53. package/dist/connectors/api.d.ts +8 -71
  54. package/dist/connectors/api.js +13 -99
  55. package/dist/connectors/guarded-fetch.d.ts +144 -0
  56. package/dist/connectors/guarded-fetch.js +108 -25
  57. package/dist/connectors/negotiation-cache.js +46 -0
  58. package/dist/connectors/option-shapes.js +27 -0
  59. package/dist/connectors/remote-mcp.d.ts +30 -4
  60. package/dist/connectors/remote-mcp.js +1780 -351
  61. package/dist/connectors/resource-uri.js +335 -0
  62. package/dist/credential-contract.d.ts +10 -0
  63. package/dist/credential-rules.js +1 -3
  64. package/dist/credential-url.js +108 -0
  65. package/dist/credentials.d.ts +6 -1
  66. package/dist/credentials.js +27 -18
  67. package/dist/d1.d.ts +130 -0
  68. package/dist/d1.js +290 -0
  69. package/dist/describe-config.d.ts +165 -0
  70. package/dist/describe-config.js +315 -0
  71. package/dist/described.js +106 -0
  72. package/dist/downstream-input-context.js +119 -0
  73. package/dist/downstream-input.js +323 -0
  74. package/dist/errors.d.ts +97 -20
  75. package/dist/errors.js +158 -80
  76. package/dist/execute.js +453 -305
  77. package/dist/executor-admission.d.ts +1 -0
  78. package/dist/executor-admission.js +11 -15
  79. package/dist/executor-contract.d.ts +20 -0
  80. package/dist/executor-contract.js +48 -0
  81. package/dist/executor-result.js +24 -9
  82. package/dist/executors/quickjs-child.js +3 -4
  83. package/dist/executors/quickjs-runtime.js +147 -59
  84. package/dist/executors/quickjs.js +29 -35
  85. package/dist/favicon.js +1 -2
  86. package/dist/fix-prompt.js +1 -3
  87. package/dist/guest-runtime.js +188 -0
  88. package/dist/guest-types.d.ts +176 -0
  89. package/dist/guest-types.js +1 -0
  90. package/dist/html-security.js +24 -0
  91. package/dist/identity.js +1 -3
  92. package/dist/inbound-credential.js +52 -0
  93. package/dist/index.d.ts +25 -257
  94. package/dist/index.js +65 -478
  95. package/dist/invocation-auth.js +69 -0
  96. package/dist/invocation.js +230 -73
  97. package/dist/json-schema.d.ts +1 -1
  98. package/dist/mcp-client-context.js +35 -0
  99. package/dist/mcp-result.js +11 -7
  100. package/dist/meta-output.js +76 -0
  101. package/dist/meta-tool-names.d.ts +2 -0
  102. package/dist/meta-tool-names.js +3 -0
  103. package/dist/meta-tools.js +285 -209
  104. package/dist/module-contracts.d.ts +8 -23
  105. package/dist/node.d.ts +0 -2
  106. package/dist/node.js +2 -2
  107. package/dist/oauth-callback-outcome.js +1 -3
  108. package/dist/oauth-handoff.js +117 -0
  109. package/dist/oauth-partition.js +17 -0
  110. package/dist/oauth-sealing.js +20 -0
  111. package/dist/operator-record.d.ts +137 -0
  112. package/dist/operator-record.js +420 -0
  113. package/dist/operator-ui/assets.js +31 -0
  114. package/dist/operator-ui/contract.d.ts +72 -0
  115. package/dist/operator-ui/contract.js +1 -0
  116. package/dist/operator-ui/fix-prompts.d.ts +18 -0
  117. package/dist/operator-ui/generated.js +5 -4
  118. package/dist/operator-ui/model.d.ts +12 -87
  119. package/dist/operator-ui/model.js +2 -9
  120. package/dist/operator-ui/setup-commands.js +1 -3
  121. package/dist/operator-ui/view.d.ts +323 -0
  122. package/dist/operator-ui/view.js +62 -189
  123. package/dist/page-styles.js +2 -3
  124. package/dist/program-source.js +14 -2
  125. package/dist/{exempt-writes.js → program-writes.js} +55 -18
  126. package/dist/provider.d.ts +142 -0
  127. package/dist/provider.js +187 -0
  128. package/dist/providers/_shared/google/result-size.js +57 -0
  129. package/dist/providers/_shared/google/service-account.d.ts +58 -0
  130. package/dist/providers/_shared/google/service-account.js +500 -0
  131. package/dist/providers/_shared/google/workspace.d.ts +276 -0
  132. package/dist/providers/_shared/google/workspace.js +518 -0
  133. package/dist/providers/basecamp/index.d.ts +43 -0
  134. package/dist/providers/basecamp/index.js +997 -0
  135. package/dist/providers/basecamp/skill.generated.js +9 -0
  136. package/dist/providers/breeze/index.d.ts +34 -0
  137. package/dist/providers/breeze/index.js +1446 -0
  138. package/dist/providers/breeze/skill.generated.js +9 -0
  139. package/dist/providers/ccb/index.d.ts +103 -0
  140. package/dist/providers/ccb/index.js +1725 -0
  141. package/dist/providers/ccb/skill.generated.js +9 -0
  142. package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
  143. package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
  144. package/dist/providers/cloudflare/skill.generated.js +10 -0
  145. package/dist/providers/docs/index.d.ts +54 -0
  146. package/dist/providers/docs/index.js +1237 -0
  147. package/dist/providers/docs/skill.generated.js +9 -0
  148. package/dist/providers/drive/index.d.ts +53 -0
  149. package/dist/providers/drive/index.js +1523 -0
  150. package/dist/providers/drive/skill.generated.js +9 -0
  151. package/dist/providers/forms/index.d.ts +54 -0
  152. package/dist/providers/forms/index.js +1217 -0
  153. package/dist/providers/forms/skill.generated.js +11 -0
  154. package/dist/providers/github/auth.d.ts +32 -0
  155. package/dist/providers/github/auth.js +361 -0
  156. package/dist/providers/github/hosted.js +218 -0
  157. package/dist/providers/github/index.d.ts +13 -0
  158. package/dist/providers/github/index.js +140 -0
  159. package/dist/providers/github/key.js +100 -0
  160. package/dist/providers/github/rest.js +321 -0
  161. package/dist/providers/github/scope.d.ts +29 -0
  162. package/dist/providers/github/scope.js +124 -0
  163. package/dist/providers/github/skill.generated.js +8 -0
  164. package/dist/providers/gmail/index.d.ts +51 -0
  165. package/dist/providers/gmail/index.js +1436 -0
  166. package/dist/providers/gmail/skill.generated.js +9 -0
  167. package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
  168. package/dist/providers/linear/index.js +352 -0
  169. package/dist/providers/linear/skill.generated.js +8 -0
  170. package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
  171. package/dist/providers/mixpanel/index.js +416 -0
  172. package/dist/providers/mixpanel/skill.generated.js +9 -0
  173. package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
  174. package/dist/providers/{notion.js → notion/index.js} +226 -239
  175. package/dist/providers/notion/skill.generated.js +10 -0
  176. package/dist/providers/overflow/index.d.ts +60 -0
  177. package/dist/providers/overflow/index.js +1535 -0
  178. package/dist/providers/overflow/skill.generated.js +8 -0
  179. package/dist/providers/planning-center/index.d.ts +59 -0
  180. package/dist/providers/planning-center/index.js +2199 -0
  181. package/dist/providers/planning-center/skill.generated.js +10 -0
  182. package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
  183. package/dist/providers/revenuecat/index.js +557 -0
  184. package/dist/providers/revenuecat/skill.generated.js +11 -0
  185. package/dist/providers/sheets/index.d.ts +54 -0
  186. package/dist/providers/sheets/index.js +1237 -0
  187. package/dist/providers/sheets/skill.generated.js +10 -0
  188. package/dist/providers/slides/index.d.ts +53 -0
  189. package/dist/providers/slides/index.js +2645 -0
  190. package/dist/providers/slides/skill.generated.js +9 -0
  191. package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
  192. package/dist/providers/stripe/index.js +266 -0
  193. package/dist/providers/stripe/skill.generated.js +11 -0
  194. package/dist/providers/tithely/index.d.ts +49 -0
  195. package/dist/providers/tithely/index.js +1003 -0
  196. package/dist/providers/tithely/skill.generated.js +8 -0
  197. package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
  198. package/dist/providers/vercel/index.js +1038 -0
  199. package/dist/providers/vercel/mcp-ownership.js +212 -0
  200. package/dist/providers/vercel/skill.generated.js +11 -0
  201. package/dist/registry.d.ts +108 -171
  202. package/dist/registry.js +513 -925
  203. package/dist/request-state.js +21 -0
  204. package/dist/resource-read.js +100 -0
  205. package/dist/result-shapes.js +17 -49
  206. package/dist/retired-toolkits.js +1 -1
  207. package/dist/routes/access-tokens.js +3 -6
  208. package/dist/routes/activity-context.js +21 -0
  209. package/dist/routes/activity-disclosure.js +40 -0
  210. package/dist/routes/activity.js +59 -21
  211. package/dist/routes/actor-labels.js +3 -50
  212. package/dist/routes/connect.js +221 -0
  213. package/dist/routes/credentials.js +15 -27
  214. package/dist/routes/mcp.js +356 -130
  215. package/dist/routes/oauth-client-metadata.js +15 -0
  216. package/dist/routes/oauth-management.js +45 -107
  217. package/dist/routes/oauth.js +136 -57
  218. package/dist/routes/operator.js +3 -3
  219. package/dist/routes/shared.d.ts +25 -64
  220. package/dist/routes/shared.js +160 -80
  221. package/dist/routes/skills.js +30 -0
  222. package/dist/routes/ui-config.js +225 -0
  223. package/dist/routes/ui-data.js +40 -35
  224. package/dist/routes/ui.js +66 -56
  225. package/dist/runtime/admission.js +4 -2
  226. package/dist/runtime/call-admission.js +3 -1
  227. package/dist/runtime/connector-scope.js +1 -4
  228. package/dist/runtime/run.js +10 -39
  229. package/dist/runtime/services.js +12 -117
  230. package/dist/runtime/shared-read.js +1 -3
  231. package/dist/runtime/storage.js +8 -1
  232. package/dist/sent-secrets.d.ts +52 -0
  233. package/dist/sent-secrets.js +474 -0
  234. package/dist/server.js +28 -71
  235. package/dist/skills.js +406 -174
  236. package/dist/sqlite.d.ts +48 -0
  237. package/dist/sqlite.js +166 -0
  238. package/dist/storage/keys.js +428 -0
  239. package/dist/storage/memory.js +7 -0
  240. package/dist/storage/sql.d.ts +66 -0
  241. package/dist/storage/sql.js +534 -0
  242. package/dist/timeout.js +4 -2
  243. package/dist/tool-name.js +17 -0
  244. package/dist/tool-safety.d.ts +9 -35
  245. package/dist/tool-safety.js +20 -40
  246. package/dist/types.d.ts +320 -54
  247. package/dist/types.js +0 -1
  248. package/dist/ui.d.ts +7 -19
  249. package/dist/ui.js +76 -90
  250. package/dist/url-safety.js +3 -6
  251. package/dist/usage-guide.js +137 -0
  252. package/dist/validate.d.ts +7 -0
  253. package/dist/validate.js +309 -47
  254. package/dist/version.d.ts +1 -7
  255. package/dist/version.js +3 -7
  256. package/dist/worker.d.ts +11 -0
  257. package/dist/worker.js +434 -0
  258. package/documentation/architecture.md +665 -230
  259. package/documentation/auth.md +962 -458
  260. package/documentation/code-mode.md +501 -312
  261. package/documentation/meta-tools.md +449 -200
  262. package/documentation/operator-tests.md +93 -0
  263. package/documentation/provider-migration-0.29.md +60 -0
  264. package/examples/worker/AGENTS.md +34 -12
  265. package/examples/worker/README.md +286 -255
  266. package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
  267. package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
  268. package/examples/worker/src/connecta.config.ts +151 -0
  269. package/examples/worker/src/index.ts +14 -197
  270. package/examples/worker/wrangler.jsonc +15 -35
  271. package/package.json +125 -35
  272. package/templates/node/.dockerignore +1 -2
  273. package/templates/node/.env.example +20 -21
  274. package/templates/node/AGENTS.md +21 -17
  275. package/templates/node/Dockerfile +3 -5
  276. package/templates/node/README.md +133 -58
  277. package/templates/node/docker-compose.yml +11 -12
  278. package/templates/node/package.json +4 -2
  279. package/templates/node/src/connecta.config.ts +119 -0
  280. package/templates/node/src/index.ts +9 -154
  281. package/templates/node/src/provision-token.ts +17 -0
  282. package/AGENTS.md +0 -170
  283. package/dist/artifacts/connector.d.ts +0 -58
  284. package/dist/artifacts/connector.js +0 -730
  285. package/dist/artifacts/document.js +0 -86
  286. package/dist/artifacts/guide.js +0 -140
  287. package/dist/artifacts/html-scan.js +0 -329
  288. package/dist/artifacts/json.js +0 -78
  289. package/dist/artifacts/kv-store.d.ts +0 -20
  290. package/dist/artifacts/kv-store.js +0 -140
  291. package/dist/artifacts/markdown.js +0 -478
  292. package/dist/artifacts/module.d.ts +0 -37
  293. package/dist/artifacts/module.js +0 -66
  294. package/dist/artifacts/operations.d.ts +0 -197
  295. package/dist/artifacts/operations.js +0 -1002
  296. package/dist/artifacts/refresh.d.ts +0 -55
  297. package/dist/artifacts/refresh.js +0 -236
  298. package/dist/artifacts/routes.js +0 -206
  299. package/dist/artifacts/types.d.ts +0 -214
  300. package/dist/artifacts/types.js +0 -22
  301. package/dist/artifacts/validate.d.ts +0 -62
  302. package/dist/artifacts/validate.js +0 -651
  303. package/dist/artifacts.d.ts +0 -6
  304. package/dist/artifacts.js +0 -9
  305. package/dist/auth/bearer.d.ts +0 -11
  306. package/dist/auth/bearer.js +0 -50
  307. package/dist/providers/linear.js +0 -217
  308. package/dist/providers/mixpanel.js +0 -247
  309. package/dist/providers/revenuecat.js +0 -317
  310. package/dist/providers/stripe.js +0 -253
  311. package/dist/providers/vercel.js +0 -1253
  312. package/dist/storage/file.d.ts +0 -15
  313. package/dist/storage/file.js +0 -455
  314. package/ethos.md +0 -93
  315. package/examples/worker/src/cloudflare-kv.ts +0 -43
  316. package/examples/worker/src/d1-activity-row.ts +0 -155
  317. package/examples/worker/src/d1-activity.ts +0 -147
  318. package/examples/worker/src/d1-storage.ts +0 -138
  319. package/examples/worker/src/r2-artifact-blobs.ts +0 -29
  320. package/templates/node/src/file-activity.ts +0 -182
package/dist/skills.js CHANGED
@@ -1,107 +1,18 @@
1
- import { boundedEchoText } from "./errors.js";
2
- const ROUTE = "Choose a route before discovery. A known-address read needs only call_tool. Unknown-address read-only work starts with execute_code to discover, call, and return the answer; use the same route for reduction, multiple or dependent calls, loops, joins, or branches. Keep discovery and calls together when schemas suffice; do not return catalog matches alone. Sample unfamiliar reads.";
3
- const RECOVERY = 'After auth_required use authorize_connector. After a truncated direct result use get_result. Guidance is on demand: fetch skills({ name: "usage" }) only when these instructions and the tool description are insufficient or a run needs repair.';
1
+ import { USAGE_SKILL } from "./usage-guide.js";
2
+ import { boundedEchoText, ConnectorCallError } from "./errors.js";
3
+ import { Effect } from "effect";
4
+ import { runEdge, withDeadlineEffect } from "./runtime/run.js";
5
+ import { closeScopeOnExit } from "./runtime/connector-scope.js";
6
+ import { sentSecretsForRequest } from "./sent-secrets.js";
7
+ const ROUTE = "Choose a route before discovery. One known-address read uses call_tool; one known-address write uses call_destructive_tool. Unknown-address read-only work starts with execute_code to discover, call, and return the answer; use the same route for reduction, multiple or dependent calls, loops, joins, or branches. Keep discovery and calls together when schemas suffice; do not return catalog matches alone. Sample unfamiliar reads.";
8
+ const RECOVERY = 'After auth_required use authorize_connector. After a truncated direct result use connecta.result. Guidance is on demand: fetch skills({ name: "usage" }) only when these instructions and the tool description are insufficient or a run needs repair.';
4
9
  /**
5
10
  * The always-loaded MCP `instructions` string. A program runs reads and the
6
- * writes config exempts from approval; every other write is its own top-level
11
+ * writes in trusted pools; a read-only pool routes every write to top-level
7
12
  * call_destructive_tool, where the host's permission prompt is the approval.
8
13
  */
9
- export const CONNECTA_INSTRUCTIONS = `${ROUTE} Only readOnlyHint: true tools and config-exempt writes run there. Keep catalog inspection and other unannotated, write-capable, or destructive work top level: search_tools then call_destructive_tool when a call is needed. Never repeat a write to recover its output. ${RECOVERY}`;
10
- const USAGE_SKILL_BASE = `# Connecta usage
11
-
12
- ## The surface
13
-
14
- Seven tools: \`execute_code\`, \`search_tools\`, \`call_tool\`, \`call_destructive_tool\`, \`authorize_connector\`, \`get_result\`, \`skills\`. Discovery and multi-call work live in a program. Top-level search remains for catalog inspection and approval-required work.
15
-
16
- Follow the MCP instructions for routing. Read at most once for syntax or repair.
17
-
18
- ## Inside a program
19
-
20
- Write one plain-JavaScript async arrow function, without TypeScript or imports. Return reduced JSON-shaped data.
21
-
22
- The minimum guest API is:
23
-
24
- - \`connecta.call("connector.tool", args)\` uses the canonical address and returns the unwrapped value.
25
- - \`connecta.search(args)\` returns \`{ tools, total, offset, limit, hasMore }\`; \`connecta.describe(args)\` returns \`{ tools }\`.
26
- - Use \`Promise.all\` for independent calls, or \`Promise.allSettled\` to keep successes and failures.
27
- - \`console.log(...)\` is captured. \`connecta.emit(block)\` produces rich output.
28
-
29
- ## Discover and select
30
-
31
- Search and call in one run when schemas suffice. Sample unfamiliar reads. Reduce a one-time write's full result here or page a direct call with get_result; never repeat the write to recover output. Search distinct operations separately with 2–4 distinctive action/object terms.
32
-
33
- For top-level catalog inspection or approval-required discovery, omit \`limit\` initially (the default is 8), then page with a limit up to 50 if needed. Empty or whitespace-only queries browse all tools. A non-empty query with no ASCII terms returns no matches; mixed input searches with its ASCII terms. \`includeSchemas: "compact"\` adds bounded input and available output shapes. An \`outputSchemaSource: "observed"\` shape is a hint, not a contract. Plain objects expose \`inputKeys\`, \`requiredInputKeys\`, and \`outputKeys\`; truncation flags mark incomplete shapes; matches also carry declared annotations.
34
-
35
- - \`connecta.search({})\` loads all catalogs. Pass \`connector: "<id>"\` when the integration is obvious. Use \`safety: "readOnly"\` for reads. These inputs filter discovery; they grant no authority.
36
- - Use \`includeSchemas: "json"\` for programmatic schema inspection; compact schemas are text, not objects with \`.properties\`. Check connectorTitle for the account/environment, then address, purpose, annotations, inputs, and outputs. Never select only because a result ranks first or has fewer required inputs.
37
- - Supply every \`requiredInputKey\` from the task or a prior result. For dependencies, match the earlier \`outputKey\` to the later required key. An empty required-key list does not permit invented arguments. Missing \`outputKeys\` means inspect \`outputSchema\`.
38
- - Use \`connecta.describe({ address })\` or \`{ addresses }\` when a compact schema is truncated or insufficient. Use \`format: "json"\` only for exact constraints. Write the property names the schema displays; never guess positions or aliases.
39
- - Reduce through available output keys. An observed key is a hint; later results may differ. Do not guess collection roots such as \`items\` or \`results\`. If a match is missing, re-search or describe.
40
- - Match provider identifiers and names exactly after resolving them from source data or a connector guide. A broad regular expression that merely finds a plausible value is not identity resolution.
41
- - Preserve the schema's JSON types exactly: a numeric id is a number, not a numeric-looking string.
42
- - Validate tabular headers, row arrays, and row widths before mapping them. Never let a header or partial row become data.
43
-
44
- Only explicitly \`readOnlyHint: true\` calls and writes the deployment's config exempts from approval run in a program. Any other write is refused before it is sent: send it through top-level \`call_destructive_tool\`, where the host asks. Sandbox code cannot widen host gates.
45
-
46
- ## Errors and repair
47
-
48
- Caught Connecta errors expose \`message\`, \`code\`, \`retryable\`, and \`details\`. Promise rejections retain these fields. Branch on fields, never prose. After a shared argument failure, repair one call before repeating it across other records. Do not retry \`retryable: false\`, and do not retry \`rate_limited\` immediately because portable code has no timer.
49
-
50
- - \`destructive_tool_requires_approval\`: stop and send the returned address through top-level \`call_destructive_tool\`.
51
- - \`write_outcome_unknown\`: an exempt write was sent but unanswered and is never re-sent; check the target.
52
- - \`auth_required\`: let the failure reach the model, then use top-level \`authorize_connector\`, give its handoff to the operator, and retry after recovery.
53
- - Truncated direct call: page with \`get_result\`, never repeat a write. A program result has no page handle; reduce a read in a new program. After a write, check the target or report the gap.
54
- - Unknown addresses and tools carry scoped search recovery. Use it inside the current run. Do not invent an address.
55
-
56
- For a direct call, \`resultMode: "value"\` unwraps the result. \`timeoutMs\` sets its deadline. Every call makes one attempt; use the returned error classification and retry hint to decide whether to reissue. \`diagnostics: true\` adds timing.
57
-
58
- \`get_result({ id, offset?, maxBytes? })\` returns a one-line JSON header \`{ resultId, offset, bytes, totalBytes, hasMore, nextAction? }\`, a newline, and then the page as raw text, for a direct-call result. Both sizes are byte counts: \`maxBytes\` must be a whole number at least 1, defaults to the result's cap, and is clamped to it; \`offset\` must be a whole number at least 0 and defaults to 0. An offset inside a multi-byte character moves back to its first byte, and the header reports the served offset. Follow \`nextAction\` until \`hasMore\` is false to reassemble pages. An unknown or expired id is an error.
59
-
60
- The \`execute_code\` description states this deployment's host-call and write budgets and per-call deadline.
61
-
62
- ## Runtime portability
63
-
64
- Portable code uses standard JavaScript builtins, \`connecta\`, and \`console.*\`. QuickJS blocks imports and lacks fetch, process, timers, crypto, and WebSocket. Dynamic Workers must use only \`{ loader }\`; bindings, modules, or globalOutbound grant ambient authority. With loader only, environment maps are empty; node:fs/http/https are absent; outbound fetch, WebSocket, node:net, and node:tls are denied; DNS is unresolved. Runtime builtins remain through \`import()\` and \`process.getBuiltinModule()\`, including node:path and cloudflare:workers; this set can drift. Timers, process, crypto, WebSocket, and data: fetch remain. Avoid every runtime-only capability because QuickJS fails.
65
-
66
- ## Examples
67
-
68
- One read-only call at a known address:
69
-
70
- \`async () => await connecta.call("crm.get_account", { id: "acct_42" })\`
71
-
72
- Dependent lookup: verify connector \`ci\`, run 42, and these schema fields first.
73
-
74
- \`\`\`js
75
- async () => {
76
- const find = async name => {
77
- const page = await connecta.search({ connector: "ci", query: name, safety: "readOnly", includeSchemas: "compact" });
78
- if (page.queryAnalysis?.catalogError) throw new Error(JSON.stringify(page.queryAnalysis.catalogError));
79
- return page.tools.find(tool => tool.name === name);
80
- };
81
- const runTool = await find("get_run");
82
- if (!runTool) return { gap: "Run lookup not resolved" };
83
- const run = await connecta.call(runTool.address, { runId: 42 });
84
- if (!run.failedJobId) return { status: run.status, gap: "No failed job identified" };
85
- const logsTool = await find("get_job_logs");
86
- if (!logsTool) return { status: run.status, gap: "Job logs not resolved" };
87
- const logs = await connecta.call(logsTool.address, { jobId: run.failedJobId });
88
- return logs.filter(row => row.level === "error").map(({ timestamp, message }) => ({ timestamp, message }));
89
- }
90
- \`\`\`
91
-
92
- ## Media output
93
-
94
- \`connecta.emit\` accepts text, image, or audio blocks and delivers them only on success. Its byte and block budgets are separate from the JSON return budget. Return data for the client to render as a view.
95
-
96
- `;
97
- /** Deployment-scoped guide routing appended to the shared usage guide. */
98
- const CONNECTOR_GUIDES_SECTION = `
99
- ## Per-connector guides
100
-
101
- Connector guides appear in \`skills({})\` and discovery with an exact \`guide\` name and bounded \`guideSummary\`. Fetch only a listed or carried name with \`skills({ name: <guide> })\`; never infer one from a connector id. \`guideRequired: true\` is a hard stop. \`guideRequiredReasons\` says why: \`connector_required\` and \`approval_required\` stand after schema expansion; \`schema_truncated\` clears after exact describe. Otherwise fetch for a relevant sequence, unit, pagination rule, alias, or API convention. A read-only call with a complete compact schema may skip an irrelevant guide. Connector guides never apply to another deployment.
102
- `;
103
- /** Shared Connecta routing guidance, byte-identical across deployments. */
104
- export const USAGE_SKILL = USAGE_SKILL_BASE + CONNECTOR_GUIDES_SECTION;
14
+ export const CONNECTA_INSTRUCTIONS = `${ROUTE} Programs call reads in read-only pools and may also write in trusted pools. The execute_code description names this endpoint's trust. In a read-only pool, discover writes with search_tools then use call_destructive_tool. Never repeat a write to recover its output. ${RECOVERY}`;
15
+ export { USAGE_SKILL } from "./usage-guide.js";
105
16
  /** True when at least one of `connectors` carries a usage guide. */
106
17
  export function hasConnectorGuides(connectors) {
107
18
  return connectors.some((connector) => connectorGuide(connector) !== undefined);
@@ -112,10 +23,15 @@ export function hasConnectorGuides(connectors) {
112
23
  * copy. Guide-free deployments still pay no fixed tool-description cost: the
113
24
  * conditional notes in meta-tools.ts remain absent.
114
25
  */
115
- function usageSkill(_connectors) {
26
+ function usageSkill() {
116
27
  return USAGE_SKILL;
117
28
  }
118
- const INVESTIGATE_SKILL = `# Investigate across services
29
+ const INVESTIGATE_SKILL = `---
30
+ name: investigate
31
+ description: Plan purchase verification, experiment checks, and customer or deployment investigations across services; resolve scope and capability limits before querying.
32
+ ---
33
+
34
+ # Investigate across services
119
35
 
120
36
  ## Plan the investigation
121
37
 
@@ -147,8 +63,8 @@ const AVAILABLE_SKILLS = [
147
63
  * Namespace for operator-authored per-connector guides. Built-in skill names
148
64
  * are bare identifiers and never contain ":", so `connector:<id>` cannot
149
65
  * collide with one — not even when a connector's id is literally "usage".
150
- * The prefixed form is the ONLY way to reach a connector guide: a bare
151
- * connector id is never resolved, so nothing shadows anything silently.
66
+ * Canonical skill URIs and the prefixed compatibility alias reach connector
67
+ * guides. A bare connector id never resolves, so nothing shadows silently.
152
68
  */
153
69
  const CONNECTOR_SKILL_PREFIX = "connector:";
154
70
  /** The skill name that fetches `connector`'s guide. */
@@ -197,8 +113,7 @@ function isAbbreviation(text, end) {
197
113
  return true;
198
114
  if (/^(?:Mr|Mrs|Ms|Dr|Prof|Sr|Jr|St)\.$/iu.test(token))
199
115
  return true;
200
- if (!/^(?:[A-Za-z]\.){2,}$/u.test(token) &&
201
- !/^(?:vs|etc|approx|dept|fig|no)\.$/iu.test(token)) {
116
+ if (!/^(?:[A-Za-z]\.){2,}$/u.test(token) && !/^(?:vs|etc|approx|dept|fig|no)\.$/iu.test(token)) {
202
117
  return false;
203
118
  }
204
119
  // Initialisms can end a sentence or extend a name ("U.S. East region").
@@ -206,16 +121,13 @@ function isAbbreviation(text, end) {
206
121
  // complete thought, while a missed ending gets an honest ellipsis. Count
207
122
  // the period only with narrow evidence of a new clause: an article in one
208
123
  // of its first two words. This is grammar evidence, not a starter-word list.
209
- const following = text
210
- .slice(end)
211
- .match(/^[)\]}'"”’]*\s+(\S+)(?:\s+(\S+))?/u);
124
+ const following = text.slice(end).match(/^[)\]}'"”’]*\s+(\S+)(?:\s+(\S+))?/u);
212
125
  if (!following)
213
126
  return false;
214
127
  const [, nextWord, afterNext] = following;
215
128
  const startsClause = nextWord !== undefined &&
216
129
  /^\p{Lu}/u.test(nextWord) &&
217
- (OPENS_CLAUSE_RE.test(nextWord) ||
218
- (afterNext !== undefined && ARTICLE_RE.test(afterNext)));
130
+ (OPENS_CLAUSE_RE.test(nextWord) || (afterNext !== undefined && ARTICLE_RE.test(afterNext)));
219
131
  return !startsClause;
220
132
  }
221
133
  /** Drop a leading YAML frontmatter block — metadata, not summary text. */
@@ -253,8 +165,7 @@ function boundedSummary(summary) {
253
165
  if (end > GUIDE_SUMMARY_LENGTH)
254
166
  break;
255
167
  const punctuationEnd = (match.index ?? 0) + 1;
256
- if (match[0].startsWith(".") &&
257
- isAbbreviation(normalized, punctuationEnd)) {
168
+ if (match[0].startsWith(".") && isAbbreviation(normalized, punctuationEnd)) {
258
169
  continue;
259
170
  }
260
171
  // Do not mistake another short fragment for a useful complete thought.
@@ -321,8 +232,7 @@ function summarizeGuide(connector, guide) {
321
232
  }
322
233
  if (startsTable(lines, index)) {
323
234
  index++;
324
- while (index + 1 < lines.length &&
325
- (lines[index + 1] ?? "").includes("|")) {
235
+ while (index + 1 < lines.length && (lines[index + 1] ?? "").includes("|")) {
326
236
  index++;
327
237
  }
328
238
  continue;
@@ -373,9 +283,9 @@ export function connectorGuideSummary(connector) {
373
283
  if (!guide)
374
284
  return undefined;
375
285
  const configured = typeof connector.usageGuide === "object"
376
- // Registry construction rejects over-budget configured summaries. Keep
377
- // normalization here so direct Connector callers see the same text.
378
- ? boundedSummary(connector.usageGuide.summary ?? "")
286
+ ? // Registry construction rejects over-budget configured summaries. Keep
287
+ // normalization here so direct Connector callers see the same text.
288
+ boundedSummary(connector.usageGuide.summary ?? "")
379
289
  : undefined;
380
290
  return configured ?? summarizeGuide(connector, guide);
381
291
  }
@@ -385,72 +295,394 @@ export function connectorGuideRequired(connector) {
385
295
  typeof connector.usageGuide === "object" &&
386
296
  connector.usageGuide.required === true);
387
297
  }
388
- /**
389
- * Every fetchable skill: the built-in guides plus one entry per connector that
390
- * carries a usage guide. Derived from the connector list passed in — the single
391
- * place guide visibility is decided.
392
- */
393
- export function listSkills(connectors) {
394
- const listing = AVAILABLE_SKILLS.map((skill) => ({
395
- name: skill.name,
396
- description: skill.description,
298
+ /** Complete entries stay atomic within ChatGPT's five-skill import budget. */
299
+ const SKILL_PAGE_SIZE = 5;
300
+ const MAX_SKILL_FILES = 512;
301
+ const MAX_SKILL_BYTES = 16 * 1024 * 1024;
302
+ const MAX_SKILL_CATALOG_BYTES = 8 * 1024 * 1024;
303
+ const MAX_SKILLS = 1_024;
304
+ const encoder = new TextEncoder();
305
+ const PRIVATE = { resultType: "complete", ttlMs: 0, cacheScope: "private" };
306
+ /** Bound metadata before cloning it, without another payload-sized byte array. */
307
+ function jsonBytes(value) {
308
+ const json = JSON.stringify(value);
309
+ let bytes = 0;
310
+ for (let i = 0; i < json.length; i++) {
311
+ const code = json.charCodeAt(i);
312
+ if (code < 0x80)
313
+ bytes++;
314
+ else if (code < 0x800)
315
+ bytes += 2;
316
+ else if (code >= 0xd800 && code <= 0xdbff && json.charCodeAt(i + 1) >= 0xdc00 && json.charCodeAt(i + 1) <= 0xdfff) {
317
+ bytes += 4;
318
+ i++;
319
+ }
320
+ else
321
+ bytes += 3;
322
+ }
323
+ return bytes;
324
+ }
325
+ function localRecord(name, uri, description, content, aliases) {
326
+ return { entry: { uri, frontmatter: { name, description }, resources: "dynamic" }, content, aliases };
327
+ }
328
+ /** JSON frontmatter is YAML too; no runtime provider imports or filesystem reads. */
329
+ function validSkillName(name) {
330
+ return name.length <= 64 && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name);
331
+ }
332
+ async function localRecords(connectors) {
333
+ const builtIns = AVAILABLE_SKILLS.map((skill) => localRecord(skill.name, `skill://connecta/${skill.name}/SKILL.md`, skill.description, skill.content(), [
334
+ skill.name,
335
+ `skill://connecta/${skill.name}`,
336
+ ]));
337
+ const guides = await Promise.all(connectors
338
+ .filter((connector) => connectorGuide(connector) !== undefined)
339
+ .sort((a, b) => Number(connectorGuideRequired(b)) - Number(connectorGuideRequired(a)) ||
340
+ (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
341
+ .map(async (connector) => {
342
+ const name = validSkillName(connector.id)
343
+ ? connector.id
344
+ : `connector-${[...new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(connector.id)))]
345
+ .map((byte) => byte.toString(16).padStart(2, "0"))
346
+ .join("")
347
+ .slice(0, 48)}`;
348
+ const description = connectorGuideSummary(connector);
349
+ const root = `skill://connecta/connectors/${encodeURIComponent(name)}`;
350
+ const content = `---\n${JSON.stringify({ name, description }, null, 2)}\n---\n\n${connectorGuide(connector)}`;
351
+ return localRecord(name, `${root}/SKILL.md`, description, content, [
352
+ connectorSkillName(connector.id),
353
+ `skill://connecta/connectors/${encodeURIComponent(connector.id)}`,
354
+ root,
355
+ ]);
397
356
  }));
398
- for (const connector of connectors) {
399
- // Undefined here means "no guide" and nothing else: a connector that has
400
- // one always summarizes to a non-empty line, configured or derived.
401
- const summary = connectorGuideSummary(connector);
402
- if (!summary)
403
- continue;
404
- listing.push({
405
- name: connectorSkillName(connector.id),
406
- description: summary,
407
- });
357
+ if (new Set(guides.map((guide) => guide.entry.uri)).size !== guides.length)
358
+ throw new ConnectorCallError("unavailable", "Connector skill slugs collide.");
359
+ return [builtIns[0], ...guides, ...builtIns.slice(1)];
360
+ }
361
+ /** Keep the downstream path tree, so relative supporting-file references still resolve. */
362
+ export function downstreamSkillUri(connectorId, uri) {
363
+ if (uri.length > 32_768)
364
+ throw new ConnectorCallError("unavailable", "Downstream skill URI exceeds its bound.");
365
+ const match = /^([A-Za-z][A-Za-z0-9+.-]*:(?:\/\/[^/?#]*)?)(\/[^?#]+)$/.exec(uri);
366
+ if (!match ||
367
+ match[2]
368
+ .split("/")
369
+ .slice(1)
370
+ .some((segment) => {
371
+ try {
372
+ const decoded = decodeURIComponent(segment);
373
+ return (!decoded ||
374
+ decoded === "." ||
375
+ decoded === ".." ||
376
+ decoded.includes("\\") ||
377
+ decoded.includes("/") ||
378
+ decoded.includes("\u0000"));
379
+ }
380
+ catch {
381
+ return true;
382
+ }
383
+ }))
384
+ throw new ConnectorCallError("unavailable", "Downstream skill URI is not a supported file URI.");
385
+ const authority = match[1].split("://")[1];
386
+ // A skill may root at the authority, e.g. skill://review/SKILL.md.
387
+ // Repeating it as a path segment preserves the Agent Skills directory name.
388
+ const mapped = `skill://downstream/${encodeURIComponent(connectorId)}/${encodeURIComponent(match[1])}${authority ? `/${encodeURIComponent(authority)}` : ""}${match[2]}`;
389
+ if (mapped.length > 32_768)
390
+ throw new ConnectorCallError("unavailable", "Downstream skill URI exceeds its bound.");
391
+ return mapped;
392
+ }
393
+ function validateEntry(entry) {
394
+ if (!entry ||
395
+ typeof entry.uri !== "string" ||
396
+ entry.uri.length > 32_768 ||
397
+ typeof entry.frontmatter !== "object" ||
398
+ !entry.frontmatter ||
399
+ Array.isArray(entry.frontmatter) ||
400
+ typeof entry.frontmatter.name !== "string" ||
401
+ !validSkillName(entry.frontmatter.name) ||
402
+ typeof entry.frontmatter.description !== "string" ||
403
+ !entry.frontmatter.description ||
404
+ entry.frontmatter.description.length > 1024 ||
405
+ (entry.resources !== "dynamic" && !Array.isArray(entry.resources))) {
406
+ throw new ConnectorCallError("unavailable", "Downstream skill entry is invalid.");
407
+ }
408
+ const root = entry.uri.slice(0, entry.uri.lastIndexOf("/") + 1);
409
+ if (!entry.uri.endsWith("/SKILL.md") || decodeURIComponent(root.split("/").at(-2) ?? "") !== entry.frontmatter.name) {
410
+ throw new ConnectorCallError("unavailable", "Downstream skill root does not match its name.");
411
+ }
412
+ if (entry.resources === "dynamic")
413
+ return;
414
+ if (!entry.resources.length || entry.resources.length > MAX_SKILL_FILES)
415
+ throw new ConnectorCallError("unavailable", "Downstream skill manifest exceeds the file bound.");
416
+ let bytes = 0;
417
+ const files = new Set();
418
+ for (const file of entry.resources) {
419
+ if (!file ||
420
+ typeof file.uri !== "string" ||
421
+ !file.uri.startsWith(root) ||
422
+ files.has(file.uri) ||
423
+ typeof file.digest !== "string" ||
424
+ !/^sha256:[a-f0-9]{64}$/.test(file.digest) ||
425
+ !Number.isSafeInteger(file.size) ||
426
+ file.size < 0) {
427
+ throw new ConnectorCallError("unavailable", "Downstream skill manifest is invalid.");
428
+ }
429
+ bytes += file.size;
430
+ files.add(file.uri);
408
431
  }
409
- return listing;
432
+ if (!files.has(entry.uri) || bytes > MAX_SKILL_BYTES)
433
+ throw new ConnectorCallError("unavailable", "Downstream skill manifest is incomplete or oversized.");
434
+ }
435
+ async function withManifest(record) {
436
+ const bytes = encoder.encode(record.content);
437
+ if (bytes.length > MAX_SKILL_BYTES)
438
+ throw new ConnectorCallError("unavailable", "Local skill exceeds the byte bound.");
439
+ const digest = [...new Uint8Array(await crypto.subtle.digest("SHA-256", bytes))]
440
+ .map((byte) => byte.toString(16).padStart(2, "0"))
441
+ .join("");
442
+ return {
443
+ ...record,
444
+ entry: { ...record.entry, resources: [{ uri: record.entry.uri, digest: `sha256:${digest}`, size: bytes.length }] },
445
+ };
410
446
  }
411
447
  /**
412
- * Resolve one skill name. Built-in names match exactly; connector guides are
413
- * reachable only through the `connector:` prefix. Every miss — unknown name,
414
- * unknown connector, connector without a guide — is an explicit error, never a
415
- * silent fallback to the generic guide.
448
+ * One caller-view registry for native methods, the meta-tool and the guest API.
449
+ * No content reaches storage or operator sinks. Downstream skills use uncached
450
+ * list/read operations on the caller's request-scoped credential partition.
451
+ * Only a complete snapshot lives in this request; a failed build is discarded.
416
452
  */
417
- export function resolveSkill(name, connectors) {
418
- const builtIn = AVAILABLE_SKILLS.find((skill) => skill.name === name);
419
- if (builtIn) {
420
- return { found: true, content: builtIn.content(connectors) };
453
+ export class SkillsRegistry {
454
+ registry;
455
+ baseUrl;
456
+ options;
457
+ scope;
458
+ local;
459
+ snapshot;
460
+ constructor(registry, baseUrl, options = {}) {
461
+ this.registry = registry;
462
+ this.baseUrl = baseUrl;
463
+ this.options = options;
464
+ this.scope = options.requestScope ?? {};
465
+ this.local = localRecords(registry.listConnectors());
466
+ }
467
+ operation(connector, read) {
468
+ const timeoutMs = this.options.probeTimeoutMs ?? 20_000;
469
+ const operationScope = {};
470
+ sentSecretsForRequest(this.scope).include(sentSecretsForRequest(operationScope));
471
+ return runEdge(withDeadlineEffect((signal) => Effect.scoped(Effect.gen({ self: this }, function* () {
472
+ const ctx = this.registry.contextFor(connector.id, this.baseUrl, operationScope, {
473
+ signal,
474
+ timeoutMs,
475
+ ...(this.options.defer ? { defer: this.options.defer } : {}),
476
+ });
477
+ yield* closeScopeOnExit(connector, ctx, this.options.defer);
478
+ return yield* Effect.tryPromise({
479
+ try: () => read(ctx),
480
+ catch: (error) => error instanceof ConnectorCallError && error.code === "auth_required"
481
+ ? new ConnectorCallError("auth_required", "Downstream skills require authorization.")
482
+ : new ConnectorCallError("unavailable", "Downstream skills are unavailable."),
483
+ });
484
+ })), {
485
+ timeoutMs,
486
+ ...(this.options.requestSignal ? { signal: this.options.requestSignal } : {}),
487
+ timeoutError: new ConnectorCallError("unavailable", "Downstream skills timed out."),
488
+ }), { signal: this.options.requestSignal });
489
+ }
490
+ async records() {
491
+ if (!this.snapshot) {
492
+ this.snapshot = this.build();
493
+ this.snapshot.catch(() => {
494
+ this.snapshot = undefined;
495
+ });
496
+ }
497
+ const records = await this.snapshot;
498
+ this.assertSafe(records);
499
+ return records;
500
+ }
501
+ /** Later catalog/read operations can add credentials to this request. */
502
+ assertSafe(records) {
503
+ const secrets = sentSecretsForRequest(this.scope);
504
+ for (const record of records) {
505
+ const uris = [record.entry.uri, ...(record.files?.values() ?? [])];
506
+ if (record.entry.resources !== "dynamic") {
507
+ for (const file of record.entry.resources) {
508
+ uris.push(file.uri);
509
+ if (secrets.contains(file.digest))
510
+ throw new ConnectorCallError("unavailable", "Skill digest contains sent credentials.");
511
+ }
512
+ }
513
+ if (record.aliases.some((alias) => secrets.contains(alias)) ||
514
+ uris.some((uri) => secrets.containsUrl(uri) || secrets.contains(uri))) {
515
+ throw new ConnectorCallError("unavailable", "Skill URI contains sent credentials.");
516
+ }
517
+ }
421
518
  }
422
- const available = () => listSkills(connectors)
423
- .map((skill) => skill.name)
424
- .join(", ");
425
- if (name.startsWith(CONNECTOR_SKILL_PREFIX)) {
426
- const id = name.slice(CONNECTOR_SKILL_PREFIX.length);
427
- const connector = connectors.find((c) => c.id === id);
428
- if (!connector) {
429
- return {
430
- found: false,
431
- message: `Unknown connector "${boundedEchoText(id)}". Available skills: ${available()}.`,
432
- };
519
+ async build() {
520
+ const records = await Promise.all((await this.local).map(withManifest));
521
+ let catalogBytes = jsonBytes(records.map((record) => record.entry));
522
+ // Bounded sequential connector reads avoid unbounded fan-out and finish
523
+ // cleanup before a later connector's failure can end the whole listing.
524
+ for (const connector of this.registry.listConnectors()) {
525
+ if (!connector.downstreamSkills || !this.registry.canReadConnectorSkills(connector.id))
526
+ continue;
527
+ const entries = await this.operation(connector, (ctx) => connector.downstreamSkills.list(ctx));
528
+ if (!Array.isArray(entries) || records.length + entries.length > MAX_SKILLS)
529
+ throw new ConnectorCallError("unavailable", "Downstream skills listing exceeds its bound.");
530
+ for (const original of entries) {
531
+ if (catalogBytes + jsonBytes(original) + 1 > MAX_SKILL_CATALOG_BYTES)
532
+ throw new ConnectorCallError("unavailable", "Skills listing exceeds its byte bound.");
533
+ const entry = structuredClone(original);
534
+ validateEntry(entry);
535
+ const files = new Map();
536
+ const uri = downstreamSkillUri(connector.id, entry.uri);
537
+ const secrets = sentSecretsForRequest(this.scope);
538
+ if (secrets.containsUrl(entry.uri))
539
+ throw new ConnectorCallError("unavailable", "Downstream skill URI contains sent credentials.");
540
+ files.set(uri, entry.uri);
541
+ const resources = entry.resources === "dynamic"
542
+ ? "dynamic"
543
+ : entry.resources.map((file) => {
544
+ const uri = downstreamSkillUri(connector.id, file.uri);
545
+ if (secrets.containsUrl(file.uri))
546
+ throw new ConnectorCallError("unavailable", "Downstream skill URI contains sent credentials.");
547
+ files.set(uri, file.uri);
548
+ return { ...file, uri };
549
+ });
550
+ if (records.some((record) => record.entry.uri === uri))
551
+ throw new ConnectorCallError("unavailable", "Downstream skills listing contains duplicate entries.");
552
+ const mapped = { ...entry, uri, resources };
553
+ catalogBytes += jsonBytes(mapped) + 1;
554
+ if (catalogBytes > MAX_SKILL_CATALOG_BYTES)
555
+ throw new ConnectorCallError("unavailable", "Skills listing exceeds its byte bound.");
556
+ records.push({ entry: mapped, aliases: [], connector, files });
557
+ }
433
558
  }
434
- const guide = connectorGuide(connector);
435
- if (!guide) {
436
- return {
437
- found: false,
438
- message: `Connector "${boundedEchoText(id)}" has no usage guide. Available skills: ${available()}.`,
439
- };
559
+ if (records.length > MAX_SKILLS || jsonBytes(records.map((record) => record.entry)) > MAX_SKILL_CATALOG_BYTES) {
560
+ throw new ConnectorCallError("unavailable", "Skills listing exceeds its byte or entry bound.");
440
561
  }
441
- return { found: true, content: guide };
562
+ // Keep the old investigation guide behind every connector and downstream
563
+ // guide, where it cannot consume one of the first five import slots.
564
+ const investigate = records.findIndex((record) => record.aliases.includes("investigate"));
565
+ if (investigate >= 0)
566
+ records.push(records.splice(investigate, 1)[0]);
567
+ this.assertSafe(records);
568
+ return records;
569
+ }
570
+ async lookup(uri) {
571
+ const local = (await this.local).find((record) => record.entry.uri === uri || record.aliases.includes(uri));
572
+ if (local) {
573
+ const record = await withManifest(local);
574
+ this.assertSafe([record]);
575
+ return record;
576
+ }
577
+ // A non-skill URI never starts a downstream resource request or listing.
578
+ if (!uri.startsWith("skill://downstream/"))
579
+ throw this.missing(uri);
580
+ const record = (await this.records()).find((record) => record.entry.uri === uri);
581
+ if (!record)
582
+ throw this.missing(uri);
583
+ return record;
584
+ }
585
+ missing(name) {
586
+ const available = [
587
+ "usage",
588
+ ...this.registry
589
+ .listConnectors()
590
+ .filter((connector) => connectorGuide(connector) !== undefined)
591
+ .map((connector) => connectorSkillName(connector.id)),
592
+ "investigate",
593
+ ].join(", ");
594
+ // Caller-authored URI/error text never enters operator records. Preserve
595
+ // legacy guidance without allowing a missing name to probe a hidden view.
596
+ const id = name.startsWith(CONNECTOR_SKILL_PREFIX) ? name.slice(CONNECTOR_SKILL_PREFIX.length) : name;
597
+ const connector = this.registry.getConnector(id);
598
+ const message = name.startsWith(CONNECTOR_SKILL_PREFIX)
599
+ ? connector
600
+ ? `Connector "${boundedEchoText(id)}" has no usage guide.`
601
+ : `Unknown connector "${boundedEchoText(id)}".`
602
+ : connector
603
+ ? `Unknown skill "${boundedEchoText(name)}". Connector guides are fetched as "${boundedEchoText(connectorSkillName(name))}".`
604
+ : `Unknown skill "${boundedEchoText(name)}".`;
605
+ return new ConnectorCallError("not_found", `${message} Available skills: ${available}.`);
442
606
  }
443
- const bare = connectors.find((c) => c.id === name);
444
- if (bare) {
607
+ async list(cursor) {
608
+ const records = await this.records();
609
+ const offset = cursor === undefined ? 0 : /^skills:[1-9][0-9]*$/.test(cursor) ? Number(cursor.slice(7)) : NaN;
610
+ if (!Number.isSafeInteger(offset) || offset % SKILL_PAGE_SIZE !== 0 || offset >= records.length)
611
+ throw new ConnectorCallError("invalid_args", "Invalid skills cursor.");
445
612
  return {
446
- found: false,
447
- message: connectorGuide(bare)
448
- ? `Unknown skill "${boundedEchoText(name)}". Connector guides are fetched as "${boundedEchoText(connectorSkillName(name))}". Available skills: ${available()}.`
449
- : `Connector "${boundedEchoText(name)}" has no usage guide. Available skills: ${available()}.`,
613
+ ...PRIVATE,
614
+ skills: records.slice(offset, offset + SKILL_PAGE_SIZE).map((record) => record.entry),
615
+ ...(offset + SKILL_PAGE_SIZE < records.length ? { nextCursor: `skills:${offset + SKILL_PAGE_SIZE}` } : {}),
450
616
  };
451
617
  }
452
- return {
453
- found: false,
454
- message: `Unknown skill "${boundedEchoText(name)}". Available skills: ${available()}.`,
455
- };
618
+ async get(uri) {
619
+ return { ...PRIVATE, skill: (await this.lookup(uri)).entry };
620
+ }
621
+ async resources(cursor) {
622
+ const page = await this.list(cursor);
623
+ return {
624
+ ...PRIVATE,
625
+ resources: page.skills.flatMap((skill) => (skill.resources === "dynamic" ? [{ uri: skill.uri }] : skill.resources).map((file) => ({
626
+ uri: file.uri,
627
+ name: file.uri === skill.uri ? String(skill.frontmatter.name) : file.uri.slice(file.uri.lastIndexOf("/") + 1),
628
+ ...(file.uri === skill.uri
629
+ ? { description: String(skill.frontmatter.description), mimeType: "text/markdown" }
630
+ : {}),
631
+ ...("size" in file ? { size: file.size } : {}),
632
+ }))),
633
+ ...(page.nextCursor ? { nextCursor: page.nextCursor } : {}),
634
+ };
635
+ }
636
+ async read(uri) {
637
+ const local = (await this.local).find((record) => record.entry.uri === uri || record.aliases.includes(uri));
638
+ if (local) {
639
+ const record = await withManifest(local);
640
+ this.assertSafe([record]);
641
+ return { ...PRIVATE, contents: [{ uri: record.entry.uri, mimeType: "text/markdown", text: record.content }] };
642
+ }
643
+ if (!uri.startsWith("skill://downstream/"))
644
+ throw this.missing(uri);
645
+ const record = (await this.records()).find((record) => record.files?.has(uri));
646
+ if (!record?.connector || !record.files || !this.registry.canReadConnectorSkills(record.connector.id))
647
+ throw this.missing(uri);
648
+ const originalUri = record.files.get(uri);
649
+ const contents = await this.operation(record.connector, (ctx) => record.connector.downstreamSkills.read(originalUri, ctx));
650
+ // Structural fields and preserved digests cannot be repaired by the
651
+ // output boundary after this read has sent another credential.
652
+ await this.records();
653
+ if (!Array.isArray(contents) || contents.length !== 1 || contents[0]?.uri !== originalUri)
654
+ throw new ConnectorCallError("unavailable", "Downstream skill read returned unexpected files.");
655
+ const content = contents[0];
656
+ if (!content || (typeof content.text === "string") === (typeof content.blob === "string"))
657
+ throw new ConnectorCallError("unavailable", "Downstream skill read did not return one file.");
658
+ let bytes;
659
+ try {
660
+ bytes = typeof content.text === "string" ? encoder.encode(content.text).length : atob(content.blob).length;
661
+ }
662
+ catch {
663
+ throw new ConnectorCallError("unavailable", "Downstream skill file is not valid base64.");
664
+ }
665
+ if (bytes > MAX_SKILL_BYTES)
666
+ throw new ConnectorCallError("unavailable", "Downstream skill file exceeds the byte bound.");
667
+ return { ...PRIVATE, contents: [{ ...content, uri }] };
668
+ }
669
+ async text(name) {
670
+ const record = await this.lookup(name);
671
+ const content = (await this.read(record.entry.uri)).contents[0];
672
+ if (typeof content.text === "string")
673
+ return content.text;
674
+ try {
675
+ return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(Uint8Array.from(atob(content.blob), (char) => char.charCodeAt(0)));
676
+ }
677
+ catch {
678
+ throw new ConnectorCallError("unavailable", "Skill instructions are not UTF-8 text.");
679
+ }
680
+ }
681
+ async summaries() {
682
+ return (await this.records()).map((record) => ({
683
+ name: record.aliases[0] ?? record.entry.uri,
684
+ uri: record.entry.uri,
685
+ description: String(record.entry.frontmatter.description),
686
+ }));
687
+ }
456
688
  }