@zackbart/connecta 0.40.0 → 0.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/CHANGELOG.md +148 -0
  2. package/bin/connecta.mjs +26 -17
  3. package/bin/meta-tool-names.d.mts +3 -1
  4. package/bin/meta-tool-names.mjs +4 -1
  5. package/bin/version.mjs +1 -1
  6. package/dist/activity.d.ts +32 -5
  7. package/dist/activity.js +32 -6
  8. package/dist/advertised-schema.js +15 -0
  9. package/dist/agent-observation.js +24 -2
  10. package/dist/agent-routes.js +33 -44
  11. package/dist/auth/clerk.d.ts +6 -1
  12. package/dist/auth/clerk.js +16 -0
  13. package/dist/auth/cloudflare-access.js +49 -7
  14. package/dist/auth/downstream-oauth/consent.js +79 -29
  15. package/dist/auth/downstream-oauth/grant-store.js +58 -7
  16. package/dist/auth/downstream-oauth/refresh-coordinator.js +4 -1
  17. package/dist/auth/downstream-oauth/refresh-records.js +6 -5
  18. package/dist/auth/downstream-oauth.js +15 -4
  19. package/dist/auth/oauth-request-not-sent.js +4 -0
  20. package/dist/auth/static-oauth.js +77 -39
  21. package/dist/auth-elicitation.js +118 -61
  22. package/dist/authorization-handoff.js +36 -3
  23. package/dist/catalog-cache.js +352 -43
  24. package/dist/catalog-freshness.js +25 -0
  25. package/dist/catalog-service.js +408 -78
  26. package/dist/catalog.js +216 -62
  27. package/dist/config-defaults.d.ts +2 -1
  28. package/dist/config-defaults.js +4 -1
  29. package/dist/config.d.ts +53 -21
  30. package/dist/config.js +56 -17
  31. package/dist/connectors/remote-mcp/call.js +1 -1
  32. package/dist/connectors/remote-mcp/catalog.js +52 -28
  33. package/dist/connectors/remote-mcp/connection.js +113 -22
  34. package/dist/connectors/remote-mcp/oauth.js +2 -2
  35. package/dist/connectors/remote-mcp/resources.js +5 -2
  36. package/dist/connectors/remote-mcp/transport.js +24 -6
  37. package/dist/connectors/remote-mcp.js +14 -5
  38. package/dist/credential-contract.d.ts +3 -1
  39. package/dist/describe-config.d.ts +4 -2
  40. package/dist/describe-config.js +1 -0
  41. package/dist/discovery-health.js +26 -11
  42. package/dist/downstream-input.js +11 -2
  43. package/dist/errors.d.ts +60 -4
  44. package/dist/errors.js +88 -3
  45. package/dist/execute.js +491 -202
  46. package/dist/executor-admission.js +2 -0
  47. package/dist/executor-contract.js +6 -0
  48. package/dist/executor-result.js +3 -3
  49. package/dist/executors/quickjs-child.js +14 -5
  50. package/dist/executors/quickjs-runtime.d.ts +2 -0
  51. package/dist/executors/quickjs-runtime.js +37 -20
  52. package/dist/executors/quickjs.js +24 -7
  53. package/dist/guarded-transport.js +2 -0
  54. package/dist/guest-functions.js +5 -4
  55. package/dist/guest-runtime.js +37 -1
  56. package/dist/guest-types.d.ts +18 -0
  57. package/dist/http/client.js +14 -2
  58. package/dist/http/errors.js +9 -4
  59. package/dist/http/fields.js +17 -5
  60. package/dist/http/hatch.js +11 -3
  61. package/dist/http/node-transport.js +110 -4
  62. package/dist/http/oauth.js +17 -6
  63. package/dist/http/paging.js +1 -1
  64. package/dist/http/provider.js +2 -6
  65. package/dist/http/vendor.js +6 -1
  66. package/dist/index.d.ts +1 -1
  67. package/dist/index.js +15 -21
  68. package/dist/invocation-failure.js +28 -0
  69. package/dist/invocation.js +167 -64
  70. package/dist/membership.d.ts +1 -1
  71. package/dist/meta-output.js +24 -18
  72. package/dist/meta-tool-names.d.ts +1 -1
  73. package/dist/meta-tool-names.js +1 -1
  74. package/dist/meta-tools.js +286 -73
  75. package/dist/module-contracts.d.ts +3 -1
  76. package/dist/native-recovery.js +4 -1
  77. package/dist/native-tools.js +177 -55
  78. package/dist/node.d.ts +7 -0
  79. package/dist/node.js +85 -4
  80. package/dist/oauth-handoff.js +53 -43
  81. package/dist/operation-activity.js +104 -0
  82. package/dist/operation-vocabulary.js +6 -0
  83. package/dist/operator-record.d.ts +9 -1
  84. package/dist/operator-record.js +16 -0
  85. package/dist/operator-ui/generated.js +2 -2
  86. package/dist/program-result.js +2 -0
  87. package/dist/program-source.js +1 -1
  88. package/dist/program-writes.js +112 -19
  89. package/dist/provider.d.ts +1 -14
  90. package/dist/provider.js +0 -3
  91. package/dist/providers/_shared/google/kit.js +2 -2
  92. package/dist/providers/_shared/google/workspace.d.ts +1 -1
  93. package/dist/providers/breeze/api.js +2 -2
  94. package/dist/providers/breeze/index.js +1 -1
  95. package/dist/providers/breeze/skill.generated.js +1 -1
  96. package/dist/providers/ccb/api.js +2 -2
  97. package/dist/providers/ccb/index.js +1 -1
  98. package/dist/providers/ccb/skill.generated.js +1 -1
  99. package/dist/providers/cloudflare/api.js +2 -2
  100. package/dist/providers/cloudflare/index.js +3 -1
  101. package/dist/providers/cloudflare/skill.generated.js +1 -1
  102. package/dist/providers/docs/index.js +1 -1
  103. package/dist/providers/docs/skill.generated.js +1 -1
  104. package/dist/providers/drive/index.js +1 -1
  105. package/dist/providers/drive/skill.generated.js +1 -1
  106. package/dist/providers/forms/index.js +2 -1
  107. package/dist/providers/forms/skill.generated.js +1 -1
  108. package/dist/providers/github/api.js +98 -6
  109. package/dist/providers/github/index.js +8 -3
  110. package/dist/providers/github/lib/auth.d.ts +3 -1
  111. package/dist/providers/github/lib/auth.js +43 -12
  112. package/dist/providers/github/lib/discovery.js +11 -1
  113. package/dist/providers/github/lib/operations.js +8 -0
  114. package/dist/providers/github/safety.js +39 -0
  115. package/dist/providers/github/skill.generated.js +1 -1
  116. package/dist/providers/github/tools.js +12 -5
  117. package/dist/providers/gmail/index.js +2 -1
  118. package/dist/providers/gmail/skill.generated.js +1 -1
  119. package/dist/providers/granola/index.js +1 -1
  120. package/dist/providers/granola/skill.generated.js +1 -1
  121. package/dist/providers/overflow/index.js +3 -1
  122. package/dist/providers/overflow/skill.generated.js +1 -1
  123. package/dist/providers/planning-center/index.js +12 -6
  124. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  125. package/dist/providers/planning-center/skill.generated.js +1 -1
  126. package/dist/providers/quickbooks/api.js +28 -25
  127. package/dist/providers/quickbooks/index.js +3 -6
  128. package/dist/providers/quickbooks/lib/writes.js +55 -0
  129. package/dist/providers/quickbooks/safety.js +17 -5
  130. package/dist/providers/quickbooks/skill.generated.js +1 -1
  131. package/dist/providers/sheets/index.js +2 -1
  132. package/dist/providers/sheets/skill.generated.js +1 -1
  133. package/dist/providers/slides/index.js +3 -3
  134. package/dist/providers/slides/skill.generated.js +1 -1
  135. package/dist/providers/stripe/index.js +2 -2
  136. package/dist/providers/stripe/skill.generated.js +1 -1
  137. package/dist/providers/tithely/index.js +2 -1
  138. package/dist/providers/tithely/skill.generated.js +1 -1
  139. package/dist/providers/vercel/api.js +3 -2
  140. package/dist/providers/vercel/skill.generated.js +1 -1
  141. package/dist/registry.d.ts +36 -4
  142. package/dist/registry.js +142 -30
  143. package/dist/resource-read.js +21 -2
  144. package/dist/result-preview.js +210 -0
  145. package/dist/result-seal.js +168 -0
  146. package/dist/route-names.js +53 -0
  147. package/dist/routes/activity-context.js +1 -0
  148. package/dist/routes/activity-disclosure.js +26 -4
  149. package/dist/routes/activity.js +2 -1
  150. package/dist/routes/connect.js +21 -13
  151. package/dist/routes/mcp.js +83 -35
  152. package/dist/routes/shared.d.ts +6 -0
  153. package/dist/routes/ui-config.js +1 -1
  154. package/dist/routes/ui-data.js +3 -2
  155. package/dist/runtime/call.js +15 -10
  156. package/dist/runtime/native-storage.js +12 -0
  157. package/dist/runtime/request-reads.js +66 -14
  158. package/dist/runtime/request-scope.js +38 -2
  159. package/dist/runtime/run.js +4 -1
  160. package/dist/sent-secrets.d.ts +20 -0
  161. package/dist/sent-secrets.js +49 -1
  162. package/dist/skill-origin.js +16 -0
  163. package/dist/skills.js +213 -57
  164. package/dist/storage/d1-driver.js +33 -5
  165. package/dist/storage/keys.js +16 -7
  166. package/dist/storage/sql-native.js +60 -3
  167. package/dist/storage/sql.js +6 -0
  168. package/dist/timing-trace.js +84 -0
  169. package/dist/tool-safety.d.ts +33 -0
  170. package/dist/tool-safety.js +51 -15
  171. package/dist/types.d.ts +36 -0
  172. package/dist/url-safety.js +38 -20
  173. package/dist/usage-guide.js +14 -13
  174. package/dist/utility-recovery.js +33 -0
  175. package/dist/validate.js +29 -10
  176. package/dist/version.d.ts +1 -1
  177. package/dist/version.js +1 -1
  178. package/dist/worker.js +60 -15
  179. package/documentation/architecture.md +152 -43
  180. package/documentation/auth.md +160 -53
  181. package/documentation/code-mode.md +396 -119
  182. package/documentation/deploying.md +35 -10
  183. package/documentation/hosted-mcp-migration.md +9 -3
  184. package/documentation/integrating.md +27 -5
  185. package/documentation/meta-tools.md +761 -157
  186. package/documentation/operating.md +91 -4
  187. package/documentation/operator-ui.md +51 -2
  188. package/documentation/providers.md +34 -20
  189. package/examples/worker/README.md +3 -3
  190. package/guides/how-it-works.md +2 -1
  191. package/guides/safety-and-trust.md +1 -3
  192. package/package.json +10 -8
  193. package/templates/node/AGENTS.md +3 -3
  194. package/templates/node/README.md +1 -1
  195. package/templates/node/package.json +1 -1
@@ -2,6 +2,9 @@ import type { ConnectorContext, JsonSchema, Logger } from "./types.js";
2
2
  /** One payload-free warning per configured connector, regardless of requests. */
3
3
  export declare function shortSecretWarning(): (value: string | undefined, logger: Logger) => void;
4
4
  export declare class SentSecrets {
5
+ /** Rises whenever this set could match more. Text checked at one revision has not been
6
+ * checked against a later one. */
7
+ revision: number;
5
8
  /** Normalized form to its strongest kind. */
6
9
  private readonly forms;
7
10
  /** A form's leading units that encode only public auth framing; absent is none. */
@@ -44,6 +47,10 @@ export declare class SentSecrets {
44
47
  contains(value: string, ignoreCase?: boolean): boolean;
45
48
  containsUrl(value: string): boolean;
46
49
  header(value: string): void;
50
+ /** A header maintained provider code declares a public identifier: matched whole only. A name
51
+ * alone never establishes this. Whitespace means an auth scheme or other framing, so that value
52
+ * is a credential whatever its header, and the answer is false. */
53
+ publicIdentifierHeader(value: string): boolean;
47
54
  request(input: RequestInfo | URL, init?: RequestInit): void;
48
55
  /** Serialized wire text about to be returned, stored, or paged, under payload rules. */
49
56
  text(value: string): string;
@@ -95,6 +102,13 @@ export declare class SentSecrets {
95
102
  /** Credentials alone: later calls keep the private arguments they submit, and cached
96
103
  * catalogs never depend on one request's arguments. */
97
104
  redactCredentials<T>(value: T): T;
105
+ /** A program's outbound arguments, with what the {@link outbound} check matched replaced,
106
+ * and whether it matched anything: a write that did is refused rather than sent altered.
107
+ * Output keeps the stricter matching; a program never holds what output withheld. */
108
+ filterCredentials<T>(value: T): {
109
+ value: T;
110
+ changed: boolean;
111
+ };
98
112
  private idle;
99
113
  private scan;
100
114
  private matcher;
@@ -104,6 +118,12 @@ export declare class SentSecrets {
104
118
  private wholeMatcher;
105
119
  private fragments;
106
120
  private credentials;
121
+ /** Credentials as an outbound argument can hold them: a whole form, or eight units of what
122
+ * follows its public framing and label. Identifiers and submitted private values are not
123
+ * here. Telling a secret from a label by shape has a limit: fewer than eight units past a
124
+ * label pass, and a public value sharing eight units of the rest, such as a publishable key
125
+ * beside its secret key, still matches. */
126
+ private outbound;
107
127
  /** Every form lowercased, for fields such as hosts that reach output lowercased. */
108
128
  private folded;
109
129
  /** The shortest text that might hold something this set cannot check, which is withheld: a
@@ -24,6 +24,11 @@ const MIN_SECRET_LENGTH = 8;
24
24
  // units is fragment-indexed. Eight units keeps ordinary words, dates, counts and auth framing out
25
25
  // of the index: a public run must repeat eight consecutive units of a secret's form to match.
26
26
  const FRAGMENT_LENGTH = MIN_SECRET_LENGTH;
27
+ // A credential's public label, for the outbound argument check alone: a vendor prefix of up to
28
+ // three short segments (`sk_test_`, `github_pat_`, `xoxb-`) or a JWT's header segment. Documents
29
+ // name these and every credential of a kind shares them. Reading a secret's opening as a label
30
+ // only stops runs starting inside it from matching; the rest of the value still matches.
31
+ const PUBLIC_LABEL = /^(?:[Ee]yJ[\w-]*\.|(?:[A-Za-z][A-Za-z0-9]{1,6}[-_]){1,3})/;
27
32
  // A private value shorter than this is matched only as a whole field value: a leaf, key, number,
28
33
  // boolean or decoded view equal to one of its forms. One to three units recur inside ordinary
29
34
  // words, counts and ids, so a substring match would rewrite them everywhere.
@@ -115,7 +120,7 @@ const TAGS = new Set(("type:text type:image type:audio type:resource type:resour
115
120
  "resultType:input_required mode:form mode:url method:elicitation/create jsonrpc:2.0 writeOutcome:unknown").split(" "));
116
121
  // Outcome flags: a boolean under these names is kept, since a boolean carries one bit whatever
117
122
  // produced it. A number or string under them is matched like any other.
118
- const FACTS = new Set(["isError", "ok", "retryable", "truncated", "hasMore"]);
123
+ const FACTS = new Set(["isError", "ok", "retryable", "truncated", "hasMore", "toolAnswered"]);
119
124
  // An error's diagnostics. Its own accessors under these names, such as V8's stack, are read once
120
125
  // into its copy, as is whatever its chain answers for them; any other own accessor is never
121
126
  // invoked. Accessors its class chain defines are read once too.
@@ -1096,6 +1101,9 @@ function windows(layer, reach) {
1096
1101
  return ranges;
1097
1102
  }
1098
1103
  export class SentSecrets {
1104
+ /** Rises whenever this set could match more. Text checked at one revision has not been
1105
+ * checked against a later one. */
1106
+ revision = 0;
1099
1107
  /** Normalized form to its strongest kind. */
1100
1108
  forms = new Map();
1101
1109
  /** A form's leading units that encode only public auth framing; absent is none. */
@@ -1132,6 +1140,7 @@ export class SentSecrets {
1132
1140
  this.widestForm = Math.max(this.widestForm, form.length);
1133
1141
  this.smallForms ||= kind < PRIVATE;
1134
1142
  this.unicodeForms ||= /[\u0080-\uffff]/.test(form);
1143
+ this.revision++;
1135
1144
  this.views = {};
1136
1145
  this.memo = new Map();
1137
1146
  for (const recipient of this.recipients)
@@ -1141,6 +1150,7 @@ export class SentSecrets {
1141
1150
  if (length >= this.unmatched)
1142
1151
  return;
1143
1152
  this.unmatched = length;
1153
+ this.revision++;
1144
1154
  this.views = {};
1145
1155
  this.memo = new Map();
1146
1156
  for (const recipient of this.recipients)
@@ -1278,6 +1288,17 @@ export class SentSecrets {
1278
1288
  else
1279
1289
  this.secret(framed[1]);
1280
1290
  }
1291
+ /** A header maintained provider code declares a public identifier: matched whole only. A name
1292
+ * alone never establishes this. Whitespace means an auth scheme or other framing, so that value
1293
+ * is a credential whatever its header, and the answer is false. */
1294
+ publicIdentifierHeader(value) {
1295
+ if (/\s/.test(value)) {
1296
+ this.header(value);
1297
+ return false;
1298
+ }
1299
+ this.identifier(value);
1300
+ return true;
1301
+ }
1281
1302
  request(input, init) {
1282
1303
  const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));
1283
1304
  for (const [name, value] of headers) {
@@ -1412,6 +1433,16 @@ export class SentSecrets {
1412
1433
  redactCredentials(value) {
1413
1434
  return this.credentials().redact(value, "envelope");
1414
1435
  }
1436
+ /** A program's outbound arguments, with what the {@link outbound} check matched replaced,
1437
+ * and whether it matched anything: a write that did is refused rather than sent altered.
1438
+ * Output keeps the stricter matching; a program never holds what output withheld. */
1439
+ filterCredentials(value) {
1440
+ const outbound = this.outbound();
1441
+ if (outbound.idle())
1442
+ return { value, changed: false };
1443
+ const filtered = outbound.project(value, outbound.scan(false));
1444
+ return { value: filtered.value, changed: filtered.changed };
1445
+ }
1415
1446
  idle() {
1416
1447
  return !this.forms.size && this.unmatched === Infinity;
1417
1448
  }
@@ -1453,6 +1484,23 @@ export class SentSecrets {
1453
1484
  }
1454
1485
  return this.views.credentials;
1455
1486
  }
1487
+ /** Credentials as an outbound argument can hold them: a whole form, or eight units of what
1488
+ * follows its public framing and label. Identifiers and submitted private values are not
1489
+ * here. Telling a secret from a label by shape has a limit: fewer than eight units past a
1490
+ * label pass, and a public value sharing eight units of the rest, such as a publishable key
1491
+ * beside its secret key, still matches. */
1492
+ outbound() {
1493
+ if (!this.views.outbound) {
1494
+ const view = new SentSecrets();
1495
+ for (const [form, kind] of this.forms) {
1496
+ const frame = this.frames.get(form) ?? 0;
1497
+ if (kind === CREDENTIAL && frame !== Infinity)
1498
+ view.store(form, kind, frame + (PUBLIC_LABEL.exec(form.slice(frame))?.[0].length ?? 0));
1499
+ }
1500
+ this.views.outbound = view;
1501
+ }
1502
+ return this.views.outbound;
1503
+ }
1456
1504
  /** Every form lowercased, for fields such as hosts that reach output lowercased. */
1457
1505
  folded() {
1458
1506
  if (!this.views.folded) {
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The fixed sentence that marks downstream guidance as third-party. A prefix has
3
+ * no closing delimiter to forge, and connector IDs are deployment-authored.
4
+ */
5
+ export function downstreamOrigin(connectorId) {
6
+ return `Third-party guidance from connector ${JSON.stringify(connectorId.slice(0, 128))}.`;
7
+ }
8
+ /** Downstream description text on one line, so it cannot begin a new listing entry. */
9
+ export function labeledDownstreamDescription(connectorId, description) {
10
+ return `${downstreamOrigin(connectorId)} ${description.replace(/\s+/g, " ").trim()}`;
11
+ }
12
+ const SKILL_RESOURCE = /^skill:\/\/|\/SKILL\.md(?:[?#]|$)/i;
13
+ /** Whether a downstream resource URI addresses skill instructions rather than ordinary data. */
14
+ export function isSkillResource(uri) {
15
+ return SKILL_RESOURCE.test(uri);
16
+ }
package/dist/skills.js CHANGED
@@ -1,41 +1,108 @@
1
1
  import { META_ROUTES, routeNames } from "./agent-routes.js";
2
+ import { READ_PROGRAM_SOURCE } from "./tool-safety.js";
2
3
  import { bytesToHex, utf8Length } from "./bytes.js";
3
4
  import { duringViewEffect } from "./runtime/call.js";
4
5
  import { RequestScope, requestOwner } from "./runtime/request-scope.js";
5
6
  import { upwardStep } from "./upward.js";
6
7
  import { authCall, authInvoke, authFlow, authMethod, registerAuth, registerAuthMethod, authOwnMethod, } from "./auth/native.js";
7
8
  import { USAGE_SKILL } from "./usage-guide.js";
9
+ import { downstreamOrigin, labeledDownstreamDescription } from "./skill-origin.js";
8
10
  import { boundedEchoText, ConnectorCallError } from "./errors.js";
9
11
  import { Effect } from "effect";
10
12
  import { runEdge, withDeadlineEffect } from "./runtime/run.js";
11
13
  import { closeScopeOnExit } from "./runtime/connector-scope.js";
12
14
  import { sentSecretsForRequest } from "./sent-secrets.js";
15
+ /** Trusted diagnostics constructed from the caller-visible local registry. */
16
+ export class SkillLookupError extends ConnectorCallError {
17
+ }
13
18
  // The meta instructions and built-in guides use the meta names: built-in guides
14
19
  // are byte-identical across deployments, and the eval code arm replaces usage.
15
- const ROUTE = `Choose a route before discovery. One known-address read uses ${META_ROUTES.read}; one known-address write uses ${META_ROUTES.write}. Unknown-address read-only work starts with ${META_ROUTES.program} 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.`;
16
- const RECOVERY = `When an auth error includes a handoff, give it to the user or operator. Use ${META_ROUTES.authorize} for a fresh link. Page resultId notices and format:"paged" handles with connecta.result(handleOrId, { page? }). Missing guidance/repair only: skills tool "usage" or connecta.skill("usage").`;
17
- /**
18
- * The always-loaded MCP `instructions` string. An endpoint is writable or
19
- * read-only: a writable one lets programs and call_destructive_tool write,
20
- * and the client owns approval; a read-only one omits write tools from search
21
- * and describe and refuses every write call. Guides may still name them.
22
- */
23
- export const CONNECTA_INSTRUCTIONS = `${ROUTE} On a writable endpoint, programs and ${META_ROUTES.write} may write. On a read-only endpoint, search omits write tools and every write call is refused; guides may still name them. The ${META_ROUTES.program} description says which this endpoint is. Never repeat a write to recover its output. ${RECOVERY}`;
20
+ const WRITE_POLICY = "This endpoint permits writes, subject to connector restrictions; the host owns approval. Never resend a write to see its output.";
21
+ const READ_ONLY_POLICY = "This endpoint can't perform writes; no write route exists here.";
22
+ const RECOVERY = "On errors and truncated results, follow nextAction; give auth links to the user or operator.";
23
+ /** A guide can name a write step, so its notice says what happens to one. */
24
+ const READ_ONLY_GUIDE_NOTICE = "This endpoint can't perform writes; guide steps that change data will be refused. No write route exists on this endpoint.";
25
+ const policy = (readOnly) => (readOnly ? READ_ONLY_POLICY : WRITE_POLICY);
26
+ /** Connector ids in instructions; execute_code's inventory carries the rest. */
27
+ const INSTRUCTION_CONNECTORS_MAX_BYTES = 120;
28
+ /** Ids in roster order, then an exact count once they pass the byte cap. Ids are ASCII. */
29
+ function connectorList(ids) {
30
+ if (ids.length === 0)
31
+ return "none";
32
+ const all = ids.join(", ");
33
+ if (all.length <= INSTRUCTION_CONNECTORS_MAX_BYTES)
34
+ return all;
35
+ for (let shown = ids.length - 1; shown > 0; shown--) {
36
+ const line = [...ids.slice(0, shown), `+${ids.length - shown} more`].join(", ");
37
+ if (line.length <= INSTRUCTION_CONNECTORS_MAX_BYTES)
38
+ return line;
39
+ }
40
+ return `+${ids.length} more`;
41
+ }
24
42
  /**
25
- * The instructions one endpoint serves: {@link CONNECTA_INSTRUCTIONS}, then
26
- * the admitted pool's `description` after a fixed lead-in. Plain `/mcp` and a
27
- * pool without a description serve the constant unchanged. The description
28
- * comes from deployment config for the pool the request was admitted through;
29
- * nothing a request sends reaches it.
43
+ * The instructions one endpoint serves: pool scope, what the endpoint reaches,
44
+ * routing, write policy, recovery. The description comes from deployment config
45
+ * for the pool the request was admitted through. `connectorIds` is that
46
+ * request's admitted roster, the same one execute_code's inventory discloses:
47
+ * configured ids only, in registry order, with no catalog, call or auth state.
48
+ * A native pool omits them because its tool names already show them.
30
49
  */
31
- export function endpointInstructions(description, native) {
32
- const instructions = native ? nativeInstructions(native.executeCode) : CONNECTA_INSTRUCTIONS;
33
- return description === undefined ? instructions : `${instructions} This endpoint: ${description}`;
50
+ export function endpointInstructions(description, native, readOnly, connectorIds, readRoute = false) {
51
+ const gateway = `Gateway to connected services: ${connectorList(connectorIds)}. One known read: ${META_ROUTES.read}. ${readOnly ? "" : `One known write: ${META_ROUTES.write}. `}`;
52
+ const instructions = native
53
+ ? nativeInstructions(native.executeCode, readOnly)
54
+ : readRoute
55
+ ? `${gateway}Other read work: one ${READ_PROGRAM_SOURCE} program that searches, calls and returns the answer, with no write approval. ${readOnly ? "" : `Programs that write: ${META_ROUTES.program}. ${META_ROUTES.search} finds a write's address. `}${policy(readOnly)} ${RECOVERY}`
56
+ : `${gateway}Everything else: one ${META_ROUTES.program} program that searches, calls and returns the answer. ${policy(readOnly)} ${RECOVERY}`;
57
+ return description === undefined ? instructions : `${description} ${instructions}`;
34
58
  }
35
59
  /** Instructions for pools whose host searches the listed native tools. */
36
- export function nativeInstructions(executeCode) {
60
+ export function nativeInstructions(executeCode, readOnly = false) {
37
61
  const routes = routeNames(executeCode ? "native-code" : "native");
38
- return `Each tool here is one operation; annotations classify reads/writes. Call known tools directly; ${executeCode ? `combine work in ${routes.program} using the program addresses in tool descriptions.` : "find tools with your host's search."} Read-only endpoints omit and refuse writes. Never repeat a write for output. Give auth handoffs to the user/operator. Tools missing? Call authorize_connector; after repair, reconnect to refresh your list. Follow nextAction to page with ${executeCode ? `connecta.result in ${routes.program}` : "read_result"}. ${routes.skills} lists guidance.`;
62
+ return `Call known tools directly; ${executeCode ? `combine several in ${routes.program} using each tool's Program address.` : "find others with your host's search."} ${policy(readOnly)} ${RECOVERY} Tools missing: ask the operator to repair, then reconnect.`;
63
+ }
64
+ /** The `capabilities.extensions` key carrying {@link endpointExtension}. */
65
+ export const ENDPOINT_EXTENSION = "dev.connecta/endpoint";
66
+ const inventoryWords = (text) => text
67
+ .toLowerCase()
68
+ .split(/[^\p{L}\p{N}]+/u)
69
+ .filter(Boolean);
70
+ /** Text that only respells what its entry already shows ("Google Calendar" for google_calendar) adds nothing. */
71
+ export function addsInformation(shown, text) {
72
+ const words = inventoryWords(text);
73
+ const shownWords = inventoryWords(shown);
74
+ return words.join("") !== shownWords.join("") && words.some((word) => !shownWords.includes(word));
75
+ }
76
+ /** Clips on a whole token, so text with no token that fits leaves nothing to show. */
77
+ export const inventoryNote = (text, maxBytes) => {
78
+ const clipped = boundedEchoText(text, maxBytes);
79
+ return clipped === "…" ? undefined : clipped;
80
+ };
81
+ /** A connector's bounded title for inventories, or undefined when it adds nothing to the id or cannot fit. */
82
+ export function connectorInventoryTitle(connector) {
83
+ const title = connector.title?.replace(/\s+/g, " ").trim();
84
+ return title && addsInformation(connector.id, title) ? inventoryNote(title, 45) : undefined;
85
+ }
86
+ /**
87
+ * Facts about the endpoint this request reached, for a client composing its own
88
+ * host-specific guidance. Built only from the admitted scope and configuration:
89
+ * `connectors` is the caller's registry view, the roster `execute_code` and
90
+ * `authorize_connector` already describe, and nothing here loads a catalog.
91
+ */
92
+ export function endpointExtension(connectors, native, executeCode, readOnly) {
93
+ return {
94
+ surface: native ? "native" : "meta",
95
+ executeCode,
96
+ readOnly,
97
+ connectors: connectors.map((connector) => {
98
+ const title = connectorInventoryTitle(connector);
99
+ return {
100
+ id: connector.id,
101
+ ...(title ? { title } : {}),
102
+ ...(connectorGuideRequired(connector) ? { readFirst: connectorSkillName(connector.id) } : {}),
103
+ };
104
+ }),
105
+ };
39
106
  }
40
107
  /** True when at least one of `connectors` carries a usage guide. */
41
108
  export function hasConnectorGuides(connectors) {
@@ -97,14 +164,19 @@ const CONNECTOR_SKILL_PREFIX = "connector:";
97
164
  export function connectorSkillName(connectorId) {
98
165
  return `${CONNECTOR_SKILL_PREFIX}${connectorId}`;
99
166
  }
100
- /** The connector's guide, or undefined when it declares none (or a blank one). */
101
- export function connectorGuide(connector) {
167
+ /** The guide as authored, or undefined when the connector declares none (or a blank one). */
168
+ function authoredGuide(connector) {
102
169
  const guide = connector.usageGuide;
103
170
  const content = typeof guide === "string" ? guide : guide?.content;
104
- if (!content || content.trim() === "")
171
+ return !content || content.trim() === "" ? undefined : content;
172
+ }
173
+ /** The connector's guide with its connection's read-only notice, or undefined when it has none. */
174
+ export function connectorGuide(connector) {
175
+ const content = authoredGuide(connector);
176
+ if (content === undefined)
105
177
  return undefined;
106
178
  return connector.readOnly === true
107
- ? `Read-only connection: only read-classified tools are available. Write calls are refused, including tools named in this guide.\n\n${content}`
179
+ ? `This connection can't perform writes; guide steps that change data will be refused. No write route exists for this connection.\n\n${content}`
108
180
  : content;
109
181
  }
110
182
  /** Discovery budget for one connector-guide summary, including an ellipsis. */
@@ -338,49 +410,116 @@ const PRIVATE = { resultType: "complete", ttlMs: 0, cacheScope: "private" };
338
410
  function localRecord(name, uri, description, content, aliases) {
339
411
  return { entry: { uri, frontmatter: { name, description }, resources: "dynamic" }, content, aliases };
340
412
  }
413
+ /** A local record's text after the frontmatter block Connecta wrote ahead of it. */
414
+ function localSkillBody(text) {
415
+ const close = text.indexOf("\n---\n", 3);
416
+ return close === -1 ? text : text.slice(close + "\n---\n".length).replace(/^\n/, "");
417
+ }
341
418
  /** JSON frontmatter is YAML too; no runtime provider imports or filesystem reads. */
342
419
  function validSkillName(name) {
343
420
  return name.length <= 64 && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name);
344
421
  }
345
- function localRecords(connectors) {
346
- return runEdge(localRecordsNative(connectors));
422
+ /** Resolve guide names once from the admitted connector roster, including collisions. */
423
+ export function resolvedConnectorGuides(connectors) {
424
+ return runEdge(resolvedConnectorGuidesNative(connectors));
425
+ }
426
+ const resolvedConnectorGuidesNative = /* @__PURE__ */ registerAuth(resolvedConnectorGuides, function (connectors) {
427
+ return authFlow(function* () {
428
+ const allocated = new Set(connectors.map((connector) => connector.id));
429
+ return yield* upwardStep(Effect.forEach(connectors
430
+ .filter((connector) => connectorGuide(connector) !== undefined)
431
+ .sort((a, b) => Number(connectorGuideRequired(b)) - Number(connectorGuideRequired(a)) ||
432
+ (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)), (connector) => authFlow(function* () {
433
+ const digest = validSkillName(connector.id)
434
+ ? ""
435
+ : bytesToHex(new Uint8Array(yield* upwardStep(authInvoke(crypto.subtle, "digest", ["SHA-256", encoder.encode(connector.id)]))));
436
+ const legacy = `connector-${digest.slice(0, 48)}`;
437
+ const slug = connector.id
438
+ .toLowerCase()
439
+ .replace(/[^a-z0-9]+/g, "-")
440
+ .replace(/^-|-$/g, "")
441
+ .slice(0, 42)
442
+ .replace(/-$/g, "") || "connector";
443
+ let name = connector.id;
444
+ if (!validSkillName(name)) {
445
+ const base = `${slug}-${digest.slice(0, 12)}`;
446
+ name = base;
447
+ let suffix = 1;
448
+ while (allocated.has(name))
449
+ name = `${base}-${suffix++}`;
450
+ allocated.add(name);
451
+ }
452
+ return {
453
+ connector,
454
+ name,
455
+ legacy,
456
+ reference: AVAILABLE_SKILLS.some((skill) => skill.name === name)
457
+ ? `skill://connecta/connectors/${name}`
458
+ : name,
459
+ };
460
+ }), { concurrency: 1 }));
461
+ });
462
+ });
463
+ function localRecords(connectors, readOnly = false) {
464
+ return runEdge(localRecordsNative(connectors, readOnly));
347
465
  }
348
- const localRecordsNative = /* @__PURE__ */ registerAuth(localRecords, function (connectors) {
466
+ const localRecordsNative = /* @__PURE__ */ registerAuth(localRecords, function (connectors, readOnly = false) {
349
467
  return authFlow(function* () {
350
468
  const builtIns = AVAILABLE_SKILLS.map((skill) => localRecord(skill.name, `skill://connecta/${skill.name}/SKILL.md`, skill.description, skill.content(), [
351
469
  skill.name,
352
470
  `skill://connecta/${skill.name}`,
353
471
  ]));
354
- const guides = yield* upwardStep(Effect.forEach(connectors
355
- .filter((connector) => connectorGuide(connector) !== undefined)
356
- .sort((a, b) => Number(connectorGuideRequired(b)) - Number(connectorGuideRequired(a)) ||
357
- (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)), (connector) => authFlow(function* () {
358
- const name = validSkillName(connector.id)
359
- ? connector.id
360
- : `connector-${bytesToHex(new Uint8Array(yield* upwardStep(authInvoke(crypto.subtle, "digest", ["SHA-256", encoder.encode(connector.id)])))).slice(0, 48)}`;
472
+ const identities = yield* upwardStep(authCall(resolvedConnectorGuides, [
473
+ connectors,
474
+ ]));
475
+ const guides = identities.map(({ connector, name, legacy }) => {
361
476
  const description = connectorGuideSummary(connector);
362
477
  const root = `skill://connecta/connectors/${encodeURIComponent(name)}`;
363
- const content = `---\n${JSON.stringify({ name, description }, null, 2)}\n---\n\n${connectorGuide(connector)}`;
478
+ // One read-only notice: the endpoint's already covers its connections.
479
+ const guide = readOnly
480
+ ? `${READ_ONLY_GUIDE_NOTICE}\n\n${authoredGuide(connector)}`
481
+ : connectorGuide(connector);
482
+ const content = `---\n${JSON.stringify({ name, description }, null, 2)}\n---\n\n${guide}`;
364
483
  return {
365
484
  ...localRecord(name, `${root}/SKILL.md`, description, content, [
366
485
  connectorSkillName(connector.id),
486
+ ...(connector.id.includes(":") ? [] : [connector.id]),
487
+ ...(name !== connector.id
488
+ ? [legacy, `skill://connecta/connectors/${legacy}`, `skill://connecta/connectors/${legacy}/SKILL.md`]
489
+ : []),
367
490
  `skill://connecta/connectors/${encodeURIComponent(connector.id)}`,
368
491
  root,
369
492
  ]),
370
493
  connector,
371
494
  };
372
- }), { concurrency: "unbounded" }));
495
+ });
373
496
  if (new Set(guides.map((guide) => guide.entry.uri)).size !== guides.length)
374
497
  throw new ConnectorCallError("unavailable", "Connector skill slugs collide.");
375
498
  return [builtIns[0], ...guides, ...builtIns.slice(1)];
376
499
  });
377
500
  });
378
- /** `granola` names connector granola's guide; built-in names and URIs never do. */
379
- function bareConnectorGuide(records, name) {
380
- if (name.includes(":") || AVAILABLE_SKILLS.some((skill) => skill.name === name))
381
- return undefined;
382
- const alias = connectorSkillName(name);
383
- return records.find((record) => record.connector !== undefined && record.aliases.includes(alias));
501
+ /** Canonical identities win; an alias shared by different guides is ambiguous. */
502
+ function localLookup(records, key) {
503
+ const canonical = new Map();
504
+ for (const record of records) {
505
+ canonical.set(record.entry.uri, record);
506
+ canonical.set(record.entry.uri.slice(0, -"/SKILL.md".length), record);
507
+ const name = record.entry.frontmatter.name;
508
+ if (!record.connector || !AVAILABLE_SKILLS.some((skill) => skill.name === name))
509
+ canonical.set(name, record);
510
+ }
511
+ const aliases = new Map();
512
+ for (const record of records) {
513
+ for (const alias of record.aliases) {
514
+ if (canonical.has(alias))
515
+ continue;
516
+ aliases.set(alias, aliases.has(alias) && aliases.get(alias) !== record ? null : record);
517
+ }
518
+ }
519
+ const result = canonical.get(key) ?? aliases.get(key);
520
+ if (result === null)
521
+ throw new ConnectorCallError("invalid_args", "Ambiguous skill alias; use its canonical URI.");
522
+ return result;
384
523
  }
385
524
  /** Keep the downstream path tree, so relative supporting-file references still resolve. */
386
525
  export function downstreamSkillUri(connectorId, uri) {
@@ -485,6 +624,7 @@ export class SkillsRegistry {
485
624
  baseUrl;
486
625
  options;
487
626
  scope;
627
+ cacheHints;
488
628
  local;
489
629
  snapshot;
490
630
  constructor(registry, baseUrl, options = {}) {
@@ -493,7 +633,11 @@ export class SkillsRegistry {
493
633
  this.options = options;
494
634
  void SkillsRegistry.nativePrograms;
495
635
  this.scope = options.requestScope ?? {};
496
- this.local = authCall(localRecords, [registry.listConnectors()]);
636
+ this.cacheHints = {
637
+ ...PRIVATE,
638
+ ttlMs: registry.listConnectors().some((connector) => connector.downstreamSkills !== undefined) ? 0 : 300_000,
639
+ };
640
+ this.local = authCall(localRecords, [registry.listConnectors(), options.readOnly === true]);
497
641
  }
498
642
  operation(connector, read) {
499
643
  return runEdge(authOwnMethod(SkillsRegistry.prototype, this, "operation", [connector, read]));
@@ -547,7 +691,7 @@ export class SkillsRegistry {
547
691
  : name.startsWith(CONNECTOR_SKILL_PREFIX)
548
692
  ? `Unknown connector "${boundedEchoText(id)}".`
549
693
  : `Unknown skill "${boundedEchoText(name)}".`;
550
- return new ConnectorCallError("not_found", `${message} Available skills: ${available}.`);
694
+ return new SkillLookupError("not_found", `${message} Available skills: ${available}.`);
551
695
  }
552
696
  list(cursor) {
553
697
  return runEdge(authOwnMethod(SkillsRegistry.prototype, this, "list", [cursor]));
@@ -574,7 +718,9 @@ export class SkillsRegistry {
574
718
  return (yield* upwardStep(authInvoke(self, "records", []))).map((record) => ({
575
719
  name: record.aliases[0] ?? record.entry.uri,
576
720
  uri: record.entry.uri,
577
- description: String(record.entry.frontmatter.description),
721
+ description: record.files
722
+ ? labeledDownstreamDescription(record.connector.id, String(record.entry.frontmatter.description))
723
+ : String(record.entry.frontmatter.description),
578
724
  }));
579
725
  })).pipe(Effect.provideService(RequestScope, requestOwner(this.scope)));
580
726
  }, SkillsRegistry.prototype.summaries);
@@ -583,10 +729,16 @@ export class SkillsRegistry {
583
729
  return duringViewEffect({ requestScope: this.scope, signal: this.options.requestSignal }, () => authFlow(function* () {
584
730
  const record = yield* upwardStep(authInvoke(self, "lookup", [name]));
585
731
  const content = (yield* upwardStep(authInvoke(self, "read", [record.entry.uri]))).contents[0];
732
+ // Connecta prepends frontmatter to local guides for native SKILL.md; a
733
+ // tool reader wants only the body. Downstream files keep their bytes.
734
+ if (!record.files && typeof content.text === "string")
735
+ return localSkillBody(content.text);
736
+ const origin = record.files ? `${downstreamOrigin(record.connector.id)}\n\n` : "";
586
737
  if (typeof content.text === "string")
587
- return content.text;
738
+ return origin + content.text;
588
739
  try {
589
- return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(Uint8Array.from(atob(content.blob), (char) => char.charCodeAt(0)));
740
+ return (origin +
741
+ new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(Uint8Array.from(atob(content.blob), (char) => char.charCodeAt(0))));
590
742
  }
591
743
  catch {
592
744
  throw new ConnectorCallError("unavailable", "Skill instructions are not UTF-8 text.");
@@ -596,7 +748,7 @@ export class SkillsRegistry {
596
748
  registerAuthMethod(SkillsRegistry.prototype, "read", function (uri) {
597
749
  const self = this;
598
750
  return duringViewEffect({ requestScope: this.scope, signal: this.options.requestSignal }, () => authFlow(function* () {
599
- const local = (yield* upwardStep(self.local)).find((record) => record.entry.uri === uri || record.aliases.includes(uri));
751
+ const local = localLookup(yield* upwardStep(self.local), uri);
600
752
  if (local) {
601
753
  const record = yield* upwardStep(authCall(withManifest, [local]));
602
754
  self.assertSafe([record]);
@@ -606,7 +758,7 @@ export class SkillsRegistry {
606
758
  const text = record.connector
607
759
  ? sentSecretsForRequest(self.scope).redact(record.content)
608
760
  : record.content;
609
- return { ...PRIVATE, contents: [{ uri: record.entry.uri, mimeType: "text/markdown", text }] };
761
+ return { ...self.cacheHints, contents: [{ uri: record.entry.uri, mimeType: "text/markdown", text }] };
610
762
  }
611
763
  if (!uri.startsWith("skill://downstream/"))
612
764
  throw self.missing(uri);
@@ -642,7 +794,7 @@ export class SkillsRegistry {
642
794
  self.options.onRead?.(record.connector, "downstream");
643
795
  // Downstream file contents take the request's full payload rules before any reader sees them.
644
796
  const clean = sentSecretsForRequest(self.scope).redact(content);
645
- return { ...PRIVATE, contents: [{ ...clean, uri }] };
797
+ return { ...self.cacheHints, contents: [{ ...clean, uri }] };
646
798
  })).pipe(Effect.provideService(RequestScope, requestOwner(this.scope)));
647
799
  }, SkillsRegistry.prototype.read);
648
800
  registerAuthMethod(SkillsRegistry.prototype, "resources", function (cursor) {
@@ -650,7 +802,7 @@ export class SkillsRegistry {
650
802
  return duringViewEffect({ requestScope: this.scope, signal: this.options.requestSignal }, () => authFlow(function* () {
651
803
  const page = yield* upwardStep(authInvoke(self, "list", [cursor]));
652
804
  return {
653
- ...PRIVATE,
805
+ ...self.cacheHints,
654
806
  resources: page.skills.flatMap((skill) => (skill.resources === "dynamic" ? [{ uri: skill.uri }] : skill.resources).map((file) => ({
655
807
  uri: file.uri,
656
808
  name: file.uri === skill.uri
@@ -668,7 +820,10 @@ export class SkillsRegistry {
668
820
  registerAuthMethod(SkillsRegistry.prototype, "get", function (uri) {
669
821
  const self = this;
670
822
  return duringViewEffect({ requestScope: this.scope, signal: this.options.requestSignal }, () => authFlow(function* () {
671
- return { ...PRIVATE, skill: (yield* upwardStep(authInvoke(self, "lookup", [uri]))).entry };
823
+ return {
824
+ ...self.cacheHints,
825
+ skill: (yield* upwardStep(authInvoke(self, "lookup", [uri]))).entry,
826
+ };
672
827
  })).pipe(Effect.provideService(RequestScope, requestOwner(this.scope)));
673
828
  }, SkillsRegistry.prototype.get);
674
829
  registerAuthMethod(SkillsRegistry.prototype, "list", function (cursor) {
@@ -679,7 +834,7 @@ export class SkillsRegistry {
679
834
  if (!Number.isSafeInteger(offset) || offset % SKILL_PAGE_SIZE !== 0 || offset >= records.length)
680
835
  throw new ConnectorCallError("invalid_args", "Invalid skills cursor.");
681
836
  return {
682
- ...PRIVATE,
837
+ ...self.cacheHints,
683
838
  skills: records.slice(offset, offset + SKILL_PAGE_SIZE).map((record) => record.entry),
684
839
  ...(offset + SKILL_PAGE_SIZE < records.length
685
840
  ? { nextCursor: `skills:${offset + SKILL_PAGE_SIZE}` }
@@ -691,8 +846,7 @@ export class SkillsRegistry {
691
846
  const self = this;
692
847
  return duringViewEffect({ requestScope: this.scope, signal: this.options.requestSignal }, () => authFlow(function* () {
693
848
  const records = yield* upwardStep(self.local);
694
- const local = records.find((record) => record.entry.uri === uri || record.aliases.includes(uri)) ??
695
- bareConnectorGuide(records, uri);
849
+ const local = localLookup(records, uri);
696
850
  if (local) {
697
851
  const record = yield* upwardStep(authCall(withManifest, [local]));
698
852
  self.assertSafe([record]);
@@ -791,9 +945,11 @@ export class SkillsRegistry {
791
945
  ...(self.options.defer ? { defer: self.options.defer } : {}),
792
946
  });
793
947
  yield* closeScopeOnExit(connector, ctx, self.options.defer);
794
- return yield* authCall(read, [ctx]).pipe(Effect.mapError((error) => error instanceof ConnectorCallError && error.code === "auth_required"
795
- ? new ConnectorCallError("auth_required", "Downstream skills require authorization.")
796
- : new ConnectorCallError("unavailable", "Downstream skills are unavailable.")));
948
+ return yield* authCall(read, [ctx]).pipe(Effect.mapError((error) => error instanceof ConnectorCallError && error.code === "unsupported_capability"
949
+ ? new ConnectorCallError("unsupported_capability", "Downstream Skills capability is not supported.", { retryable: false })
950
+ : error instanceof ConnectorCallError && error.code === "auth_required"
951
+ ? new ConnectorCallError("auth_required", "Downstream skills require authorization.")
952
+ : new ConnectorCallError("unavailable", "Downstream skills are unavailable.")));
797
953
  })), {
798
954
  timeoutMs,
799
955
  ...(self.options.requestSignal ? { signal: self.options.requestSignal } : {}),