@zackbart/connecta 0.40.0 → 0.41.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 (115) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/bin/version.mjs +1 -1
  3. package/dist/activity.d.ts +30 -4
  4. package/dist/activity.js +28 -2
  5. package/dist/agent-observation.js +24 -2
  6. package/dist/agent-routes.js +4 -41
  7. package/dist/auth/clerk.d.ts +6 -1
  8. package/dist/auth/clerk.js +16 -0
  9. package/dist/auth/cloudflare-access.js +49 -7
  10. package/dist/auth/downstream-oauth/consent.js +79 -29
  11. package/dist/auth/downstream-oauth/grant-store.js +58 -7
  12. package/dist/auth/downstream-oauth/refresh-records.js +6 -5
  13. package/dist/auth/downstream-oauth.js +12 -4
  14. package/dist/auth/oauth-request-not-sent.js +4 -0
  15. package/dist/auth/static-oauth.js +51 -21
  16. package/dist/catalog-cache.js +62 -21
  17. package/dist/catalog-freshness.js +13 -0
  18. package/dist/catalog-service.js +39 -12
  19. package/dist/catalog.js +103 -38
  20. package/dist/config.d.ts +10 -4
  21. package/dist/config.js +30 -7
  22. package/dist/connectors/remote-mcp/call.js +1 -1
  23. package/dist/connectors/remote-mcp/connection.js +52 -18
  24. package/dist/connectors/remote-mcp/oauth.js +2 -2
  25. package/dist/connectors/remote-mcp/resources.js +5 -2
  26. package/dist/connectors/remote-mcp/transport.js +24 -6
  27. package/dist/connectors/remote-mcp.js +4 -4
  28. package/dist/credential-contract.d.ts +3 -1
  29. package/dist/describe-config.d.ts +1 -0
  30. package/dist/describe-config.js +1 -0
  31. package/dist/downstream-input.js +11 -2
  32. package/dist/errors.d.ts +8 -1
  33. package/dist/errors.js +2 -0
  34. package/dist/execute.js +20 -7
  35. package/dist/executor-result.js +1 -1
  36. package/dist/executors/quickjs-child.js +1 -1
  37. package/dist/executors/quickjs-runtime.js +1 -1
  38. package/dist/executors/quickjs.js +1 -1
  39. package/dist/guarded-transport.js +2 -0
  40. package/dist/http/client.js +7 -0
  41. package/dist/http/hatch.js +2 -0
  42. package/dist/http/node-transport.js +110 -4
  43. package/dist/http/oauth.js +17 -6
  44. package/dist/index.js +10 -21
  45. package/dist/invocation-failure.js +28 -0
  46. package/dist/invocation.js +72 -33
  47. package/dist/membership.d.ts +1 -1
  48. package/dist/meta-tools.js +27 -13
  49. package/dist/module-contracts.d.ts +3 -1
  50. package/dist/native-tools.js +11 -4
  51. package/dist/node.d.ts +7 -0
  52. package/dist/node.js +82 -3
  53. package/dist/operation-activity.js +103 -0
  54. package/dist/operator-record.d.ts +8 -0
  55. package/dist/operator-record.js +15 -0
  56. package/dist/operator-ui/generated.js +2 -2
  57. package/dist/provider.d.ts +1 -1
  58. package/dist/providers/_shared/google/workspace.d.ts +1 -1
  59. package/dist/providers/breeze/index.js +1 -1
  60. package/dist/providers/ccb/index.js +1 -1
  61. package/dist/providers/cloudflare/index.js +1 -1
  62. package/dist/providers/docs/index.js +1 -1
  63. package/dist/providers/drive/index.js +1 -1
  64. package/dist/providers/forms/index.js +1 -1
  65. package/dist/providers/github/index.js +1 -1
  66. package/dist/providers/gmail/index.js +1 -1
  67. package/dist/providers/granola/index.js +1 -1
  68. package/dist/providers/overflow/index.js +1 -1
  69. package/dist/providers/planning-center/index.js +1 -1
  70. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  71. package/dist/providers/quickbooks/api.js +28 -25
  72. package/dist/providers/quickbooks/index.js +1 -1
  73. package/dist/providers/quickbooks/lib/writes.js +55 -0
  74. package/dist/providers/quickbooks/safety.js +17 -5
  75. package/dist/providers/quickbooks/skill.generated.js +1 -1
  76. package/dist/providers/sheets/index.js +1 -1
  77. package/dist/providers/slides/index.js +1 -1
  78. package/dist/providers/stripe/index.js +2 -2
  79. package/dist/providers/tithely/index.js +1 -1
  80. package/dist/providers/vercel/api.js +1 -0
  81. package/dist/registry.js +16 -4
  82. package/dist/resource-read.js +21 -2
  83. package/dist/route-names.js +44 -0
  84. package/dist/routes/activity-context.js +1 -0
  85. package/dist/routes/activity-disclosure.js +26 -4
  86. package/dist/routes/activity.js +2 -1
  87. package/dist/routes/mcp.js +32 -13
  88. package/dist/routes/ui-config.js +1 -1
  89. package/dist/routes/ui-data.js +3 -2
  90. package/dist/runtime/native-storage.js +12 -0
  91. package/dist/runtime/request-reads.js +66 -14
  92. package/dist/runtime/request-scope.js +2 -1
  93. package/dist/skill-origin.js +16 -0
  94. package/dist/skills.js +132 -43
  95. package/dist/storage/d1-driver.js +33 -5
  96. package/dist/storage/keys.js +7 -1
  97. package/dist/storage/sql-native.js +60 -3
  98. package/dist/storage/sql.js +6 -0
  99. package/dist/timing-trace.js +84 -0
  100. package/dist/types.d.ts +5 -0
  101. package/dist/url-safety.js +38 -20
  102. package/dist/utility-recovery.js +33 -0
  103. package/dist/version.d.ts +1 -1
  104. package/dist/version.js +1 -1
  105. package/dist/worker.js +33 -8
  106. package/documentation/architecture.md +48 -1
  107. package/documentation/auth.md +116 -36
  108. package/documentation/code-mode.md +61 -46
  109. package/documentation/deploying.md +25 -1
  110. package/documentation/meta-tools.md +113 -11
  111. package/documentation/operating.md +86 -1
  112. package/documentation/operator-ui.md +51 -2
  113. package/documentation/providers.md +16 -14
  114. package/package.json +10 -8
  115. package/templates/node/package.json +1 -1
package/dist/skills.js CHANGED
@@ -5,11 +5,15 @@ import { RequestScope, requestOwner } from "./runtime/request-scope.js";
5
5
  import { upwardStep } from "./upward.js";
6
6
  import { authCall, authInvoke, authFlow, authMethod, registerAuth, registerAuthMethod, authOwnMethod, } from "./auth/native.js";
7
7
  import { USAGE_SKILL } from "./usage-guide.js";
8
+ import { downstreamOrigin, labeledDownstreamDescription } from "./skill-origin.js";
8
9
  import { boundedEchoText, ConnectorCallError } from "./errors.js";
9
10
  import { Effect } from "effect";
10
11
  import { runEdge, withDeadlineEffect } from "./runtime/run.js";
11
12
  import { closeScopeOnExit } from "./runtime/connector-scope.js";
12
13
  import { sentSecretsForRequest } from "./sent-secrets.js";
14
+ /** Trusted diagnostics constructed from the caller-visible local registry. */
15
+ export class SkillLookupError extends ConnectorCallError {
16
+ }
13
17
  // The meta instructions and built-in guides use the meta names: built-in guides
14
18
  // are byte-identical across deployments, and the eval code arm replaces usage.
15
19
  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.`;
@@ -20,22 +24,36 @@ const RECOVERY = `When an auth error includes a handoff, give it to the user or
20
24
  * and the client owns approval; a read-only one omits write tools from search
21
25
  * and describe and refuses every write call. Guides may still name them.
22
26
  */
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}`;
27
+ function writePolicy(readOnly, routes) {
28
+ return readOnly
29
+ ? "This endpoint can't perform writes; guide steps that change data will be refused. No write route exists on this endpoint."
30
+ : `This endpoint permits writes through ${routes.write}${routes.program === META_ROUTES.program ? ` and ${routes.program}` : ""}, subject to connector restrictions; the host owns approval.`;
31
+ }
32
+ export const CONNECTA_INSTRUCTIONS = `${ROUTE} ${writePolicy(false, META_ROUTES)} Never repeat a write to recover its output. ${RECOVERY}`;
24
33
  /**
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
34
+ * The instructions one endpoint serves: pool scope and guide pointer first,
35
+ * then stable routing rules. Plain `/mcp` and a
27
36
  * pool without a description serve the constant unchanged. The description
28
37
  * comes from deployment config for the pool the request was admitted through;
29
38
  * nothing a request sends reaches it.
30
39
  */
31
- export function endpointInstructions(description, native) {
32
- const instructions = native ? nativeInstructions(native.executeCode) : CONNECTA_INSTRUCTIONS;
33
- return description === undefined ? instructions : `${instructions} This endpoint: ${description}`;
40
+ export function endpointInstructions(description, native, readOnly = false) {
41
+ const instructions = native
42
+ ? nativeInstructions(native.executeCode, readOnly)
43
+ : readOnly
44
+ ? `Choose a route before discovery. One known read uses ${META_ROUTES.read}; other read work uses ${META_ROUTES.program} to discover, call and return the answer. ${writePolicy(true, META_ROUTES)} Never repeat a write for output. ${RECOVERY}`
45
+ : CONNECTA_INSTRUCTIONS;
46
+ if (description === undefined)
47
+ return instructions;
48
+ const routing = native
49
+ ? instructions
50
+ : `Known read: ${META_ROUTES.read}${readOnly ? "" : `; known write: ${META_ROUTES.write}`}. Other work: ${META_ROUTES.program}; discover and call together, return answers, sample unfamiliar reads. ${writePolicy(readOnly, META_ROUTES)} Never repeat writes for output. Give auth handoffs to user/operator; ${META_ROUTES.authorize} refreshes links. Page resultId/format:"paged" with connecta.result(handleOrId,{page?}).`;
51
+ return `Use skills. ${description} ${routing}`;
34
52
  }
35
53
  /** Instructions for pools whose host searches the listed native tools. */
36
- export function nativeInstructions(executeCode) {
54
+ export function nativeInstructions(executeCode, readOnly = false) {
37
55
  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.`;
56
+ return `Annotations classify reads/writes. Call known tools directly; ${executeCode ? `combine work in ${routes.program} using tool descriptions’ program addresses.` : "find tools with your host's search."} ${writePolicy(readOnly, routes)} Never repeat a write for output. Give auth handoffs to the user/operator. Tools missing? Ask the operator to repair, then reconnect. Follow nextAction to page with ${executeCode ? `connecta.result in ${routes.program}` : "read_result"}. ${routes.skills} lists guidance.`;
39
57
  }
40
58
  /** True when at least one of `connectors` carries a usage guide. */
41
59
  export function hasConnectorGuides(connectors) {
@@ -104,7 +122,7 @@ export function connectorGuide(connector) {
104
122
  if (!content || content.trim() === "")
105
123
  return undefined;
106
124
  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}`
125
+ ? `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
126
  : content;
109
127
  }
110
128
  /** Discovery budget for one connector-guide summary, including an ellipsis. */
@@ -342,45 +360,103 @@ function localRecord(name, uri, description, content, aliases) {
342
360
  function validSkillName(name) {
343
361
  return name.length <= 64 && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name);
344
362
  }
345
- function localRecords(connectors) {
346
- return runEdge(localRecordsNative(connectors));
363
+ /** Resolve guide names once from the admitted connector roster, including collisions. */
364
+ export function resolvedConnectorGuides(connectors) {
365
+ return runEdge(resolvedConnectorGuidesNative(connectors));
366
+ }
367
+ const resolvedConnectorGuidesNative = /* @__PURE__ */ registerAuth(resolvedConnectorGuides, function (connectors) {
368
+ return authFlow(function* () {
369
+ const allocated = new Set(connectors.map((connector) => connector.id));
370
+ return yield* upwardStep(Effect.forEach(connectors
371
+ .filter((connector) => connectorGuide(connector) !== undefined)
372
+ .sort((a, b) => Number(connectorGuideRequired(b)) - Number(connectorGuideRequired(a)) ||
373
+ (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)), (connector) => authFlow(function* () {
374
+ const digest = validSkillName(connector.id)
375
+ ? ""
376
+ : bytesToHex(new Uint8Array(yield* upwardStep(authInvoke(crypto.subtle, "digest", ["SHA-256", encoder.encode(connector.id)]))));
377
+ const legacy = `connector-${digest.slice(0, 48)}`;
378
+ const slug = connector.id
379
+ .toLowerCase()
380
+ .replace(/[^a-z0-9]+/g, "-")
381
+ .replace(/^-|-$/g, "")
382
+ .slice(0, 42)
383
+ .replace(/-$/g, "") || "connector";
384
+ let name = connector.id;
385
+ if (!validSkillName(name)) {
386
+ const base = `${slug}-${digest.slice(0, 12)}`;
387
+ name = base;
388
+ let suffix = 1;
389
+ while (allocated.has(name))
390
+ name = `${base}-${suffix++}`;
391
+ allocated.add(name);
392
+ }
393
+ return {
394
+ connector,
395
+ name,
396
+ legacy,
397
+ reference: AVAILABLE_SKILLS.some((skill) => skill.name === name)
398
+ ? `skill://connecta/connectors/${name}`
399
+ : name,
400
+ };
401
+ }), { concurrency: 1 }));
402
+ });
403
+ });
404
+ function localRecords(connectors, readOnly = false) {
405
+ return runEdge(localRecordsNative(connectors, readOnly));
347
406
  }
348
- const localRecordsNative = /* @__PURE__ */ registerAuth(localRecords, function (connectors) {
407
+ const localRecordsNative = /* @__PURE__ */ registerAuth(localRecords, function (connectors, readOnly = false) {
349
408
  return authFlow(function* () {
350
409
  const builtIns = AVAILABLE_SKILLS.map((skill) => localRecord(skill.name, `skill://connecta/${skill.name}/SKILL.md`, skill.description, skill.content(), [
351
410
  skill.name,
352
411
  `skill://connecta/${skill.name}`,
353
412
  ]));
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)}`;
413
+ const identities = yield* upwardStep(authCall(resolvedConnectorGuides, [
414
+ connectors,
415
+ ]));
416
+ const guides = identities.map(({ connector, name, legacy }) => {
361
417
  const description = connectorGuideSummary(connector);
362
418
  const root = `skill://connecta/connectors/${encodeURIComponent(name)}`;
363
- const content = `---\n${JSON.stringify({ name, description }, null, 2)}\n---\n\n${connectorGuide(connector)}`;
419
+ const content = `---\n${JSON.stringify({ name, description }, null, 2)}\n---\n\n${readOnly ? `${writePolicy(true, META_ROUTES)}\n\n` : ""}${connectorGuide(connector)}`;
364
420
  return {
365
421
  ...localRecord(name, `${root}/SKILL.md`, description, content, [
366
422
  connectorSkillName(connector.id),
423
+ ...(connector.id.includes(":") ? [] : [connector.id]),
424
+ ...(name !== connector.id
425
+ ? [legacy, `skill://connecta/connectors/${legacy}`, `skill://connecta/connectors/${legacy}/SKILL.md`]
426
+ : []),
367
427
  `skill://connecta/connectors/${encodeURIComponent(connector.id)}`,
368
428
  root,
369
429
  ]),
370
430
  connector,
371
431
  };
372
- }), { concurrency: "unbounded" }));
432
+ });
373
433
  if (new Set(guides.map((guide) => guide.entry.uri)).size !== guides.length)
374
434
  throw new ConnectorCallError("unavailable", "Connector skill slugs collide.");
375
435
  return [builtIns[0], ...guides, ...builtIns.slice(1)];
376
436
  });
377
437
  });
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));
438
+ /** Canonical identities win; an alias shared by different guides is ambiguous. */
439
+ function localLookup(records, key) {
440
+ const canonical = new Map();
441
+ for (const record of records) {
442
+ canonical.set(record.entry.uri, record);
443
+ canonical.set(record.entry.uri.slice(0, -"/SKILL.md".length), record);
444
+ const name = record.entry.frontmatter.name;
445
+ if (!record.connector || !AVAILABLE_SKILLS.some((skill) => skill.name === name))
446
+ canonical.set(name, record);
447
+ }
448
+ const aliases = new Map();
449
+ for (const record of records) {
450
+ for (const alias of record.aliases) {
451
+ if (canonical.has(alias))
452
+ continue;
453
+ aliases.set(alias, aliases.has(alias) && aliases.get(alias) !== record ? null : record);
454
+ }
455
+ }
456
+ const result = canonical.get(key) ?? aliases.get(key);
457
+ if (result === null)
458
+ throw new ConnectorCallError("invalid_args", "Ambiguous skill alias; use its canonical URI.");
459
+ return result;
384
460
  }
385
461
  /** Keep the downstream path tree, so relative supporting-file references still resolve. */
386
462
  export function downstreamSkillUri(connectorId, uri) {
@@ -485,6 +561,7 @@ export class SkillsRegistry {
485
561
  baseUrl;
486
562
  options;
487
563
  scope;
564
+ cacheHints;
488
565
  local;
489
566
  snapshot;
490
567
  constructor(registry, baseUrl, options = {}) {
@@ -493,7 +570,11 @@ export class SkillsRegistry {
493
570
  this.options = options;
494
571
  void SkillsRegistry.nativePrograms;
495
572
  this.scope = options.requestScope ?? {};
496
- this.local = authCall(localRecords, [registry.listConnectors()]);
573
+ this.cacheHints = {
574
+ ...PRIVATE,
575
+ ttlMs: registry.listConnectors().some((connector) => connector.downstreamSkills !== undefined) ? 0 : 300_000,
576
+ };
577
+ this.local = authCall(localRecords, [registry.listConnectors(), options.readOnly === true]);
497
578
  }
498
579
  operation(connector, read) {
499
580
  return runEdge(authOwnMethod(SkillsRegistry.prototype, this, "operation", [connector, read]));
@@ -547,7 +628,7 @@ export class SkillsRegistry {
547
628
  : name.startsWith(CONNECTOR_SKILL_PREFIX)
548
629
  ? `Unknown connector "${boundedEchoText(id)}".`
549
630
  : `Unknown skill "${boundedEchoText(name)}".`;
550
- return new ConnectorCallError("not_found", `${message} Available skills: ${available}.`);
631
+ return new SkillLookupError("not_found", `${message} Available skills: ${available}.`);
551
632
  }
552
633
  list(cursor) {
553
634
  return runEdge(authOwnMethod(SkillsRegistry.prototype, this, "list", [cursor]));
@@ -574,7 +655,9 @@ export class SkillsRegistry {
574
655
  return (yield* upwardStep(authInvoke(self, "records", []))).map((record) => ({
575
656
  name: record.aliases[0] ?? record.entry.uri,
576
657
  uri: record.entry.uri,
577
- description: String(record.entry.frontmatter.description),
658
+ description: record.files
659
+ ? labeledDownstreamDescription(record.connector.id, String(record.entry.frontmatter.description))
660
+ : String(record.entry.frontmatter.description),
578
661
  }));
579
662
  })).pipe(Effect.provideService(RequestScope, requestOwner(this.scope)));
580
663
  }, SkillsRegistry.prototype.summaries);
@@ -583,10 +666,12 @@ export class SkillsRegistry {
583
666
  return duringViewEffect({ requestScope: this.scope, signal: this.options.requestSignal }, () => authFlow(function* () {
584
667
  const record = yield* upwardStep(authInvoke(self, "lookup", [name]));
585
668
  const content = (yield* upwardStep(authInvoke(self, "read", [record.entry.uri]))).contents[0];
669
+ const origin = record.files ? `${downstreamOrigin(record.connector.id)}\n\n` : "";
586
670
  if (typeof content.text === "string")
587
- return content.text;
671
+ return origin + content.text;
588
672
  try {
589
- return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(Uint8Array.from(atob(content.blob), (char) => char.charCodeAt(0)));
673
+ return (origin +
674
+ new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(Uint8Array.from(atob(content.blob), (char) => char.charCodeAt(0))));
590
675
  }
591
676
  catch {
592
677
  throw new ConnectorCallError("unavailable", "Skill instructions are not UTF-8 text.");
@@ -596,7 +681,7 @@ export class SkillsRegistry {
596
681
  registerAuthMethod(SkillsRegistry.prototype, "read", function (uri) {
597
682
  const self = this;
598
683
  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));
684
+ const local = localLookup(yield* upwardStep(self.local), uri);
600
685
  if (local) {
601
686
  const record = yield* upwardStep(authCall(withManifest, [local]));
602
687
  self.assertSafe([record]);
@@ -606,7 +691,7 @@ export class SkillsRegistry {
606
691
  const text = record.connector
607
692
  ? sentSecretsForRequest(self.scope).redact(record.content)
608
693
  : record.content;
609
- return { ...PRIVATE, contents: [{ uri: record.entry.uri, mimeType: "text/markdown", text }] };
694
+ return { ...self.cacheHints, contents: [{ uri: record.entry.uri, mimeType: "text/markdown", text }] };
610
695
  }
611
696
  if (!uri.startsWith("skill://downstream/"))
612
697
  throw self.missing(uri);
@@ -642,7 +727,7 @@ export class SkillsRegistry {
642
727
  self.options.onRead?.(record.connector, "downstream");
643
728
  // Downstream file contents take the request's full payload rules before any reader sees them.
644
729
  const clean = sentSecretsForRequest(self.scope).redact(content);
645
- return { ...PRIVATE, contents: [{ ...clean, uri }] };
730
+ return { ...self.cacheHints, contents: [{ ...clean, uri }] };
646
731
  })).pipe(Effect.provideService(RequestScope, requestOwner(this.scope)));
647
732
  }, SkillsRegistry.prototype.read);
648
733
  registerAuthMethod(SkillsRegistry.prototype, "resources", function (cursor) {
@@ -650,7 +735,7 @@ export class SkillsRegistry {
650
735
  return duringViewEffect({ requestScope: this.scope, signal: this.options.requestSignal }, () => authFlow(function* () {
651
736
  const page = yield* upwardStep(authInvoke(self, "list", [cursor]));
652
737
  return {
653
- ...PRIVATE,
738
+ ...self.cacheHints,
654
739
  resources: page.skills.flatMap((skill) => (skill.resources === "dynamic" ? [{ uri: skill.uri }] : skill.resources).map((file) => ({
655
740
  uri: file.uri,
656
741
  name: file.uri === skill.uri
@@ -668,7 +753,10 @@ export class SkillsRegistry {
668
753
  registerAuthMethod(SkillsRegistry.prototype, "get", function (uri) {
669
754
  const self = this;
670
755
  return duringViewEffect({ requestScope: this.scope, signal: this.options.requestSignal }, () => authFlow(function* () {
671
- return { ...PRIVATE, skill: (yield* upwardStep(authInvoke(self, "lookup", [uri]))).entry };
756
+ return {
757
+ ...self.cacheHints,
758
+ skill: (yield* upwardStep(authInvoke(self, "lookup", [uri]))).entry,
759
+ };
672
760
  })).pipe(Effect.provideService(RequestScope, requestOwner(this.scope)));
673
761
  }, SkillsRegistry.prototype.get);
674
762
  registerAuthMethod(SkillsRegistry.prototype, "list", function (cursor) {
@@ -679,7 +767,7 @@ export class SkillsRegistry {
679
767
  if (!Number.isSafeInteger(offset) || offset % SKILL_PAGE_SIZE !== 0 || offset >= records.length)
680
768
  throw new ConnectorCallError("invalid_args", "Invalid skills cursor.");
681
769
  return {
682
- ...PRIVATE,
770
+ ...self.cacheHints,
683
771
  skills: records.slice(offset, offset + SKILL_PAGE_SIZE).map((record) => record.entry),
684
772
  ...(offset + SKILL_PAGE_SIZE < records.length
685
773
  ? { nextCursor: `skills:${offset + SKILL_PAGE_SIZE}` }
@@ -691,8 +779,7 @@ export class SkillsRegistry {
691
779
  const self = this;
692
780
  return duringViewEffect({ requestScope: this.scope, signal: this.options.requestSignal }, () => authFlow(function* () {
693
781
  const records = yield* upwardStep(self.local);
694
- const local = records.find((record) => record.entry.uri === uri || record.aliases.includes(uri)) ??
695
- bareConnectorGuide(records, uri);
782
+ const local = localLookup(records, uri);
696
783
  if (local) {
697
784
  const record = yield* upwardStep(authCall(withManifest, [local]));
698
785
  self.assertSafe([record]);
@@ -791,9 +878,11 @@ export class SkillsRegistry {
791
878
  ...(self.options.defer ? { defer: self.options.defer } : {}),
792
879
  });
793
880
  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.")));
881
+ return yield* authCall(read, [ctx]).pipe(Effect.mapError((error) => error instanceof ConnectorCallError && error.code === "unsupported_capability"
882
+ ? new ConnectorCallError("unsupported_capability", "Downstream Skills capability is not supported.", { retryable: false })
883
+ : error instanceof ConnectorCallError && error.code === "auth_required"
884
+ ? new ConnectorCallError("auth_required", "Downstream skills require authorization.")
885
+ : new ConnectorCallError("unavailable", "Downstream skills are unavailable.")));
797
886
  })), {
798
887
  timeoutMs,
799
888
  ...(self.options.requestSignal ? { signal: self.options.requestSignal } : {}),
@@ -1,15 +1,43 @@
1
+ import { Effect, Option } from "effect";
2
+ import { RequestScope } from "../runtime/request-scope.js";
3
+ import { hasTimingTraces, traceFor } from "../timing-trace.js";
4
+ import { utf8Length } from "../bytes.js";
5
+ // Request-wide totals include admission, credentials and catalog work. These
6
+ // I/O-spanning observations are not pure storage latency or CPU attribution.
7
+ function timedD1(family, statements, operation) {
8
+ return Effect.suspend(() => {
9
+ if (!hasTimingTraces())
10
+ return operation;
11
+ return Effect.flatMap(Effect.serviceOption(RequestScope), (owner) => {
12
+ const trace = Option.isSome(owner) ? traceFor(owner.value.identity) : undefined;
13
+ if (!trace)
14
+ return operation;
15
+ const start = performance.now();
16
+ let bytes = 0;
17
+ return operation.pipe(Effect.tap((value) => Effect.sync(() => {
18
+ // Count serialized application result bytes, not D1 wire bytes.
19
+ try {
20
+ bytes = utf8Length(JSON.stringify(value) ?? "");
21
+ }
22
+ catch {
23
+ /* no payload retained */
24
+ }
25
+ })), Effect.ensuring(Effect.sync(() => trace.record(family, performance.now() - start, bytes, statements))));
26
+ });
27
+ });
28
+ }
1
29
  import { effectSqlDriver, sqlDriverEdge, sqlPromise } from "./sql-driver.js";
2
30
  export function d1Driver(db) {
3
31
  const prepare = (statement) => statement.params.length > 0 ? db.prepare(statement.sql).bind(...statement.params) : db.prepare(statement.sql);
4
32
  return sqlDriverEdge(effectSqlDriver({
5
- all: (statement) => sqlPromise(() => prepare(statement)
33
+ all: (statement) => timedD1("d1.read", 1, sqlPromise(() => prepare(statement)
6
34
  .all()
7
- .then((result) => result.results)),
8
- run: (statement) => sqlPromise(() => prepare(statement)
35
+ .then((result) => result.results))),
36
+ run: (statement) => timedD1("d1.write", 1, sqlPromise(() => prepare(statement)
9
37
  .run()
10
- .then((result) => result.meta.changes ?? 0)),
38
+ .then((result) => result.meta.changes ?? 0))),
11
39
  // D1's public binding API preserves per-statement changes, including zero;
12
40
  // execute the whole array once. No withTransaction, RETURNING, or replay.
13
- batch: (statements) => sqlPromise(() => db.batch(statements.map(prepare)).then((results) => results.map((result) => result.meta.changes ?? 0))),
41
+ batch: (statements) => timedD1("d1.batch", statements.length, sqlPromise(() => db.batch(statements.map(prepare)).then((results) => results.map((result) => result.meta.changes ?? 0)))),
14
42
  }));
15
43
  }
@@ -176,6 +176,8 @@ export const connectorHealthKeys = {
176
176
  discovery: (connectorId, partitionDigest) => validateStorageKey(`connector-health:v1:${connectorId}:${partitionDigest}:discovery`),
177
177
  status: (connectorId, partitionDigest) => validateStorageKey(`connector-health:v1:${connectorId}:${partitionDigest}:status`),
178
178
  };
179
+ const responseCacheLocatorPrefix = (id, config) => validateStorageKey(`response-cache:v1:${id}:${config}:`);
180
+ const responseCacheLocatorSeparator = ":";
179
181
  /**
180
182
  * SDK catalog response cache, complete manifests published after their chunks.
181
183
  * Secret-bearing inputs reach these keys only through `secretDigest`.
@@ -195,7 +197,11 @@ export const responseCacheKeys = {
195
197
  },
196
198
  prefix: (id) => validateStorageKey(`response-cache:v1:${id}:`),
197
199
  generation: (id) => validateStorageKey(`response-cache:v1:${id}:generation`),
198
- namespace: (id, config, generation) => validateStorageKey(`response-cache:v1:${id}:${config}:${generation}:`),
200
+ namespace: (id, config, generation) => validateStorageKey(`${responseCacheLocatorPrefix(id, config)}${generation}${responseCacheLocatorSeparator}`),
201
+ /** SQL binds these layout parts around the stored generation; it owns no delimiters. */
202
+ locatorPrefix: responseCacheLocatorPrefix,
203
+ locatorSeparator: responseCacheLocatorSeparator,
204
+ locatedEntry: (prefix, generation, partition) => validateStorageKey(`${prefix}${generation}${responseCacheLocatorSeparator}${partition}`),
199
205
  entry: (namespace, partition) => validateStorageKey(`${namespace}${partition}`),
200
206
  refreshDigest: (id, config, partition) => validateStorageKey(`response-cache:v1:${id}:refresh-digest:${config}:${partition}`),
201
207
  chunk: (entry, revision, index) => validateStorageKey(`${entry}:chunk:${revision}:${index}`),
@@ -27,7 +27,7 @@ import { Effect } from "effect";
27
27
  import { activityBehaviorFacts, activityPackageVersion, activityClientFact, InvalidActivityCursorError, } from "../activity.js";
28
28
  import { assertKnownOptions, ConfigError, keys, optionsOf } from "../config-schema.js";
29
29
  import { agentFrictionForCode } from "../activity-friction.js";
30
- import { accessTokenKeys, validateStorageKey } from "./keys.js";
30
+ import { accessTokenKeys, responseCacheKeys, validateStorageKey } from "./keys.js";
31
31
  const sql = (text, ...params) => ({
32
32
  sql: text,
33
33
  params,
@@ -132,6 +132,34 @@ export function nativeSqlStorage(driver) {
132
132
  return new Map(rows.map((row) => [row.key, textOf(row.value, row.value_bytes, "Stored access token metadata")]));
133
133
  });
134
134
  },
135
+ catalogSnapshot(keys, generationKey, prefix, suffixes) {
136
+ return Effect.gen(function* () {
137
+ if (keys.length < 1 || keys.length > 4 || suffixes.length > 2)
138
+ throw new RangeError("Catalog locator exceeds bounds");
139
+ for (const key of [...keys, generationKey, prefix, ...suffixes])
140
+ validateStorageKey(key);
141
+ yield* ensure();
142
+ const rows = yield* driver.all(sql(`WITH initial AS (
143
+ SELECT key, value FROM connecta_kv WHERE key IN (${keys.map(() => "?").join(",")}) AND ${live}
144
+ ), suffixes(suffix) AS (VALUES ${suffixes.map(() => "(?)").join(",") || "(NULL)"}), wanted(key) AS (
145
+ SELECT key FROM initial UNION ALL
146
+ SELECT ? || (SELECT value FROM initial WHERE key = ?) || ? || suffix FROM suffixes
147
+ ) SELECT key, ${textColumn("value")} FROM connecta_kv
148
+ WHERE key IN (SELECT key FROM wanted) AND ${live}`, ...keys, ...suffixes, prefix, generationKey, responseCacheKeys.locatorSeparator));
149
+ const values = new Map(rows.map((row) => [row.key, textOf(row.value, row.value_bytes, "Stored catalog locator")]));
150
+ for (const key of keys)
151
+ if (!values.has(key))
152
+ values.set(key, null);
153
+ const generation = values.get(generationKey);
154
+ if (generation)
155
+ for (const suffix of suffixes) {
156
+ const key = responseCacheKeys.locatedEntry(prefix, generation, suffix);
157
+ if (!values.has(key))
158
+ values.set(key, null);
159
+ }
160
+ return values;
161
+ });
162
+ },
135
163
  getMany(keys) {
136
164
  return Effect.gen(function* () {
137
165
  for (const key of keys)
@@ -157,6 +185,25 @@ export function nativeSqlStorage(driver) {
157
185
  return values;
158
186
  });
159
187
  },
188
+ setMany(entries, opts) {
189
+ return Effect.gen(function* () {
190
+ if (entries.length > 64)
191
+ throw new RangeError("setMany accepts at most 64 rows");
192
+ for (const [key] of entries)
193
+ validateStorageKey(key);
194
+ const expiry = expiryOptions(opts);
195
+ if (!entries.length)
196
+ return;
197
+ yield* ensure();
198
+ const changes = yield* driver.batch([
199
+ expirySweep(),
200
+ ...entries.map(([key, value]) => sql(`INSERT INTO connecta_kv (key, value, expires_at_ms) VALUES (?, ?, ${expiry.expression})
201
+ ON CONFLICT (key) DO UPDATE SET value = excluded.value, expires_at_ms = excluded.expires_at_ms`, key, value, expiry.value)),
202
+ ]);
203
+ if (changes.length !== entries.length + 1 || changes.slice(1).some((changed) => changed !== 1))
204
+ throw new Error("SQL multi-set confirmation is incomplete");
205
+ });
206
+ },
160
207
  set(key, value, opts) {
161
208
  return Effect.gen(function* () {
162
209
  validateStorageKey(key);
@@ -334,11 +381,20 @@ function activityRow(read) {
334
381
  }
335
382
  return row;
336
383
  }
384
+ // Operation metadata shares the existing kind-tagged text slot; no new columns.
385
+ function parseOperation(raw) {
386
+ try {
387
+ return raw === null ? undefined : JSON.parse(raw);
388
+ }
389
+ catch {
390
+ return undefined;
391
+ }
392
+ }
337
393
  function rowToEvent(row) {
338
394
  // Rows written before `friction` had a column derive it from the code.
339
395
  // Friction without a code — an oversized but successful result — exists
340
396
  // only in the column, which is why the column exists.
341
- const friction = row.friction ?? agentFrictionForCode(row.error_code ?? undefined);
397
+ const friction = row.event_kind === "operation" ? undefined : (row.friction ?? agentFrictionForCode(row.error_code ?? undefined));
342
398
  const packageVersion = activityPackageVersion(row.package_version);
343
399
  const clientName = activityClientFact(row.client_name, "name");
344
400
  const clientVersion = activityClientFact(row.client_version, "version");
@@ -351,6 +407,7 @@ function rowToEvent(row) {
351
407
  classification: row.classification,
352
408
  resultBytes: row.result_bytes,
353
409
  kind: row.event_kind,
410
+ operation: row.event_kind === "operation" ? parseOperation(row.drift_kind) : undefined,
354
411
  pool: row.pool_name,
355
412
  actorBasis: row.actor_basis,
356
413
  drift: {
@@ -461,7 +518,7 @@ export function nativeSqlActivityStore(driver, kind, options = {}) {
461
518
  // Stored beside the code rather than derived from it: a truncated
462
519
  // result is friction on a call that succeeded, and
463
520
  // `error_code IS NOT NULL` stays an honest count of failures.
464
- event.friction ?? null, event.approval ?? null, activityPackageVersion(event.packageVersion) ?? null, event.serverName, event.serverVersion, activityClientFact(event.clientName, "name") ?? null, activityClientFact(event.clientVersion, "version") ?? null, event.deploymentId ?? null, facts.classification ?? null, facts.resultBytes ?? null, facts.kind ?? null, facts.drift?.kind ?? null, facts.drift?.addedTools ?? null, facts.drift?.removedTools ?? null, facts.drift?.changedTools ?? null, facts.pool ?? null, facts.actorBasis ?? null),
521
+ event.kind === "operation" ? null : (event.friction ?? null), event.approval ?? null, activityPackageVersion(event.packageVersion) ?? null, event.serverName, event.serverVersion, activityClientFact(event.clientName, "name") ?? null, activityClientFact(event.clientVersion, "version") ?? null, event.deploymentId ?? null, facts.classification ?? null, facts.resultBytes ?? null, facts.kind ?? null, facts.kind === "operation" ? JSON.stringify(facts.operation ?? null) : (facts.drift?.kind ?? null), facts.drift?.addedTools ?? null, facts.drift?.removedTools ?? null, facts.drift?.changedTools ?? null, facts.pool ?? null, facts.actorBasis ?? null),
465
522
  sql(`DELETE FROM tool_call_activity WHERE id IN (
466
523
  SELECT id FROM tool_call_activity
467
524
  WHERE occurred_at_ms < ?
@@ -10,6 +10,12 @@ export function sqlStorage(driver, kind) {
10
10
  const root = {};
11
11
  const native = {
12
12
  ...store,
13
+ setMany: (entries, options) => {
14
+ let write = store.setMany(entries, options);
15
+ for (const [key] of entries)
16
+ write = observedWrite(root, key, write);
17
+ return write;
18
+ },
13
19
  set: (key, value, options) => observedWrite(root, key, store.set(key, value, options)),
14
20
  delete: (key) => observedWrite(root, key, store.delete(key)),
15
21
  compareAndSet: (key, expected, next, options) => observedWrite(root, key, store.compareAndSet(key, expected, next, options)),
@@ -0,0 +1,84 @@
1
+ const families = new Set([
2
+ "catalog.cache.lookup",
3
+ "catalog.cache.hit",
4
+ "catalog.cache.expired",
5
+ "catalog.chunks",
6
+ "catalog.validated.hit",
7
+ "catalog.validated.miss",
8
+ "catalog.intake",
9
+ "catalog.decode",
10
+ "catalog.validate",
11
+ "catalog.accepted",
12
+ "catalog",
13
+ "admission",
14
+ "connector",
15
+ "result",
16
+ "d1.read",
17
+ "d1.write",
18
+ "d1.batch",
19
+ "memo.hit",
20
+ "memo.miss",
21
+ "memo.fresh",
22
+ ]);
23
+ const traces = new WeakMap();
24
+ let active = 0;
25
+ let firstRequest = true;
26
+ export const hasTimingTraces = () => active > 0;
27
+ export const traceFor = (identity) => identity ? traces.get(identity) : undefined;
28
+ const finite = (value) => (Number.isFinite(value) && value >= 0 ? value : 0);
29
+ export class TimingTrace {
30
+ first;
31
+ started = performance.now();
32
+ totals = new Map();
33
+ closed = false;
34
+ constructor(first) {
35
+ this.first = first;
36
+ }
37
+ record(family, durationMs = 0, bytes = 0, statements = 0, absent = 0, items = 0) {
38
+ if (this.closed || !families.has(family))
39
+ return;
40
+ const total = this.totals.get(family) ?? { count: 0, durationMs: 0, bytes: 0, statements: 0, absent: 0, items: 0 };
41
+ total.count++;
42
+ total.durationMs += finite(durationMs);
43
+ total.bytes += finite(bytes);
44
+ total.statements += finite(statements);
45
+ total.absent += finite(absent);
46
+ total.items += finite(items);
47
+ this.totals.set(family, total);
48
+ }
49
+ finish(logger) {
50
+ if (this.closed)
51
+ return;
52
+ this.closed = true;
53
+ try {
54
+ logger.info("connecta.timing", {
55
+ version: 1,
56
+ isolateFirstRequest: this.first,
57
+ durationMs: performance.now() - this.started,
58
+ weakReferences: typeof WeakRef === "function" && typeof FinalizationRegistry === "function",
59
+ operations: Object.fromEntries(this.totals),
60
+ });
61
+ }
62
+ catch {
63
+ /* Observability cannot change a request's outcome. */
64
+ }
65
+ this.totals.clear();
66
+ }
67
+ }
68
+ /** Called for each MCP request, including those not sampled. */
69
+ export function startTimingTrace(identity, sampleRate, logger) {
70
+ const first = firstRequest;
71
+ firstRequest = false;
72
+ if (sampleRate <= 0 || (sampleRate < 1 && Math.random() >= sampleRate))
73
+ return undefined;
74
+ const trace = new TimingTrace(first);
75
+ traces.set(identity, trace);
76
+ active++;
77
+ return () => {
78
+ if (traces.get(identity) !== trace)
79
+ return;
80
+ traces.delete(identity);
81
+ active--;
82
+ trace.finish(logger);
83
+ };
84
+ }
package/dist/types.d.ts CHANGED
@@ -842,6 +842,11 @@ export interface InboundAuth {
842
842
  kind: string;
843
843
  /** This provider may admit a human identity to operator mutation routes. */
844
844
  interactiveOperator?: true;
845
+ /**
846
+ * This provider checks credentials against the deployment's own URL and was
847
+ * given none, so construction refuses a configuration without `publicUrl`.
848
+ */
849
+ requiresPublicUrl?: true;
845
850
  /**
846
851
  * Recognize credential syntax or trusted runtime context, without verifying
847
852
  * it or doing I/O. The first recognizing provider owns the verdict, including