@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.
- package/CHANGELOG.md +99 -0
- package/README.md +126 -134
- package/bin/connecta.mjs +13 -2
- package/bin/version.mjs +1 -1
- package/dist/argument-redaction.js +230 -0
- package/dist/auth-elicitation.js +14 -9
- package/dist/authorization-handoff.js +111 -0
- package/dist/call-recovery.js +25 -0
- package/dist/catalog-cache.js +2 -2
- package/dist/catalog-service.js +103 -63
- package/dist/config-schema.d.ts +14 -7
- package/dist/config-schema.js +29 -10
- package/dist/config.d.ts +2 -0
- package/dist/config.js +12 -7
- package/dist/connectors/guarded-fetch.d.ts +9 -1
- package/dist/connectors/guarded-fetch.js +17 -5
- package/dist/connectors/negotiation-cache.js +1 -1
- package/dist/downstream-input-context.js +9 -0
- package/dist/downstream-input.js +13 -11
- package/dist/errors.d.ts +38 -12
- package/dist/errors.js +19 -9
- package/dist/execute.js +375 -43
- package/dist/executor-result.js +11 -9
- package/dist/executors/quickjs-child.js +12 -5
- package/dist/executors/quickjs-protocol.js +5 -6
- package/dist/executors/quickjs-runtime.js +6 -1
- package/dist/executors/quickjs.js +20 -4
- package/dist/guest-runtime.js +52 -2
- package/dist/guest-types.d.ts +47 -5
- package/dist/index.d.ts +1 -1
- package/dist/invocation.js +72 -14
- package/dist/meta-output.js +0 -15
- package/dist/meta-tools.js +166 -95
- package/dist/oauth-handoff.js +88 -11
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/view.d.ts +9 -0
- package/dist/operator-ui/view.js +11 -0
- package/dist/program-result.js +5 -0
- package/dist/program-writes.js +7 -48
- package/dist/provider.d.ts +8 -6
- package/dist/provider.js +3 -4
- package/dist/providers/_shared/google/workspace.d.ts +1 -1
- package/dist/providers/_shared/request-token.js +83 -0
- package/dist/providers/_shared/rest/dispatch.js +41 -0
- package/dist/providers/_shared/rest/operation-index.d.ts +133 -0
- package/dist/providers/_shared/rest/operation-index.js +527 -0
- package/dist/providers/_shared/rest/tools.d.ts +141 -0
- package/dist/providers/_shared/rest/tools.js +569 -0
- package/dist/providers/_shared/rest/value-safety.d.ts +151 -0
- package/dist/providers/_shared/rest/value-safety.js +580 -0
- package/dist/providers/cloudflare/graphql.js +385 -0
- package/dist/providers/cloudflare/index.d.ts +47 -51
- package/dist/providers/cloudflare/index.js +188 -1997
- package/dist/providers/cloudflare/openapi.generated.js +3347 -0
- package/dist/providers/cloudflare/rest.d.ts +37 -0
- package/dist/providers/cloudflare/rest.js +1056 -0
- package/dist/providers/cloudflare/skill.generated.js +7 -4
- package/dist/providers/cloudflare/value-safety.js +1247 -0
- package/dist/providers/infisical/index.d.ts +7 -0
- package/dist/providers/infisical/index.js +713 -0
- package/dist/providers/infisical/skill.generated.js +8 -0
- package/dist/providers/notion/index.d.ts +35 -31
- package/dist/providers/notion/index.js +60 -1499
- package/dist/providers/notion/openapi.generated.js +75 -0
- package/dist/providers/notion/rest.d.ts +24 -0
- package/dist/providers/notion/rest.js +1067 -0
- package/dist/providers/notion/skill.generated.js +5 -4
- package/dist/providers/notion/value-safety.js +31 -0
- package/dist/providers/stripe/index.d.ts +26 -20
- package/dist/providers/stripe/index.js +103 -130
- package/dist/providers/stripe/openapi.generated.js +649 -0
- package/dist/providers/stripe/rest.d.ts +14 -0
- package/dist/providers/stripe/rest.js +395 -0
- package/dist/providers/stripe/skill.generated.js +7 -5
- package/dist/providers/stripe/value-safety.js +705 -0
- package/dist/providers/vercel/index.d.ts +31 -22
- package/dist/providers/vercel/index.js +96 -853
- package/dist/providers/vercel/openapi.generated.js +456 -0
- package/dist/providers/vercel/rest.d.ts +20 -0
- package/dist/providers/vercel/rest.js +1002 -0
- package/dist/providers/vercel/skill.generated.js +4 -5
- package/dist/providers/vercel/value-safety.js +328 -0
- package/dist/registry.d.ts +8 -6
- package/dist/registry.js +134 -61
- package/dist/resource-read.js +4 -1
- package/dist/routes/mcp.js +11 -2
- package/dist/sent-secrets.d.ts +54 -13
- package/dist/sent-secrets.js +770 -231
- package/dist/skills.js +26 -16
- package/dist/sqlite.js +21 -6
- package/dist/storage/keys.js +20 -5
- package/dist/storage/memory.js +9 -4
- package/dist/storage/sql.js +18 -9
- package/dist/types.d.ts +17 -3
- package/dist/usage-guide.js +49 -40
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +2 -1
- package/dist/write-outcome.js +47 -0
- package/documentation/README.md +64 -0
- package/documentation/architecture.md +241 -97
- package/documentation/auth.md +138 -5
- package/documentation/code-mode.md +224 -107
- package/documentation/deploying.md +146 -0
- package/documentation/integrating.md +170 -0
- package/documentation/meta-tools.md +142 -30
- package/documentation/operating.md +135 -0
- package/documentation/operator-tests.md +4 -0
- package/documentation/operator-ui.md +132 -0
- package/documentation/provider-auth-migration.md +307 -0
- package/documentation/provider-migration-0.29.md +22 -5
- package/examples/worker/README.md +9 -2
- package/package.json +9 -2
- package/templates/node/README.md +9 -2
- package/templates/node/package.json +1 -1
- 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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
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
|
-
|
|
142
|
-
entry[2] > now);
|
|
144
|
+
Number.isSafeInteger(entry[2]));
|
|
143
145
|
}
|
|
144
|
-
/**
|
|
145
|
-
|
|
146
|
-
*
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
|
555
|
-
*
|
|
556
|
-
*
|
|
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
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
const
|
|
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
|
|
577
|
-
|
|
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 (
|
|
629
|
+
if (booking.status === "exhausted" || booking.result === undefined)
|
|
580
630
|
return false;
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
//
|
|
593
|
-
// a write
|
|
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
|
-
|
|
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
|
-
|
|
602
|
-
|
|
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();
|
package/dist/resource-read.js
CHANGED
|
@@ -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
|
-
|
|
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 } : {}),
|
package/dist/routes/mcp.js
CHANGED
|
@@ -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
|
-
(
|
|
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));
|
package/dist/sent-secrets.d.ts
CHANGED
|
@@ -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
|
-
|
|
6
|
-
private
|
|
7
|
-
private unicode;
|
|
5
|
+
/** Normalized form to its strongest kind. */
|
|
6
|
+
private readonly forms;
|
|
8
7
|
private readonly recipients;
|
|
9
|
-
private
|
|
10
|
-
/** The
|
|
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
|
-
/**
|
|
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.
|
|
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
|