@zackbart/connecta 0.25.0 → 0.26.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/AGENTS.md +6 -5
- package/CHANGELOG.md +271 -0
- package/README.md +32 -15
- package/SECURITY.md +6 -5
- package/bin/connecta.mjs +5 -1
- package/dist/activity.d.ts +10 -3
- package/dist/activity.js +10 -1
- package/dist/artifacts/connector.d.ts +55 -0
- package/dist/artifacts/connector.js +724 -0
- package/dist/artifacts/document.js +76 -0
- package/dist/artifacts/guide.js +140 -0
- package/dist/artifacts/html-scan.js +329 -0
- package/dist/artifacts/json.js +68 -0
- package/dist/artifacts/kv-store.d.ts +20 -0
- package/dist/artifacts/kv-store.js +140 -0
- package/dist/artifacts/markdown.js +449 -0
- package/dist/artifacts/module.d.ts +37 -0
- package/dist/artifacts/module.js +60 -0
- package/dist/artifacts/operations.d.ts +195 -0
- package/dist/artifacts/operations.js +944 -0
- package/dist/artifacts/refresh.d.ts +53 -0
- package/dist/artifacts/refresh.js +235 -0
- package/dist/artifacts/routes.js +194 -0
- package/dist/artifacts/types.d.ts +214 -0
- package/dist/artifacts/types.js +22 -0
- package/dist/artifacts/validate.d.ts +62 -0
- package/dist/artifacts/validate.js +652 -0
- package/dist/artifacts.d.ts +6 -0
- package/dist/artifacts.js +9 -0
- package/dist/catalog-service.js +39 -29
- package/dist/catalog.js +42 -8
- package/dist/connector-access.d.ts +18 -4
- package/dist/connector-access.js +65 -20
- package/dist/connector-caller.d.ts +8 -0
- package/dist/connector-caller.js +19 -0
- package/dist/connectors/remote-mcp.js +16 -2
- package/dist/errors.d.ts +16 -1
- package/dist/errors.js +28 -0
- package/dist/execute.js +427 -70
- package/dist/executor-admission.d.ts +11 -1
- package/dist/executor-admission.js +79 -14
- package/dist/executors/quickjs.js +4 -2
- package/dist/index.d.ts +62 -6
- package/dist/index.js +230 -6
- package/dist/invocation.js +56 -10
- package/dist/meta-tools.js +7 -2
- package/dist/module-contracts.d.ts +21 -1
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/model.d.ts +62 -5
- package/dist/operator-ui/view.js +94 -5
- package/dist/program-source.js +116 -0
- package/dist/registry.d.ts +42 -6
- package/dist/registry.js +148 -47
- package/dist/resumable.js +1291 -0
- package/dist/routes/activity.js +6 -69
- package/dist/routes/actor-labels.js +114 -0
- package/dist/routes/credentials.js +27 -5
- package/dist/routes/mcp.js +116 -59
- package/dist/routes/oauth-management.js +22 -9
- package/dist/routes/operator.js +1 -0
- package/dist/routes/shared.d.ts +23 -1
- package/dist/routes/shared.js +20 -2
- package/dist/routes/ui-data.js +2 -1
- package/dist/routes/ui.js +13 -4
- package/dist/run-journal.js +347 -0
- package/dist/runtime/shared-read.js +106 -0
- package/dist/server.js +26 -0
- package/dist/skills.js +27 -8
- package/dist/timeout.js +26 -0
- package/dist/tool-safety.d.ts +35 -0
- package/dist/tool-safety.js +31 -0
- package/dist/types.d.ts +18 -0
- package/dist/ui.d.ts +12 -5
- package/dist/ui.js +32 -6
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +196 -19
- package/documentation/auth.md +83 -2
- package/documentation/code-mode.md +221 -48
- package/documentation/meta-tools.md +44 -25
- package/ethos.md +12 -10
- package/examples/worker/AGENTS.md +6 -0
- package/examples/worker/README.md +174 -10
- package/examples/worker/src/d1-activity-row.ts +14 -2
- package/examples/worker/src/d1-activity.ts +4 -2
- package/examples/worker/src/index.ts +36 -1
- package/examples/worker/src/r2-artifact-blobs.ts +29 -0
- package/examples/worker/wrangler.jsonc +5 -1
- package/package.json +6 -2
- package/templates/node/AGENTS.md +9 -2
- package/templates/node/README.md +31 -3
- package/templates/node/package.json +1 -1
- package/templates/node/src/index.ts +27 -0
package/dist/registry.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Cause, Clock, Deferred, Effect, Exit, Result } from "effect";
|
|
1
|
+
import { Cause, Clock, Deferred, Duration, Effect, Exit, Option, Result, } from "effect";
|
|
2
2
|
import { storedCredentialShape, } from "./credential-rules.js";
|
|
3
3
|
import { ConnectorCallError, msg } from "./errors.js";
|
|
4
4
|
import { ConnectorCallAdmissionController, aggregateCallAdmissionSnapshots, } from "./call-admission.js";
|
|
@@ -8,10 +8,14 @@ import { MAX_CATALOG_CHUNK_BYTES, MAX_CATALOG_TOOLS, MAX_SERIALIZED_CATALOG_BYTE
|
|
|
8
8
|
import { ObservedOutputSchemas } from "./result-shapes.js";
|
|
9
9
|
import { GUIDE_SUMMARY_LENGTH, normalizeGuideSummary, } from "./skills.js";
|
|
10
10
|
import { attachOAuthSealer, vaultOAuthSealer } from "./oauth-sealing.js";
|
|
11
|
+
import { attachCaller } from "./connector-caller.js";
|
|
11
12
|
import { closeScopeOnExit } from "./runtime/connector-scope.js";
|
|
12
13
|
import { detach, runEdge, withDeadlineEffect } from "./runtime/run.js";
|
|
14
|
+
import { SharedRead } from "./runtime/shared-read.js";
|
|
13
15
|
import { Logger as LoggerService } from "./runtime/services.js";
|
|
14
16
|
import { runOnPartition, storageDelete, storageGet, storageSet, } from "./runtime/storage.js";
|
|
17
|
+
import { DEFAULT_PROBE_TIMEOUT_MS, normalizeTimeoutMs } from "./timeout.js";
|
|
18
|
+
import { isExplicitlyReadOnly } from "./tool-safety.js";
|
|
15
19
|
const ID_RE = /^[a-z0-9_-]+$/;
|
|
16
20
|
const DEFAULT_TTL_SECONDS = 300;
|
|
17
21
|
const DEFAULT_STALE_SECONDS = 3600;
|
|
@@ -69,6 +73,17 @@ export function resolveMaxResultBytes(value, inherited) {
|
|
|
69
73
|
? value
|
|
70
74
|
: inherited;
|
|
71
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* What a joiner hears from a flight that will not answer for it: one that
|
|
78
|
+
* passed its bound, or failed for a reason that was its owner's alone. It
|
|
79
|
+
* never reaches a caller; a joiner that hears it makes a fresh attempt.
|
|
80
|
+
*/
|
|
81
|
+
class AbandonedCatalogRefresh extends Error {
|
|
82
|
+
constructor(id) {
|
|
83
|
+
super(`The catalog refresh of "${id}" was abandoned.`);
|
|
84
|
+
this.name = "AbandonedCatalogRefresh";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
72
87
|
const CATALOG_CHUNK_IO_CONCURRENCY = 4;
|
|
73
88
|
/**
|
|
74
89
|
* Run `operation` over every chunk index or chunk with the fixed chunk I/O
|
|
@@ -129,7 +144,7 @@ export class Registry {
|
|
|
129
144
|
// completes once its storage work is done; see enqueueCatalogMutation.
|
|
130
145
|
/** Serialize persisted catalog set/delete operations within this isolate. */
|
|
131
146
|
catalogMutations = new Map();
|
|
132
|
-
/** Same-request cold loads share one
|
|
147
|
+
/** Same-request cold loads share one read without retaining the request.
|
|
133
148
|
* See documentation/architecture.md#the-two-lifetimes. */
|
|
134
149
|
requestCatalogLoads = new WeakMap();
|
|
135
150
|
/** One live refresh per connector across agent and operator requests. */
|
|
@@ -811,7 +826,7 @@ export class Registry {
|
|
|
811
826
|
Deferred.doneUnsafe(turn, Exit.void);
|
|
812
827
|
}
|
|
813
828
|
}
|
|
814
|
-
async refreshToolsWithContext(id, connector, ctx, skipPublicationWhenAborted = false) {
|
|
829
|
+
async refreshToolsWithContext(id, connector, ctx, skipPublicationWhenAborted = false, flight) {
|
|
815
830
|
const generation = this.catalogGeneration(id);
|
|
816
831
|
const tools = await connector.listTools(ctx);
|
|
817
832
|
// The listing a maintained proxy just served is also the only catalog
|
|
@@ -826,9 +841,10 @@ export class Registry {
|
|
|
826
841
|
if (skipPublicationWhenAborted && ctx.signal?.aborted)
|
|
827
842
|
return tools;
|
|
828
843
|
// The caller that began this refresh may still use its result, but a
|
|
829
|
-
// credential/OAuth change that landed while listTools was in flight
|
|
830
|
-
// the listing must not enter
|
|
831
|
-
|
|
844
|
+
// credential/OAuth change that landed while listTools was in flight, or a
|
|
845
|
+
// flight abandoned while it listed, means the listing must not enter
|
|
846
|
+
// either shared cache layer.
|
|
847
|
+
if (!this.mayPublish(id, generation, flight))
|
|
832
848
|
return tools;
|
|
833
849
|
if (tools.length > MAX_CATALOG_TOOLS) {
|
|
834
850
|
const message = `Connector "${id}" returned ${tools.length} tools, over the ` +
|
|
@@ -838,7 +854,7 @@ export class Registry {
|
|
|
838
854
|
}
|
|
839
855
|
const previous = this.cache.get(id);
|
|
840
856
|
const snapshot = await snapshotCatalog(tools);
|
|
841
|
-
if (
|
|
857
|
+
if (!this.mayPublish(id, generation, flight) ||
|
|
842
858
|
(skipPublicationWhenAborted && ctx.signal?.aborted)) {
|
|
843
859
|
return tools;
|
|
844
860
|
}
|
|
@@ -872,28 +888,88 @@ export class Registry {
|
|
|
872
888
|
return tools;
|
|
873
889
|
}
|
|
874
890
|
/**
|
|
875
|
-
*
|
|
876
|
-
*
|
|
877
|
-
*
|
|
891
|
+
* Whether a refresh may still enter the shared cache layers: its generation
|
|
892
|
+
* is current, and the flight it runs in has been neither abandoned nor
|
|
893
|
+
* outlived its bound. Asked at each publication point, since listing and
|
|
894
|
+
* snapshotting both yield, and a late result must never overwrite the one a
|
|
895
|
+
* fresh attempt published in the meantime.
|
|
896
|
+
*/
|
|
897
|
+
mayPublish(id, generation, flight) {
|
|
898
|
+
return (generation === this.catalogGeneration(id) &&
|
|
899
|
+
(!flight || (!flight.abandoned && Date.now() < flight.abandonAt)));
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Publish one shared refresh before starting its connector work. The first
|
|
903
|
+
* caller owns the scope and signal, and the flight lives `boundMs` from now
|
|
904
|
+
* at most; every later caller joins the result without gaining access to
|
|
905
|
+
* that context. A flight found past its bound is abandoned here, and this
|
|
906
|
+
* caller starts the fresh attempt.
|
|
878
907
|
*/
|
|
879
|
-
startCatalogRefresh(id, generation, work) {
|
|
908
|
+
startCatalogRefresh(id, generation, boundMs, work, ownerLeft = () => false) {
|
|
880
909
|
const existing = this.catalogRefreshes.get(id);
|
|
881
|
-
if (existing?.generation === generation)
|
|
882
|
-
|
|
910
|
+
if (existing?.generation === generation) {
|
|
911
|
+
if (Date.now() < existing.abandonAt)
|
|
912
|
+
return { flight: existing };
|
|
913
|
+
this.abandonCatalogRefresh(id, existing);
|
|
914
|
+
}
|
|
883
915
|
const flight = {
|
|
884
916
|
generation,
|
|
917
|
+
abandonAt: Date.now() + boundMs,
|
|
918
|
+
abandoned: false,
|
|
885
919
|
outcome: Deferred.makeUnsafe(),
|
|
886
920
|
};
|
|
887
921
|
this.catalogRefreshes.set(id, flight);
|
|
888
|
-
const owner = Effect.onExit(work, (exit) => Effect.sync(() => {
|
|
922
|
+
const owner = Effect.onExit(work(flight), (exit) => Effect.sync(() => {
|
|
889
923
|
if (this.catalogRefreshes.get(id) === flight) {
|
|
890
924
|
this.catalogRefreshes.delete(id);
|
|
891
925
|
}
|
|
926
|
+
// A failure that was the owner's alone is no answer for anyone
|
|
927
|
+
// else: its own cancellation (its signal, read here in its own
|
|
928
|
+
// request), or anything that arrives once the flight is past its
|
|
929
|
+
// bound. Joiners make a fresh attempt under their own deadlines.
|
|
930
|
+
const ownersAlone = Exit.isFailure(exit) &&
|
|
931
|
+
(flight.abandoned ||
|
|
932
|
+
Date.now() >= flight.abandonAt ||
|
|
933
|
+
ownerLeft());
|
|
892
934
|
// Last, because joined fibers resume inside this call.
|
|
893
|
-
Deferred.doneUnsafe(flight.outcome, exit);
|
|
935
|
+
Deferred.doneUnsafe(flight.outcome, ownersAlone ? Exit.fail(new AbandonedCatalogRefresh(id)) : exit);
|
|
894
936
|
}));
|
|
895
937
|
return { flight, owner };
|
|
896
938
|
}
|
|
939
|
+
/**
|
|
940
|
+
* Give up on a flight that outlived its bound. Later readers start a fresh
|
|
941
|
+
* attempt, its joiners are told to, and nothing its owner lists from now on
|
|
942
|
+
* reaches a cache. The owner's work is left alone: it belongs to another
|
|
943
|
+
* request, which nothing here may reach into, and it can no longer publish.
|
|
944
|
+
*/
|
|
945
|
+
abandonCatalogRefresh(id, flight) {
|
|
946
|
+
if (flight.abandoned)
|
|
947
|
+
return;
|
|
948
|
+
flight.abandoned = true;
|
|
949
|
+
if (this.catalogRefreshes.get(id) === flight) {
|
|
950
|
+
this.catalogRefreshes.delete(id);
|
|
951
|
+
}
|
|
952
|
+
this.opts.logger.warn(`[connecta] connector "${id}" catalog refresh outlived its bound; abandoning it for a fresh attempt.`);
|
|
953
|
+
Deferred.doneUnsafe(flight.outcome, Exit.fail(new AbandonedCatalogRefresh(id)));
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* Wait on another caller's flight, no longer than its bound. Succeeds with
|
|
957
|
+
* its tools, or with `undefined` when it will not answer for this caller —
|
|
958
|
+
* past its bound, when this waiter abandons it, or failed for its owner
|
|
959
|
+
* alone — so the caller makes a fresh attempt. The timer is this waiter's
|
|
960
|
+
* own: on workerd it is what keeps a request that waits on another alive,
|
|
961
|
+
* and bounded.
|
|
962
|
+
*/
|
|
963
|
+
joinCatalogRefresh(id, flight) {
|
|
964
|
+
return Effect.suspend(() => Deferred.await(flight.outcome).pipe(Effect.timeoutOption(Duration.millis(Math.max(0, flight.abandonAt - Date.now()))), Effect.flatMap((joined) => Option.isSome(joined)
|
|
965
|
+
? Effect.succeed(joined.value)
|
|
966
|
+
: Effect.sync(() => {
|
|
967
|
+
this.abandonCatalogRefresh(id, flight);
|
|
968
|
+
return undefined;
|
|
969
|
+
})), Effect.catch((error) => error instanceof AbandonedCatalogRefresh
|
|
970
|
+
? Effect.succeed(undefined)
|
|
971
|
+
: Effect.fail(error))));
|
|
972
|
+
}
|
|
897
973
|
/** Force a live listTools refresh and replace both catalog cache layers. */
|
|
898
974
|
async refreshTools(id, baseUrl, requestScope, callOptions = {}) {
|
|
899
975
|
const connector = this.connectors.get(id);
|
|
@@ -901,14 +977,27 @@ export class Registry {
|
|
|
901
977
|
throw new Error(`Unknown connector "${id}"`);
|
|
902
978
|
if (connector.staticTools)
|
|
903
979
|
return connector.staticTools;
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
980
|
+
// A flight lives as long as its owner will wait for it. Past that, the
|
|
981
|
+
// owner may have answered and gone, and on workerd its I/O went with it.
|
|
982
|
+
const boundMs = normalizeTimeoutMs(callOptions.timeoutMs) ?? DEFAULT_PROBE_TIMEOUT_MS;
|
|
983
|
+
for (;;) {
|
|
984
|
+
const { flight, owner } = this.startCatalogRefresh(id, this.catalogGeneration(id), boundMs, (flight) => Effect.tryPromise({
|
|
985
|
+
try: () => this.refreshToolsWithContext(id, connector, this.contextFor(id, baseUrl, requestScope, callOptions), false, flight),
|
|
986
|
+
catch: (error) => error,
|
|
987
|
+
}), () => callOptions.signal?.aborted === true);
|
|
988
|
+
// The owner lists in its own request. A joiner waits through an edge of
|
|
989
|
+
// its own, under its own signal, and whatever it does with the tools
|
|
990
|
+
// happens after that, back in its request (see "Effect inside" in
|
|
991
|
+
// documentation/architecture.md). A flight that will not answer for it
|
|
992
|
+
// sends it round again, to join a fresh attempt or to own one.
|
|
993
|
+
if (owner)
|
|
994
|
+
return runEdge(owner);
|
|
995
|
+
const joined = await runEdge(this.joinCatalogRefresh(id, flight), {
|
|
996
|
+
signal: callOptions.signal,
|
|
997
|
+
});
|
|
998
|
+
if (joined)
|
|
999
|
+
return joined;
|
|
1000
|
+
}
|
|
912
1001
|
}
|
|
913
1002
|
observeCatalogAccess(id, state) {
|
|
914
1003
|
this.catalogAccess.set(id, {
|
|
@@ -928,7 +1017,7 @@ export class Registry {
|
|
|
928
1017
|
// The refresh this request starts, if none of this generation is live.
|
|
929
1018
|
// Its scope is closed however it ends, deadline included, before the
|
|
930
1019
|
// flight completes.
|
|
931
|
-
const refresh = withDeadlineEffect((signal) => Effect.gen({ self: this }, function* () {
|
|
1020
|
+
const refresh = (flight) => withDeadlineEffect((signal) => Effect.gen({ self: this }, function* () {
|
|
932
1021
|
const current = this.cache.get(id);
|
|
933
1022
|
if (current && current.exp > Date.now())
|
|
934
1023
|
return current.tools;
|
|
@@ -942,7 +1031,7 @@ export class Registry {
|
|
|
942
1031
|
});
|
|
943
1032
|
yield* closeScopeOnExit(connector, ctx, defer);
|
|
944
1033
|
return yield* Effect.tryPromise({
|
|
945
|
-
try: () => this.refreshToolsWithContext(id, connector, ctx, true),
|
|
1034
|
+
try: () => this.refreshToolsWithContext(id, connector, ctx, true, flight),
|
|
946
1035
|
catch: (error) => error,
|
|
947
1036
|
});
|
|
948
1037
|
}), {
|
|
@@ -952,14 +1041,15 @@ export class Registry {
|
|
|
952
1041
|
const logFailure = (err) => {
|
|
953
1042
|
this.opts.logger.warn(`[connecta] connector "${id}" deferred catalog refresh failed: ${msg(err)}`);
|
|
954
1043
|
};
|
|
955
|
-
const { flight, owner } = this.startCatalogRefresh(id, expectedGeneration, Effect.scoped(refresh));
|
|
1044
|
+
const { flight, owner } = this.startCatalogRefresh(id, expectedGeneration, options.refreshTimeoutMs, (flight) => Effect.scoped(refresh(flight)));
|
|
956
1045
|
if (owner) {
|
|
957
1046
|
// Runs past this response, under the runtime's waitUntil below.
|
|
958
1047
|
flight.deferredTail = detach(Effect.catchCause(owner, (cause) => Effect.sync(() => logFailure(Cause.squash(cause)))));
|
|
959
1048
|
}
|
|
960
1049
|
// A stale reader that joins a blocking refresh logs its failure once for
|
|
961
|
-
// every reader after it, and waits through an edge of its own
|
|
962
|
-
|
|
1050
|
+
// every reader after it, and waits through an edge of its own, no longer
|
|
1051
|
+
// than the flight's bound.
|
|
1052
|
+
flight.deferredTail ??= runEdge(this.joinCatalogRefresh(id, flight)).then(() => { }, logFailure);
|
|
963
1053
|
try {
|
|
964
1054
|
defer(flight.deferredTail);
|
|
965
1055
|
}
|
|
@@ -1075,9 +1165,15 @@ export class Registry {
|
|
|
1075
1165
|
* WeakMap neither roots the request scope nor lets its connector context
|
|
1076
1166
|
* escape into another request; settled entries are also removed eagerly.
|
|
1077
1167
|
*
|
|
1168
|
+
* The read owns its signal, and each caller waits under its own: one that
|
|
1169
|
+
* is cancelled while others wait leaves with its own failure, and the read
|
|
1170
|
+
* is cancelled only when every caller has gone (src/runtime/shared-read.ts).
|
|
1171
|
+
*
|
|
1078
1172
|
* The deployment-wide flight below this layer coalesces the actual live
|
|
1079
|
-
* refresh across requests. Its
|
|
1080
|
-
* later callers join only its result, never its
|
|
1173
|
+
* refresh across requests. Its owner's context governs the listing, and its
|
|
1174
|
+
* owner's deadline bounds it; later callers join only its result, never its
|
|
1175
|
+
* request scope, and a failure that was the owner's alone sends them to a
|
|
1176
|
+
* fresh attempt rather than to them.
|
|
1081
1177
|
*/
|
|
1082
1178
|
async getTools(id, baseUrl, requestScope, callOptions = {}, readOptions) {
|
|
1083
1179
|
const connector = this.connectors.get(id);
|
|
@@ -1093,20 +1189,23 @@ export class Registry {
|
|
|
1093
1189
|
loads = new Map();
|
|
1094
1190
|
this.requestCatalogLoads.set(requestScope, loads);
|
|
1095
1191
|
}
|
|
1096
|
-
const
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1192
|
+
const requestLoads = loads;
|
|
1193
|
+
let load = requestLoads.get(id);
|
|
1194
|
+
if (!load) {
|
|
1195
|
+
const started = new SharedRead((signal) => this.loadTools(id, baseUrl, requestScope, { ...callOptions, signal }, readOptions),
|
|
1196
|
+
// Settled or cancelled, the read leaves the map before any caller
|
|
1197
|
+
// resumes, so the next one asks the caches afresh.
|
|
1198
|
+
() => {
|
|
1199
|
+
if (requestLoads.get(id) === started)
|
|
1200
|
+
requestLoads.delete(id);
|
|
1201
|
+
if (requestLoads.size === 0) {
|
|
1202
|
+
this.requestCatalogLoads.delete(requestScope);
|
|
1203
|
+
}
|
|
1204
|
+
});
|
|
1205
|
+
requestLoads.set(id, started);
|
|
1206
|
+
load = started;
|
|
1109
1207
|
}
|
|
1208
|
+
return runEdge(load.join(callOptions.signal));
|
|
1110
1209
|
}
|
|
1111
1210
|
async credentialDriftFor(id) {
|
|
1112
1211
|
const credential = this.connectors.get(id)?.credential;
|
|
@@ -1231,15 +1330,17 @@ class ScopedRegistryView {
|
|
|
1231
1330
|
throw new Error(`Unknown connector "${args[0]}"`);
|
|
1232
1331
|
}
|
|
1233
1332
|
const tools = await registry.getTools(...args);
|
|
1234
|
-
const
|
|
1333
|
+
const guarded = this.scope.guardedToolAccess?.get(args[0]);
|
|
1334
|
+
const granted = this.scope.toolAccess?.get(args[0]) ?? guarded;
|
|
1235
1335
|
if (!granted)
|
|
1236
1336
|
return tools;
|
|
1237
1337
|
// Every consumer — search, describe, call_tool, and a program's
|
|
1238
1338
|
// connecta.call — resolves through this list, so an ungranted tool is
|
|
1239
1339
|
// indistinguishable from one the connector never had.
|
|
1240
|
-
const visible = tools.filter((tool) => granted.has(tool.name)
|
|
1340
|
+
const visible = tools.filter((tool) => granted.has(tool.name) &&
|
|
1341
|
+
(!guarded?.has(tool.name) || isExplicitlyReadOnly(tool)));
|
|
1241
1342
|
if (visible.length < granted.size) {
|
|
1242
|
-
const present = new Set(
|
|
1343
|
+
const present = new Set(tools.map((tool) => tool.name));
|
|
1243
1344
|
for (const name of granted) {
|
|
1244
1345
|
if (!present.has(name))
|
|
1245
1346
|
this.root.noteAbsentGrant(args[0], name);
|
|
@@ -1251,7 +1352,7 @@ class ScopedRegistryView {
|
|
|
1251
1352
|
const registry = this.registryFor(args[0]);
|
|
1252
1353
|
if (!registry)
|
|
1253
1354
|
throw new Error(`Unknown connector "${args[0]}"`);
|
|
1254
|
-
return registry.contextFor(...args);
|
|
1355
|
+
return attachCaller(registry.contextFor(...args), this.scope.caller);
|
|
1255
1356
|
}
|
|
1256
1357
|
admitCall(...args) {
|
|
1257
1358
|
const registry = this.registryFor(args[0]);
|