@zackbart/connecta 0.29.0 → 0.31.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 (116) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/README.md +126 -134
  3. package/bin/connecta.mjs +13 -2
  4. package/bin/version.mjs +1 -1
  5. package/dist/argument-redaction.js +230 -0
  6. package/dist/auth-elicitation.js +14 -9
  7. package/dist/authorization-handoff.js +111 -0
  8. package/dist/call-recovery.js +25 -0
  9. package/dist/catalog-cache.js +2 -2
  10. package/dist/catalog-service.js +103 -63
  11. package/dist/config-schema.d.ts +14 -7
  12. package/dist/config-schema.js +29 -10
  13. package/dist/config.d.ts +2 -0
  14. package/dist/config.js +12 -7
  15. package/dist/connectors/guarded-fetch.d.ts +9 -1
  16. package/dist/connectors/guarded-fetch.js +17 -5
  17. package/dist/connectors/negotiation-cache.js +1 -1
  18. package/dist/downstream-input-context.js +9 -0
  19. package/dist/downstream-input.js +13 -11
  20. package/dist/errors.d.ts +38 -12
  21. package/dist/errors.js +19 -9
  22. package/dist/execute.js +375 -43
  23. package/dist/executor-result.js +11 -9
  24. package/dist/executors/quickjs-child.js +12 -5
  25. package/dist/executors/quickjs-protocol.js +5 -6
  26. package/dist/executors/quickjs-runtime.js +6 -1
  27. package/dist/executors/quickjs.js +20 -4
  28. package/dist/guest-runtime.js +52 -2
  29. package/dist/guest-types.d.ts +47 -5
  30. package/dist/index.d.ts +1 -1
  31. package/dist/invocation.js +72 -14
  32. package/dist/meta-output.js +0 -15
  33. package/dist/meta-tools.js +166 -95
  34. package/dist/oauth-handoff.js +88 -11
  35. package/dist/operator-ui/generated.js +2 -2
  36. package/dist/operator-ui/view.d.ts +9 -0
  37. package/dist/operator-ui/view.js +11 -0
  38. package/dist/program-result.js +5 -0
  39. package/dist/program-writes.js +7 -48
  40. package/dist/provider.d.ts +8 -6
  41. package/dist/provider.js +3 -4
  42. package/dist/providers/_shared/google/workspace.d.ts +1 -1
  43. package/dist/providers/_shared/request-token.js +83 -0
  44. package/dist/providers/_shared/rest/dispatch.js +41 -0
  45. package/dist/providers/_shared/rest/operation-index.d.ts +133 -0
  46. package/dist/providers/_shared/rest/operation-index.js +527 -0
  47. package/dist/providers/_shared/rest/tools.d.ts +141 -0
  48. package/dist/providers/_shared/rest/tools.js +569 -0
  49. package/dist/providers/_shared/rest/value-safety.d.ts +151 -0
  50. package/dist/providers/_shared/rest/value-safety.js +580 -0
  51. package/dist/providers/cloudflare/graphql.js +385 -0
  52. package/dist/providers/cloudflare/index.d.ts +47 -51
  53. package/dist/providers/cloudflare/index.js +188 -1997
  54. package/dist/providers/cloudflare/openapi.generated.js +3347 -0
  55. package/dist/providers/cloudflare/rest.d.ts +37 -0
  56. package/dist/providers/cloudflare/rest.js +1056 -0
  57. package/dist/providers/cloudflare/skill.generated.js +7 -4
  58. package/dist/providers/cloudflare/value-safety.js +1247 -0
  59. package/dist/providers/infisical/index.d.ts +7 -0
  60. package/dist/providers/infisical/index.js +713 -0
  61. package/dist/providers/infisical/skill.generated.js +8 -0
  62. package/dist/providers/notion/index.d.ts +35 -31
  63. package/dist/providers/notion/index.js +60 -1499
  64. package/dist/providers/notion/openapi.generated.js +75 -0
  65. package/dist/providers/notion/rest.d.ts +24 -0
  66. package/dist/providers/notion/rest.js +1067 -0
  67. package/dist/providers/notion/skill.generated.js +5 -4
  68. package/dist/providers/notion/value-safety.js +31 -0
  69. package/dist/providers/stripe/index.d.ts +26 -20
  70. package/dist/providers/stripe/index.js +103 -130
  71. package/dist/providers/stripe/openapi.generated.js +649 -0
  72. package/dist/providers/stripe/rest.d.ts +14 -0
  73. package/dist/providers/stripe/rest.js +395 -0
  74. package/dist/providers/stripe/skill.generated.js +7 -5
  75. package/dist/providers/stripe/value-safety.js +705 -0
  76. package/dist/providers/vercel/index.d.ts +31 -22
  77. package/dist/providers/vercel/index.js +96 -853
  78. package/dist/providers/vercel/openapi.generated.js +456 -0
  79. package/dist/providers/vercel/rest.d.ts +20 -0
  80. package/dist/providers/vercel/rest.js +1002 -0
  81. package/dist/providers/vercel/skill.generated.js +4 -5
  82. package/dist/providers/vercel/value-safety.js +328 -0
  83. package/dist/registry.d.ts +8 -6
  84. package/dist/registry.js +134 -61
  85. package/dist/resource-read.js +4 -1
  86. package/dist/routes/mcp.js +11 -2
  87. package/dist/sent-secrets.d.ts +54 -13
  88. package/dist/sent-secrets.js +770 -231
  89. package/dist/skills.js +26 -16
  90. package/dist/sqlite.js +21 -6
  91. package/dist/storage/keys.js +20 -5
  92. package/dist/storage/memory.js +9 -4
  93. package/dist/storage/sql.js +18 -9
  94. package/dist/types.d.ts +17 -3
  95. package/dist/usage-guide.js +49 -40
  96. package/dist/version.d.ts +1 -1
  97. package/dist/version.js +1 -1
  98. package/dist/worker.js +2 -1
  99. package/dist/write-outcome.js +47 -0
  100. package/documentation/README.md +64 -0
  101. package/documentation/architecture.md +241 -97
  102. package/documentation/auth.md +138 -5
  103. package/documentation/code-mode.md +224 -107
  104. package/documentation/deploying.md +146 -0
  105. package/documentation/integrating.md +170 -0
  106. package/documentation/meta-tools.md +142 -30
  107. package/documentation/operating.md +135 -0
  108. package/documentation/operator-tests.md +4 -0
  109. package/documentation/operator-ui.md +132 -0
  110. package/documentation/provider-auth-migration.md +307 -0
  111. package/documentation/provider-migration-0.29.md +22 -5
  112. package/examples/worker/README.md +9 -2
  113. package/package.json +9 -2
  114. package/templates/node/README.md +9 -2
  115. package/templates/node/package.json +1 -1
  116. package/dist/providers/vercel/mcp-ownership.js +0 -212
package/dist/registry.js CHANGED
@@ -19,7 +19,7 @@ import { attachCaller } from "./connector-caller.js";
19
19
  import { runEdge } from "./runtime/run.js";
20
20
  import { SharedRead } from "./runtime/shared-read.js";
21
21
  import { runOnPartition, storageCompareAndSet, storageDelete, storageGet, storageSet } from "./runtime/storage.js";
22
- import { jsonCodec, OAUTH_HANDOFF_TTL_SECONDS, oauthHandoffKeys, resultKeys, scopes, stashLedgerKeys, } from "./storage/keys.js";
22
+ import { jsonCodec, OAUTH_HANDOFF_TTL_SECONDS, oauthHandoffKeys, resultKeys, scopes, stashLedgerKeys, stashExpiryKeys, } from "./storage/keys.js";
23
23
  import { classifyTool } from "./tool-safety.js";
24
24
  const ID_RE = /^[a-z0-9_-]+$/;
25
25
  const DEFAULT_MAX_RESULT_BYTES = CONFIG_DEFAULTS.calls.maxResultBytes;
@@ -87,6 +87,7 @@ function frozenFacts(tools) {
87
87
  }
88
88
  function namespaced(storage, prefix) {
89
89
  return {
90
+ capabilities: storage.capabilities,
90
91
  get: (k) => storage.get(prefix + k),
91
92
  set: (k, v, o) => storage.set(prefix + k, v, o),
92
93
  delete: (k) => storage.delete(prefix + k),
@@ -108,19 +109,21 @@ const STASH_LEDGER_ATTEMPTS = 32;
108
109
  /** First backoff window after a lost swap; it doubles up to the cap. */
109
110
  const STASH_LEDGER_BACKOFF_MS = 4;
110
111
  const STASH_LEDGER_BACKOFF_CAP_MS = 250;
111
- /**
112
- * Slack past a stash's deadline before its charge leaves the ledger, and the
113
- * longest one chunk write may take. A chunk's TTL is what remains of the
114
- * deadline on the clock read before its write, and the store starts that TTL
115
- * no later than the write returns, so the chunk expires within the write's
116
- * duration of the deadline. A write slower than this fails the stash.
117
- */
118
- const STASH_LEDGER_GRACE_MS = 30_000;
112
+ /** Whole chunk-write phase, measured from booking, including the header. */
113
+ const STASH_WRITE_TIMEOUT_MS = 30_000;
114
+ /** Total cleanup/release or settlement budget, including ledger retries. */
115
+ const STASH_COMPLETION_TIMEOUT_MS = 15_000;
116
+ /** Stop waiting without starting background storage work after the deadline. */
117
+ function completeStash(effect) {
118
+ return effect.pipe(Effect.asVoid, Effect.timeoutOrElse({ duration: Duration.millis(STASH_COMPLETION_TIMEOUT_MS), orElse: () => Effect.void }),
119
+ // onError finalizers are otherwise uninterruptible, including their I/O.
120
+ Effect.interruptible);
121
+ }
119
122
  /**
120
123
  * Live charges in a stored ledger. Anything malformed reads as empty: the
121
124
  * ledger bounds an advisory cache, and the next successful swap replaces it.
122
125
  */
123
- function liveStashCharges(raw, now) {
126
+ function stashCharges(raw) {
124
127
  if (raw === null)
125
128
  return [];
126
129
  let parsed;
@@ -138,16 +141,36 @@ function liveStashCharges(raw, now) {
138
141
  typeof entry[0] === "string" &&
139
142
  Number.isSafeInteger(entry[1]) &&
140
143
  entry[1] >= 0 &&
141
- typeof entry[2] === "number" &&
142
- entry[2] > now);
144
+ Number.isSafeInteger(entry[2]));
143
145
  }
144
- /**
145
- * Rewrite the stash ledger by compare-and-set. `plan` sees the live charges
146
- * and the time it read them at, and answers its result with the entries to
147
- * store, or without them when there is nothing to write. Only `plan` refuses:
148
- * a lost swap backs off and plans again from a fresh read. Answers undefined
149
- * once the attempts run out.
150
- */
146
+ /** Caller time can nominate a charge for pruning, but only storage proves expiry. */
147
+ function liveStashCharges(raw, now) {
148
+ return Effect.gen(function* () {
149
+ const live = [];
150
+ const expired = new Map();
151
+ for (const entry of stashCharges(raw)) {
152
+ const deadline = entry[2];
153
+ if (deadline > now) {
154
+ live.push(entry);
155
+ continue;
156
+ }
157
+ let proven = expired.get(deadline);
158
+ if (proven === undefined) {
159
+ // A successful absolute-expiry write followed by an absent read proves
160
+ // storage has passed this deadline, even when the caller runs ahead.
161
+ // These probes expire at the deadline they check; no cleanup is needed.
162
+ const key = stashExpiryKeys.deadline(deadline);
163
+ yield* storageSet(key, "live", { expiresAtMs: deadline });
164
+ proven = (yield* storageGet(key)) === null;
165
+ expired.set(deadline, proven);
166
+ }
167
+ if (!proven)
168
+ live.push(entry);
169
+ }
170
+ return live;
171
+ });
172
+ }
173
+ /** Rewrite the ledger with bounded retries; every stored charge already expires. */
151
174
  function swapStashLedger(plan) {
152
175
  return Effect.gen(function* () {
153
176
  const ledger = stashLedgerKeys.ledger;
@@ -156,16 +179,34 @@ function swapStashLedger(plan) {
156
179
  const window = Math.min(STASH_LEDGER_BACKOFF_CAP_MS, STASH_LEDGER_BACKOFF_MS * 2 ** (attempt - 1));
157
180
  yield* Effect.sleep(Duration.millis(Math.floor((yield* Random.next) * window) + 1));
158
181
  }
182
+ const outcome = yield* Effect.gen(function* () {
183
+ const raw = yield* storageGet(ledger);
184
+ const now = yield* Clock.currentTimeMillis;
185
+ const planned = plan(yield* liveStashCharges(raw, now), now);
186
+ if (planned.entries !== undefined) {
187
+ const entries = planned.entries;
188
+ // The ledger row itself expires with its last charge. A delayed CAS
189
+ // cannot create an immortal row or restart any reservation's lifetime.
190
+ const next = entries.length ? jsonCodec.encode({ v: 1, entries }) : null;
191
+ const expiresAtMs = entries.reduce((latest, entry) => Math.max(latest, entry[2]), entries[0]?.[2] ?? now);
192
+ if (!(yield* storageCompareAndSet(ledger, raw, next, { expiresAtMs })))
193
+ return undefined;
194
+ }
195
+ return { status: "applied", result: planned.result };
196
+ }).pipe(Effect.catch(() => Effect.succeed(undefined)));
197
+ if (outcome !== undefined)
198
+ return outcome;
199
+ }
200
+ // The final CAS can commit and then throw too. Confirm a read-only plan
201
+ // once more, especially an already-booked reservation, without another CAS.
202
+ return yield* Effect.gen(function* () {
159
203
  const raw = yield* storageGet(ledger);
160
204
  const now = yield* Clock.currentTimeMillis;
161
- const planned = plan(liveStashCharges(raw, now), now);
162
- if (planned.entries === undefined)
163
- return planned.result;
164
- if (yield* storageCompareAndSet(ledger, raw, jsonCodec.encode({ v: 1, entries: planned.entries }))) {
165
- return planned.result;
166
- }
167
- }
168
- return undefined;
205
+ const planned = plan(yield* liveStashCharges(raw, now), now);
206
+ return planned.entries === undefined
207
+ ? { status: "applied", result: planned.result }
208
+ : { status: "exhausted" };
209
+ }).pipe(Effect.catch(() => Effect.succeed({ status: "exhausted" })));
169
210
  });
170
211
  }
171
212
  async function sha256Hex(value) {
@@ -551,9 +592,11 @@ export class Registry {
551
592
  * The bounds are the deployment's, not this isolate's. Every charge is a
552
593
  * row in one ledger record in storage, booked by compare-and-set before any
553
594
  * chunk is written, so every isolate and process sharing the store sees the
554
- * same entries and bytes. A charge leaves the ledger just after its
555
- * result's deadline, which every chunk's TTL ends by; the storage TTL
556
- * reclaims the rows themselves.
595
+ * same entries and bytes. A pending write keeps its finite charge reserved.
596
+ * Writes stop after 30 seconds; absolute chunk expiries cannot pass the
597
+ * booked deadline even on late commit. Rejected responses leave writes
598
+ * unconfirmed and retain their charge. Cleanup/release and settlement each
599
+ * stop waiting after 15 seconds. Storage TTL reclaims the rows.
557
600
  */
558
601
  stashResult(id, chunks, ttlSeconds, partition = scopes.results) {
559
602
  return runOnPartition(Effect.gen({ self: this }, function* () {
@@ -564,48 +607,78 @@ export class Registry {
564
607
  if (bytes > maxBytes || maxEntries === 0)
565
608
  return false;
566
609
  const keys = chunks.map((_, index) => partition + resultKeys.chunk(id, index));
567
- const charge = keys[0];
568
- // One deadline for the charge and every chunk under it. Writes take
569
- // time, so a chunk's TTL is whatever remains of the deadline when it is
570
- // written, never a fresh `ttlSeconds`, and no single write may take
571
- // longer than the grace: no chunk outlives the charge that bounds it.
572
- const deadline = yield* swapStashLedger((live, now) => {
610
+ // IDs belong to reservations, not reusable result IDs or subjects.
611
+ const charge = crypto.randomUUID();
612
+ const ttlMs = ttlSeconds * 1000;
613
+ if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0)
614
+ return false;
615
+ const booking = yield* swapStashLedger((live, now) => {
616
+ // A committed CAS with a lost response is already our booking, even
617
+ // at full capacity. Reuse its deadline instead of adding a second slot.
618
+ const own = live.find((entry) => entry[0] === charge);
619
+ if (own)
620
+ return { result: own[2] };
573
621
  const used = live.reduce((sum, entry) => sum + entry[1], 0);
574
622
  if (live.length >= maxEntries || used + bytes > maxBytes)
575
623
  return { result: undefined };
576
- const end = now + ttlSeconds * 1000;
577
- return { entries: [...live, [charge, bytes, end + STASH_LEDGER_GRACE_MS]], result: end };
624
+ const deadline = now + STASH_WRITE_TIMEOUT_MS + ttlMs;
625
+ if (!Number.isSafeInteger(deadline))
626
+ return { result: undefined };
627
+ return { entries: [...live, [charge, bytes, deadline]], result: deadline };
578
628
  });
579
- if (deadline === undefined)
629
+ if (booking.status === "exhausted" || booking.result === undefined)
580
630
  return false;
581
- // A failed write may still have persisted. Delete every key it could
582
- // have written and release the charge only when all of them are gone;
583
- // otherwise the charge stays booked until it expires, by which time the
584
- // storage TTL has removed whatever did land.
585
- const release = Effect.gen(function* () {
586
- for (const key of keys)
587
- yield* storageDelete(key);
588
- yield* swapStashLedger((live) => live.some((entry) => entry[0] === charge)
589
- ? { entries: live.filter((entry) => entry[0] !== charge), result: undefined }
590
- : { result: undefined });
591
- }).pipe(Effect.ignore);
592
- // Trailing chunks first: the header chunk is what makes an id readable, so
593
- // a write that fails midway leaves no envelope pointing at absent chunks.
631
+ const deadline = booking.result;
632
+ const writeDeadline = deadline - ttlMs;
633
+ const chunkExpiry = deadline - STASH_WRITE_TIMEOUT_MS;
634
+ const owns = (entry) => entry[0] === charge;
635
+ const settle = swapStashLedger((live) => live.some((entry) => owns(entry) && entry[2] > chunkExpiry)
636
+ ? {
637
+ entries: live.map((entry) => (owns(entry) ? [charge, bytes, Math.min(entry[2], chunkExpiry)] : entry)),
638
+ result: undefined,
639
+ }
640
+ : { result: undefined });
641
+ let pendingWrite = false;
642
+ // A rejected or timed-out driver call may still commit after deletion.
643
+ // Only a confirmed write response clears pendingWrite.
644
+ const release = completeStash(Effect.gen(function* () {
645
+ const deleted = yield* Effect.gen(function* () {
646
+ for (const key of keys)
647
+ yield* storageDelete(key);
648
+ return true;
649
+ }).pipe(Effect.catch(() => Effect.succeed(false)));
650
+ if (pendingWrite)
651
+ return;
652
+ if (deleted) {
653
+ const released = yield* swapStashLedger((live) => live.some(owns)
654
+ ? { entries: live.filter((entry) => !owns(entry)), result: undefined }
655
+ : { result: undefined });
656
+ if (released.status === "applied")
657
+ return;
658
+ }
659
+ yield* settle;
660
+ }));
661
+ // Trailing chunks first, then the header. The caller receives no result
662
+ // ID after failure or timeout, including a late header commit.
663
+ const remaining = Math.max(0, writeDeadline - (yield* Clock.currentTimeMillis));
594
664
  const written = yield* Effect.gen(function* () {
595
665
  for (let index = keys.length - 1; index >= 0; index--) {
596
666
  const before = yield* Clock.currentTimeMillis;
597
- const remaining = Math.floor((deadline - before) / 1000);
598
- // Zero would mean no expiry: a stash that outlasts its deadline fails.
599
- if (remaining < 1)
667
+ if (before >= writeDeadline || before >= chunkExpiry)
600
668
  return false;
601
- yield* storageSet(keys[index], chunks[index], { ttlSeconds: remaining });
602
- if ((yield* Clock.currentTimeMillis) - before > STASH_LEDGER_GRACE_MS)
669
+ pendingWrite = true;
670
+ yield* storageSet(keys[index], chunks[index], { expiresAtMs: chunkExpiry }).pipe(Effect.tap(() => Effect.sync(() => {
671
+ pendingWrite = false;
672
+ })));
673
+ if ((yield* Clock.currentTimeMillis) >= writeDeadline)
603
674
  return false;
604
675
  }
605
676
  return true;
606
- }).pipe(Effect.onError(() => release));
677
+ }).pipe(Effect.timeoutOrElse({ duration: Duration.millis(remaining), orElse: () => Effect.succeed(false) }), Effect.onError(() => release.pipe(Effect.ignore)));
607
678
  if (!written)
608
679
  yield* release;
680
+ else
681
+ yield* completeStash(settle);
609
682
  return written;
610
683
  }), this.opts);
611
684
  }
@@ -616,7 +689,7 @@ export class Registry {
616
689
  resultIdentity() {
617
690
  return { subject: null, principal: null, endpoint: "/mcp", origin: null };
618
691
  }
619
- recheckResultAccess(_address, _classification, _signal) {
692
+ recheckResultAccess(_address, _classification, _signal, _access) {
620
693
  return Promise.resolve(true);
621
694
  }
622
695
  resultsStorage() {
@@ -996,8 +1069,8 @@ class ScopedRegistryView {
996
1069
  origin: this.scope.origin ?? null,
997
1070
  };
998
1071
  }
999
- recheckResultAccess(address, classification, signal) {
1000
- return this.scope.currentResultAccess?.(address, classification, signal) ?? Promise.resolve(true);
1072
+ recheckResultAccess(address, classification, signal, access) {
1073
+ return this.scope.currentResultAccess?.(address, classification, signal, access) ?? Promise.resolve(true);
1001
1074
  }
1002
1075
  resultsStorage() {
1003
1076
  return this.scope.subjectKey ? this.root.scopedStorage(this.scope.subjectKey) : this.root.resultsStorage();
@@ -39,6 +39,7 @@ export function readResource(registry, raw, options) {
39
39
  return withDeadlineEffect((signal) => Effect.scoped(Effect.gen(function* () {
40
40
  const target = yield* Effect.try({ try: () => resourceTarget(registry, raw), catch: (error) => error });
41
41
  connector = target.connector;
42
+ options.onResolved?.(target.connector);
42
43
  // Await this Promise in the reader's request context, as invocation.ts does.
43
44
  yield* Effect.acquireRelease(Effect.suspend(() => {
44
45
  const pending = registry.admitCall(target.connector.id, {
@@ -50,7 +51,8 @@ export function readResource(registry, raw, options) {
50
51
  pending.then((permit) => permit.release(), () => { });
51
52
  })));
52
53
  }), (permit) => Effect.sync(() => permit.release()), { interruptible: true });
53
- return yield* timed((elapsed) => options.onConnectorTime?.(elapsed), Effect.tryPromise({
54
+ // Downstream contents enter the guest here, so they take the full payload rules.
55
+ const contents = yield* timed((elapsed) => options.onConnectorTime?.(elapsed), Effect.tryPromise({
54
56
  try: () => {
55
57
  if (signal.aborted)
56
58
  throw signal.reason;
@@ -66,6 +68,7 @@ export function readResource(registry, raw, options) {
66
68
  },
67
69
  catch: (error) => error,
68
70
  }));
71
+ return options.sentSecrets.redact(contents);
69
72
  })), {
70
73
  timeoutMs: options.timeoutMs,
71
74
  ...(options.signal ? { signal: options.signal } : {}),
@@ -675,7 +675,7 @@ export function createMcpRoute(opts) {
675
675
  ...(authz.principalKey ? { principalKey: authz.principalKey } : {}),
676
676
  endpoint: new URL(request.url).pathname,
677
677
  origin: request.headers.get("Origin"),
678
- currentResultAccess: async (address, classification, signal) => {
678
+ currentResultAccess: async (address, classification, signal, access) => {
679
679
  signal?.throwIfAborted();
680
680
  const current = await authorize(localRequest, baseUrl, opts.config.auth, runtimeContext, opts.config.identity);
681
681
  if (!current.ok) {
@@ -699,6 +699,13 @@ export function createMcpRoute(opts) {
699
699
  ...(current.subjectKey ? { subjectKey: current.subjectKey } : {}),
700
700
  ...(current.principalKey ? { principalKey: current.principalKey } : {}),
701
701
  });
702
+ if (classification === "write" && currentTrust !== "trusted")
703
+ return false;
704
+ if (address === undefined)
705
+ return true;
706
+ // Skill and resource content requires a whole grant; catalog metadata requires visibility.
707
+ if (!address.includes("."))
708
+ return Boolean(access === "catalog" ? view.getConnector(address) : view.getResourceConnector(address));
702
709
  const resolved = view.resolveAddress(address);
703
710
  if (!resolved)
704
711
  return false;
@@ -707,7 +714,9 @@ export function createMcpRoute(opts) {
707
714
  const tool = (await view.getTools(resolved.connector.id, baseUrl, requestScope, signal ? { signal } : {})).find((tool) => tool.name === resolved.toolName);
708
715
  signal?.throwIfAborted();
709
716
  return Boolean(tool &&
710
- ((classification === "read" && tool.classification === "read") || currentTrust === "trusted"));
717
+ (access === "catalog" ||
718
+ (classification === "read" && tool.classification === "read") ||
719
+ currentTrust === "trusted"));
711
720
  }
712
721
  finally {
713
722
  await closeConnectorScope(resolved.connector, view.contextFor(resolved.connector.id, baseUrl, requestScope), runtimeContext?.waitUntil?.bind(runtimeContext));
@@ -1,30 +1,69 @@
1
- import type { ConnectorContext, Logger } from "./types.js";
1
+ 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
- private readonly values;
6
- private matcher;
7
- private unicode;
5
+ /** Normalized form to its strongest kind. */
6
+ private readonly forms;
8
7
  private readonly recipients;
9
- private form;
10
- /** The request receives existing and future credentials from every context. */
8
+ private privateChars;
9
+ /** The shortest form left unregistered; any string this long might contain it. */
10
+ private unmatched;
11
+ private unicode;
12
+ private views;
13
+ private store;
14
+ private unregistered;
15
+ private register;
16
+ /** The request receives existing and future secrets from every context. */
11
17
  include(source: SentSecrets): void;
12
18
  add(value: string): void;
13
19
  /** Explicit secrets use the same floor as every other credential. */
14
20
  secret(value: string): void;
21
+ /** Submitted writeOnly strings and numbers join this request's set, including property
22
+ * names inside a private object. Empty strings, booleans, and null carry no text. */
23
+ arguments(args: unknown, schema: JsonSchema | undefined): void;
15
24
  /** Structural fields must be refused, never repaired into different URLs. */
16
25
  contains(value: string, ignoreCase?: boolean): boolean;
17
26
  containsUrl(value: string): boolean;
18
27
  header(value: string): void;
19
28
  request(input: RequestInfo | URL, init?: RequestInit): void;
29
+ /** Serialized wire text about to be returned, stored, or paged, under payload rules. */
20
30
  text(value: string): string;
21
- /** Skill supporting files may contain arbitrary bytes, including credential echoes. */
31
+ /** Copy only what changes, including non-enumerable Error fields; never retain a raw cause.
32
+ * Payload rules apply where downstream or guest data enters; envelopes repeat literal
33
+ * replacement. */
34
+ redact<T>(value: T, mode?: "payload" | "prose" | "envelope"): T;
35
+ /** Credentials alone: later calls keep the private arguments they submit, and cached
36
+ * catalogs never depend on one request's arguments. */
37
+ redactCredentials<T>(value: T): T;
38
+ private idle;
39
+ private scan;
40
+ private matcher;
41
+ private credentials;
42
+ /** Strings at least this long might hold a form that was never matched. */
43
+ private floor;
44
+ /** Every raw and JSON-unescaped match, mapped to source spans before any replacement. Text
45
+ * decodes one more layer while the last one could still hold an escape, up to
46
+ * MAX_DECODE_LEVELS counting the JSON strings it came from. Work beyond the call's budget
47
+ * or that depth returns undefined, which withholds the string. */
48
+ private find;
49
+ /** A structured leaf, property name, or prose. A short private form withholds prose
50
+ * wherever it occurs, and a leaf only when the whole leaf is that form. */
51
+ private string;
52
+ /** A number, boolean, or null matching by its canonical text becomes a placeholder string. */
53
+ private scalar;
54
+ /** JSON objects, arrays, and string literals are parsed and walked, nested JSON strings
55
+ * included; their serialized bytes must then hold no literal the walk cannot see, such as
56
+ * one spanning tokens. Other text is prose or a structured leaf. */
57
+ private wire;
58
+ /** Iterative and copy-on-write: only containers on a changed path are copied. A container's
59
+ * entries are counted against the node budget before it is walked; a cycle or a container
60
+ * past MAX_WALK_DEPTH is withheld, and one past the node budget is checked as serialized text. */
61
+ private walk;
62
+ /** A container larger than the node budget is scanned as serialized text, Error fields
63
+ * included: kept unchanged when nothing registered appears, otherwise withheld whole. */
64
+ private serialized;
65
+ /** Skill supporting files may contain arbitrary bytes, including echoes. */
22
66
  private blob;
23
- /** Decode bounded windows with source spans, including twice-escaped JSON.
24
- * Scratch space follows credential length, never the whole skill file. */
25
- private escapedText;
26
- /** Copy, including non-enumerable Error fields; never retain a raw cause. */
27
- redact<T>(value: T): T;
28
67
  /** Match across blocks before isError/JSON unwrapping concatenates them. */
29
68
  private joinedContent;
30
69
  }
@@ -33,7 +72,9 @@ export declare function sentSecretsFor(ctx: ConnectorContext): SentSecrets;
33
72
  export declare function sentSecretsForRequest(scope: object, secrets?: SentSecrets): SentSecrets;
34
73
  /** The agent-facing choke point. Apply to values before bridge serialization
35
74
  * and to serialized wire text, so JSON escapes and structured strings share
36
- * the same rule. Intake redaction protects caches independently of this edge. */
75
+ * the same rule. Payloads were redacted where they entered; this boundary
76
+ * repeats credential and literal private-value replacement around Connecta's
77
+ * own envelopes, whose identifiers and counts are never private data. */
37
78
  export declare function redactAgentOutput<T>(secrets: SentSecrets, value: T): T;
38
79
  /** Wrap the complete operation table, including rejections. Adding an operation
39
80
  * cannot add an unredacted exit. A direct invocation owns a fresh scope unless