@zackbart/connecta 0.7.1 → 0.7.3
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 +72 -0
- package/README.md +2 -1
- package/dist/connectors/remote-mcp.d.ts.map +1 -1
- package/dist/connectors/remote-mcp.js +117 -79
- package/dist/connectors/remote-mcp.js.map +1 -1
- package/dist/credential-health.d.ts +8 -5
- package/dist/credential-health.d.ts.map +1 -1
- package/dist/credential-health.js +20 -13
- package/dist/credential-health.js.map +1 -1
- package/dist/executors/quickjs.d.ts.map +1 -1
- package/dist/executors/quickjs.js +57 -5
- package/dist/executors/quickjs.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +47 -11
- package/dist/meta-tools.js.map +1 -1
- package/dist/registry.d.ts +18 -22
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +33 -21
- package/dist/registry.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +9 -6
- package/dist/server.js.map +1 -1
- package/dist/timeout.d.ts +9 -4
- package/dist/timeout.d.ts.map +1 -1
- package/dist/timeout.js +34 -4
- package/dist/timeout.js.map +1 -1
- package/dist/toolkits.d.ts +8 -0
- package/dist/toolkits.d.ts.map +1 -1
- package/dist/toolkits.js +3 -0
- package/dist/toolkits.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/ui.d.ts +12 -1
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +187 -6
- package/dist/ui.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/connectors/remote-mcp.ts +128 -83
- package/src/credential-health.ts +20 -18
- package/src/executors/quickjs.ts +65 -5
- package/src/index.ts +2 -1
- package/src/meta-tools.ts +75 -26
- package/src/registry.ts +48 -20
- package/src/server.ts +11 -8
- package/src/timeout.ts +41 -4
- package/src/toolkits.ts +11 -0
- package/src/types.ts +2 -2
- package/src/ui.ts +212 -11
- package/src/version.ts +1 -1
package/src/meta-tools.ts
CHANGED
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
import {
|
|
31
31
|
DEFAULT_PROBE_TIMEOUT_MS,
|
|
32
32
|
normalizeTimeoutMs,
|
|
33
|
-
|
|
33
|
+
withAbortableTimeout,
|
|
34
34
|
} from "./timeout.js";
|
|
35
35
|
import { credentialVerdictApplies } from "./credential-health.js";
|
|
36
36
|
import type { ConnectorStatus, KVStorage, ToolDef } from "./types.js";
|
|
@@ -597,6 +597,18 @@ export function createMetaTools(
|
|
|
597
597
|
// identity lets remote connectors reuse one downstream client inside that
|
|
598
598
|
// request without leaking request-bound I/O into the next one.
|
|
599
599
|
const requestScope = {};
|
|
600
|
+
const withProbeDeadline = <T>(
|
|
601
|
+
label: string,
|
|
602
|
+
operation: (options: {
|
|
603
|
+
signal: AbortSignal;
|
|
604
|
+
timeoutMs: number;
|
|
605
|
+
}) => Promise<T>,
|
|
606
|
+
) =>
|
|
607
|
+
withAbortableTimeout(
|
|
608
|
+
(signal) => operation({ signal, timeoutMs: probeTimeoutMs }),
|
|
609
|
+
probeTimeoutMs,
|
|
610
|
+
label,
|
|
611
|
+
);
|
|
600
612
|
|
|
601
613
|
interface RunCallOutcome {
|
|
602
614
|
toolResult: ToolResult;
|
|
@@ -907,8 +919,8 @@ export function createMetaTools(
|
|
|
907
919
|
// call scope. Closing it cannot defeat call_tool/batch/execute_code reuse.
|
|
908
920
|
const connectors = registry.listConnectors();
|
|
909
921
|
const scope = probe ? {} : requestScope;
|
|
910
|
-
const
|
|
911
|
-
|
|
922
|
+
const pending = connectors.map(
|
|
923
|
+
async (c) => {
|
|
912
924
|
const statusStarted = Date.now();
|
|
913
925
|
const observed = registry.healthFor(c.id);
|
|
914
926
|
const verdict = await registry.credentialHealthFor(c.id);
|
|
@@ -917,10 +929,10 @@ export function createMetaTools(
|
|
|
917
929
|
| { state: "ok" | "error" | "unknown"; message?: string };
|
|
918
930
|
if (probe) {
|
|
919
931
|
try {
|
|
920
|
-
status = await
|
|
921
|
-
registry.statusFor(c.id, baseUrl, scope),
|
|
922
|
-
probeTimeoutMs,
|
|
932
|
+
status = await withProbeDeadline(
|
|
923
933
|
`list_connectors probe of "${c.id}"`,
|
|
934
|
+
(options) =>
|
|
935
|
+
registry.statusFor(c.id, baseUrl, scope, options),
|
|
924
936
|
);
|
|
925
937
|
} catch (err) {
|
|
926
938
|
// A probe that outran probeTimeoutMs (or otherwise threw)
|
|
@@ -1004,14 +1016,45 @@ export function createMetaTools(
|
|
|
1004
1016
|
// the first (now stale) authorization URL.
|
|
1005
1017
|
if (probe && status.state === "ok") {
|
|
1006
1018
|
try {
|
|
1007
|
-
tools = await
|
|
1008
|
-
registry.refreshTools(c.id, baseUrl, scope),
|
|
1009
|
-
probeTimeoutMs,
|
|
1019
|
+
tools = await withProbeDeadline(
|
|
1010
1020
|
`list_connectors catalog refresh of "${c.id}"`,
|
|
1021
|
+
(options) =>
|
|
1022
|
+
registry.refreshTools(c.id, baseUrl, scope, options),
|
|
1011
1023
|
);
|
|
1012
1024
|
registry.recordSuccess(c.id, Date.now() - statusStarted);
|
|
1013
1025
|
} catch (err) {
|
|
1014
|
-
|
|
1026
|
+
const details = classifyCallError(err);
|
|
1027
|
+
if (details.code === "auth_required") {
|
|
1028
|
+
let authStatus: ConnectorStatus | undefined;
|
|
1029
|
+
try {
|
|
1030
|
+
authStatus = await withProbeDeadline(
|
|
1031
|
+
`list_connectors authorization status of "${c.id}"`,
|
|
1032
|
+
(options) =>
|
|
1033
|
+
registry.statusFor(c.id, baseUrl, scope, options),
|
|
1034
|
+
);
|
|
1035
|
+
} catch {
|
|
1036
|
+
// The typed auth verdict is still authoritative; this second
|
|
1037
|
+
// read exists only to recover the connector's pending URL.
|
|
1038
|
+
}
|
|
1039
|
+
status =
|
|
1040
|
+
authStatus?.state === "auth_required"
|
|
1041
|
+
? authStatus
|
|
1042
|
+
: {
|
|
1043
|
+
state: "auth_required" as const,
|
|
1044
|
+
message: details.message,
|
|
1045
|
+
};
|
|
1046
|
+
await registry.recordCredentialHealth(c.id, {
|
|
1047
|
+
state: "auth_required",
|
|
1048
|
+
checkedAt,
|
|
1049
|
+
...(status.message ? { message: status.message } : {}),
|
|
1050
|
+
...("authorizationUrl" in status &&
|
|
1051
|
+
status.authorizationUrl
|
|
1052
|
+
? { authorizationUrl: status.authorizationUrl }
|
|
1053
|
+
: {}),
|
|
1054
|
+
});
|
|
1055
|
+
} else {
|
|
1056
|
+
status = { state: "error" as const, message: msg(err) };
|
|
1057
|
+
}
|
|
1015
1058
|
registry.recordFailure(c.id, Date.now() - statusStarted, err);
|
|
1016
1059
|
}
|
|
1017
1060
|
}
|
|
@@ -1036,17 +1079,23 @@ export function createMetaTools(
|
|
|
1036
1079
|
: {}),
|
|
1037
1080
|
...(status.message ? { message: status.message } : {}),
|
|
1038
1081
|
};
|
|
1039
|
-
}
|
|
1040
|
-
)
|
|
1041
|
-
|
|
1042
|
-
await Promise.all(
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1082
|
+
},
|
|
1083
|
+
);
|
|
1084
|
+
if (!probe) {
|
|
1085
|
+
return jsonResult({ connectors: await Promise.all(pending) });
|
|
1086
|
+
}
|
|
1087
|
+
const settled = await Promise.allSettled(pending);
|
|
1088
|
+
await Promise.all(
|
|
1089
|
+
connectors.map((connector) =>
|
|
1090
|
+
closeConnectorScope(
|
|
1091
|
+
connector,
|
|
1092
|
+
registry.contextFor(connector.id, baseUrl, scope),
|
|
1048
1093
|
),
|
|
1049
|
-
)
|
|
1094
|
+
),
|
|
1095
|
+
);
|
|
1096
|
+
const out = settled.map((result) => {
|
|
1097
|
+
if (result.status === "rejected") throw result.reason;
|
|
1098
|
+
return result.value;
|
|
1050
1099
|
});
|
|
1051
1100
|
return jsonResult({ connectors: out });
|
|
1052
1101
|
},
|
|
@@ -1079,10 +1128,10 @@ export function createMetaTools(
|
|
|
1079
1128
|
}> = [];
|
|
1080
1129
|
const catalogs = await Promise.allSettled(
|
|
1081
1130
|
conns.map((c) =>
|
|
1082
|
-
|
|
1083
|
-
registry.getTools(c.id, baseUrl, requestScope),
|
|
1084
|
-
probeTimeoutMs,
|
|
1131
|
+
withProbeDeadline(
|
|
1085
1132
|
`search_tools probe of "${c.id}"`,
|
|
1133
|
+
(options) =>
|
|
1134
|
+
registry.getTools(c.id, baseUrl, requestScope, options),
|
|
1086
1135
|
),
|
|
1087
1136
|
),
|
|
1088
1137
|
);
|
|
@@ -1208,10 +1257,10 @@ export function createMetaTools(
|
|
|
1208
1257
|
];
|
|
1209
1258
|
const loaded = await Promise.allSettled(
|
|
1210
1259
|
connectorIds.map((id) =>
|
|
1211
|
-
|
|
1212
|
-
registry.getTools(id, baseUrl, requestScope),
|
|
1213
|
-
probeTimeoutMs,
|
|
1260
|
+
withProbeDeadline(
|
|
1214
1261
|
`describe_tools probe of "${id}"`,
|
|
1262
|
+
(options) =>
|
|
1263
|
+
registry.getTools(id, baseUrl, requestScope, options),
|
|
1215
1264
|
),
|
|
1216
1265
|
),
|
|
1217
1266
|
);
|
package/src/registry.ts
CHANGED
|
@@ -6,7 +6,11 @@ import type {
|
|
|
6
6
|
Logger,
|
|
7
7
|
ToolDef,
|
|
8
8
|
} from "./types.js";
|
|
9
|
-
import
|
|
9
|
+
import {
|
|
10
|
+
storedCredentialShape,
|
|
11
|
+
type CredentialVault,
|
|
12
|
+
} from "./credentials.js";
|
|
13
|
+
import { ConnectorCallError } from "./errors.js";
|
|
10
14
|
import {
|
|
11
15
|
CredentialHealthChecker,
|
|
12
16
|
type CredentialCheckOptions,
|
|
@@ -154,6 +158,11 @@ function msg(err: unknown): string {
|
|
|
154
158
|
return err instanceof Error ? err.message : String(err);
|
|
155
159
|
}
|
|
156
160
|
|
|
161
|
+
type ConnectorOperationOptions = Pick<
|
|
162
|
+
ConnectorContext,
|
|
163
|
+
"signal" | "timeoutMs"
|
|
164
|
+
>;
|
|
165
|
+
|
|
157
166
|
/**
|
|
158
167
|
* The registry surface a per-connection MCP server consumes: every meta-tool
|
|
159
168
|
* (`src/meta-tools.ts`) and the `execute_code` sandbox bridge (`src/execute.ts`)
|
|
@@ -178,18 +187,20 @@ export interface RegistryView {
|
|
|
178
187
|
id: string,
|
|
179
188
|
baseUrl: string,
|
|
180
189
|
requestScope?: object,
|
|
190
|
+
callOptions?: ConnectorOperationOptions,
|
|
181
191
|
): Promise<ToolDef[]>;
|
|
182
192
|
refreshTools(
|
|
183
193
|
id: string,
|
|
184
194
|
baseUrl: string,
|
|
185
195
|
requestScope?: object,
|
|
196
|
+
callOptions?: ConnectorOperationOptions,
|
|
186
197
|
): Promise<ToolDef[]>;
|
|
187
198
|
peekTools(id: string): ToolDef[] | undefined;
|
|
188
199
|
contextFor(
|
|
189
200
|
id: string,
|
|
190
201
|
baseUrl: string,
|
|
191
202
|
requestScope?: object,
|
|
192
|
-
callOptions?:
|
|
203
|
+
callOptions?: ConnectorOperationOptions,
|
|
193
204
|
): ConnectorContext;
|
|
194
205
|
resultsStorage(): KVStorage;
|
|
195
206
|
recordSuccess(id: string, latencyMs: number): void;
|
|
@@ -209,6 +220,7 @@ export interface RegistryView {
|
|
|
209
220
|
id: string,
|
|
210
221
|
baseUrl: string,
|
|
211
222
|
requestScope?: object,
|
|
223
|
+
callOptions?: ConnectorOperationOptions,
|
|
212
224
|
): Promise<ConnectorStatus>;
|
|
213
225
|
invalidateStored(id: string): Promise<void>;
|
|
214
226
|
}
|
|
@@ -352,21 +364,31 @@ export class Registry implements RegistryView {
|
|
|
352
364
|
id: string,
|
|
353
365
|
baseUrl: string,
|
|
354
366
|
requestScope: object = {},
|
|
355
|
-
callOptions:
|
|
367
|
+
callOptions: ConnectorOperationOptions = {},
|
|
356
368
|
): ConnectorContext {
|
|
369
|
+
const credentialConfig = this.connectors.get(id)?.credential;
|
|
370
|
+
let credentialAccess: ConnectorContext["credential"];
|
|
371
|
+
if (this.opts.credentialVault && credentialConfig) {
|
|
372
|
+
const vault = this.opts.credentialVault;
|
|
373
|
+
const readValues = async () => {
|
|
374
|
+
const values = await vault.getAll(id);
|
|
375
|
+
const shape = storedCredentialShape(credentialConfig, values);
|
|
376
|
+
if (shape.state === "mismatch") {
|
|
377
|
+
throw new ConnectorCallError("auth_required", shape.message);
|
|
378
|
+
}
|
|
379
|
+
return values;
|
|
380
|
+
};
|
|
381
|
+
credentialAccess = {
|
|
382
|
+
get: async (field = "value") =>
|
|
383
|
+
(await readValues())?.[field] ?? null,
|
|
384
|
+
getAll: readValues,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
357
387
|
return {
|
|
358
388
|
storage: namespaced(this.opts.storage, `conn:${id}:`),
|
|
359
389
|
logger: this.opts.logger,
|
|
360
390
|
baseUrl,
|
|
361
|
-
...(
|
|
362
|
-
? {
|
|
363
|
-
credential: {
|
|
364
|
-
get: (field?: string) =>
|
|
365
|
-
this.opts.credentialVault!.get(id, field),
|
|
366
|
-
getAll: () => this.opts.credentialVault!.getAll(id),
|
|
367
|
-
},
|
|
368
|
-
}
|
|
369
|
-
: {}),
|
|
391
|
+
...(credentialAccess ? { credential: credentialAccess } : {}),
|
|
370
392
|
requestScope,
|
|
371
393
|
...callOptions,
|
|
372
394
|
};
|
|
@@ -441,13 +463,14 @@ export class Registry implements RegistryView {
|
|
|
441
463
|
id: string,
|
|
442
464
|
baseUrl: string,
|
|
443
465
|
requestScope?: object,
|
|
466
|
+
callOptions: ConnectorOperationOptions = {},
|
|
444
467
|
): Promise<ToolDef[]> {
|
|
445
468
|
const connector = this.connectors.get(id);
|
|
446
469
|
if (!connector) throw new Error(`Unknown connector "${id}"`);
|
|
447
470
|
const tools = connector.staticTools
|
|
448
471
|
? connector.staticTools
|
|
449
472
|
: await connector.listTools(
|
|
450
|
-
this.contextFor(id, baseUrl, requestScope),
|
|
473
|
+
this.contextFor(id, baseUrl, requestScope, callOptions),
|
|
451
474
|
);
|
|
452
475
|
const now = Date.now();
|
|
453
476
|
const previous = this.cache.get(id);
|
|
@@ -479,6 +502,7 @@ export class Registry implements RegistryView {
|
|
|
479
502
|
id: string,
|
|
480
503
|
baseUrl: string,
|
|
481
504
|
requestScope?: object,
|
|
505
|
+
callOptions: ConnectorOperationOptions = {},
|
|
482
506
|
): Promise<ToolDef[]> {
|
|
483
507
|
const connector = this.connectors.get(id);
|
|
484
508
|
if (!connector) throw new Error(`Unknown connector "${id}"`);
|
|
@@ -512,7 +536,7 @@ export class Registry implements RegistryView {
|
|
|
512
536
|
}
|
|
513
537
|
|
|
514
538
|
try {
|
|
515
|
-
return await this.refreshTools(id, baseUrl, requestScope);
|
|
539
|
+
return await this.refreshTools(id, baseUrl, requestScope, callOptions);
|
|
516
540
|
} catch (err) {
|
|
517
541
|
if (stale) {
|
|
518
542
|
this.opts.logger.warn(
|
|
@@ -626,10 +650,11 @@ export class Registry implements RegistryView {
|
|
|
626
650
|
id: string,
|
|
627
651
|
baseUrl: string,
|
|
628
652
|
requestScope: object = {},
|
|
653
|
+
callOptions: ConnectorOperationOptions = {},
|
|
629
654
|
): Promise<ConnectorStatus> {
|
|
630
655
|
const connector = this.connectors.get(id);
|
|
631
656
|
if (!connector) return { state: "error", message: "Unknown connector" };
|
|
632
|
-
const ctx = this.contextFor(id, baseUrl, requestScope);
|
|
657
|
+
const ctx = this.contextFor(id, baseUrl, requestScope, callOptions);
|
|
633
658
|
if (connector.status) {
|
|
634
659
|
try {
|
|
635
660
|
return await connector.status(ctx);
|
|
@@ -638,7 +663,7 @@ export class Registry implements RegistryView {
|
|
|
638
663
|
}
|
|
639
664
|
}
|
|
640
665
|
try {
|
|
641
|
-
await this.getTools(id, baseUrl, requestScope);
|
|
666
|
+
await this.getTools(id, baseUrl, requestScope, callOptions);
|
|
642
667
|
return { state: "ok" };
|
|
643
668
|
} catch (err) {
|
|
644
669
|
return { state: "error", message: msg(err) };
|
|
@@ -782,11 +807,12 @@ export class ScopedRegistry implements RegistryView {
|
|
|
782
807
|
id: string,
|
|
783
808
|
baseUrl: string,
|
|
784
809
|
requestScope?: object,
|
|
810
|
+
callOptions: ConnectorOperationOptions = {},
|
|
785
811
|
): Promise<ToolDef[]> {
|
|
786
812
|
if (!this.visible(id)) throw this.unknownConnector(id);
|
|
787
813
|
return this.inScopeTools(
|
|
788
814
|
id,
|
|
789
|
-
await this.base.getTools(id, baseUrl, requestScope),
|
|
815
|
+
await this.base.getTools(id, baseUrl, requestScope, callOptions),
|
|
790
816
|
);
|
|
791
817
|
}
|
|
792
818
|
|
|
@@ -794,11 +820,12 @@ export class ScopedRegistry implements RegistryView {
|
|
|
794
820
|
id: string,
|
|
795
821
|
baseUrl: string,
|
|
796
822
|
requestScope?: object,
|
|
823
|
+
callOptions: ConnectorOperationOptions = {},
|
|
797
824
|
): Promise<ToolDef[]> {
|
|
798
825
|
if (!this.visible(id)) throw this.unknownConnector(id);
|
|
799
826
|
return this.inScopeTools(
|
|
800
827
|
id,
|
|
801
|
-
await this.base.refreshTools(id, baseUrl, requestScope),
|
|
828
|
+
await this.base.refreshTools(id, baseUrl, requestScope, callOptions),
|
|
802
829
|
);
|
|
803
830
|
}
|
|
804
831
|
|
|
@@ -812,7 +839,7 @@ export class ScopedRegistry implements RegistryView {
|
|
|
812
839
|
id: string,
|
|
813
840
|
baseUrl: string,
|
|
814
841
|
requestScope: object = {},
|
|
815
|
-
callOptions:
|
|
842
|
+
callOptions: ConnectorOperationOptions = {},
|
|
816
843
|
): ConnectorContext {
|
|
817
844
|
// Unreachable through the meta-tools (they resolve first), so a throw here
|
|
818
845
|
// is a loud backstop rather than a silent grant of connector storage and
|
|
@@ -897,12 +924,13 @@ export class ScopedRegistry implements RegistryView {
|
|
|
897
924
|
id: string,
|
|
898
925
|
baseUrl: string,
|
|
899
926
|
requestScope: object = {},
|
|
927
|
+
callOptions: { signal?: AbortSignal; timeoutMs?: number } = {},
|
|
900
928
|
): Promise<ConnectorStatus> {
|
|
901
929
|
// Same shape the unscoped registry returns for an unregistered id.
|
|
902
930
|
if (!this.visible(id)) {
|
|
903
931
|
return { state: "error", message: "Unknown connector" };
|
|
904
932
|
}
|
|
905
|
-
return this.base.statusFor(id, baseUrl, requestScope);
|
|
933
|
+
return this.base.statusFor(id, baseUrl, requestScope, callOptions);
|
|
906
934
|
}
|
|
907
935
|
|
|
908
936
|
async invalidateStored(id: string): Promise<void> {
|
package/src/server.ts
CHANGED
|
@@ -222,8 +222,8 @@ async function authorize(
|
|
|
222
222
|
| {
|
|
223
223
|
ok: true;
|
|
224
224
|
actor: ActivityActor;
|
|
225
|
-
|
|
226
|
-
|
|
225
|
+
/** True only when the admitting provider can also authorize UI mutation. */
|
|
226
|
+
uiAdminEligible?: boolean;
|
|
227
227
|
/** The admitting identity's toolkit binding (docs/toolkits.md). */
|
|
228
228
|
toolkitBinding?: ToolkitBinding;
|
|
229
229
|
}
|
|
@@ -261,8 +261,9 @@ async function authorize(
|
|
|
261
261
|
kind: provider.kind,
|
|
262
262
|
...(subjectId ? { id: subjectId } : {}),
|
|
263
263
|
},
|
|
264
|
-
|
|
265
|
-
|
|
264
|
+
...(result.userId && provider.uiAuth?.kind === "clerk"
|
|
265
|
+
? { uiAdminEligible: true }
|
|
266
|
+
: {}),
|
|
266
267
|
...(binding.binding ? { toolkitBinding: binding.binding } : {}),
|
|
267
268
|
};
|
|
268
269
|
}
|
|
@@ -1197,7 +1198,7 @@ export function createFetchHandler(
|
|
|
1197
1198
|
|
|
1198
1199
|
const operatorPage = operatorPageForPath(path);
|
|
1199
1200
|
if (operatorPage) {
|
|
1200
|
-
if (request.method !== "GET") {
|
|
1201
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
1201
1202
|
return privateJson({ error: "method not allowed" }, { status: 405 });
|
|
1202
1203
|
}
|
|
1203
1204
|
// Open shell — carries no operator data; everything comes from the
|
|
@@ -1212,7 +1213,9 @@ export function createFetchHandler(
|
|
|
1212
1213
|
// — only script execution, the XSS sink, is gated.
|
|
1213
1214
|
const nonce = uiScriptNonce();
|
|
1214
1215
|
return new Response(
|
|
1215
|
-
|
|
1216
|
+
request.method === "HEAD"
|
|
1217
|
+
? null
|
|
1218
|
+
: renderUiHtml(uiAuth, mcpUrl, opts.branding, nonce, operatorPage),
|
|
1216
1219
|
{
|
|
1217
1220
|
status: 200,
|
|
1218
1221
|
headers: {
|
|
@@ -1235,8 +1238,7 @@ export function createFetchHandler(
|
|
|
1235
1238
|
// After the restriction check, not before: an identity that may not
|
|
1236
1239
|
// read this surface should not get to trigger background work from it.
|
|
1237
1240
|
sweepCredentials();
|
|
1238
|
-
const eligibleClerkOperator =
|
|
1239
|
-
authz.providerKind === "clerk" && Boolean(authz.userId);
|
|
1241
|
+
const eligibleClerkOperator = authz.uiAdminEligible === true;
|
|
1240
1242
|
const credentialManagement = credentialManagementCapability({
|
|
1241
1243
|
eligibleClerkOperator,
|
|
1242
1244
|
hasCredentialSlots: registry
|
|
@@ -1253,6 +1255,7 @@ export function createFetchHandler(
|
|
|
1253
1255
|
eligibleClerkOperator ? opts.credentialVault : undefined,
|
|
1254
1256
|
Boolean(opts.activity?.list),
|
|
1255
1257
|
credentialManagement,
|
|
1258
|
+
opts.toolkits,
|
|
1256
1259
|
);
|
|
1257
1260
|
return privateJson(data);
|
|
1258
1261
|
}
|
package/src/timeout.ts
CHANGED
|
@@ -21,10 +21,9 @@ export function normalizeTimeoutMs(
|
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Reject `promise` after `ms` if it has not settled, so one hung downstream
|
|
24
|
-
* cannot stall a whole fan-out.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* (AbortSignal plumbed through the registry) is a deferred follow-up.
|
|
24
|
+
* cannot stall a whole fan-out. This form bounds only the caller-facing wait;
|
|
25
|
+
* use `withAbortableTimeout` when the operation accepts an AbortSignal and the
|
|
26
|
+
* underlying work must stop too.
|
|
28
27
|
*/
|
|
29
28
|
export function withTimeout<T>(
|
|
30
29
|
promise: Promise<T>,
|
|
@@ -47,3 +46,41 @@ export function withTimeout<T>(
|
|
|
47
46
|
);
|
|
48
47
|
});
|
|
49
48
|
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Give one operation a caller-facing deadline and the matching cancellation
|
|
52
|
+
* signal. The timeout rejects with the stable, labelled error while aborting
|
|
53
|
+
* any in-flight work that honors the signal.
|
|
54
|
+
*/
|
|
55
|
+
export function withAbortableTimeout<T>(
|
|
56
|
+
operation: (signal: AbortSignal) => Promise<T>,
|
|
57
|
+
ms: number,
|
|
58
|
+
label: string,
|
|
59
|
+
): Promise<T> {
|
|
60
|
+
const controller = new AbortController();
|
|
61
|
+
return new Promise<T>((resolve, reject) => {
|
|
62
|
+
const timeoutError = new Error(`${label} timed out after ${ms}ms`);
|
|
63
|
+
const timer = setTimeout(() => {
|
|
64
|
+
controller.abort(timeoutError);
|
|
65
|
+
reject(timeoutError);
|
|
66
|
+
}, ms);
|
|
67
|
+
let promise: Promise<T>;
|
|
68
|
+
try {
|
|
69
|
+
promise = operation(controller.signal);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
clearTimeout(timer);
|
|
72
|
+
reject(err);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
promise.then(
|
|
76
|
+
(value) => {
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
resolve(value);
|
|
79
|
+
},
|
|
80
|
+
(err) => {
|
|
81
|
+
clearTimeout(timer);
|
|
82
|
+
reject(err);
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
}
|
package/src/toolkits.ts
CHANGED
|
@@ -41,6 +41,14 @@ export type ToolkitConfig = Record<string, ToolkitDefinition>;
|
|
|
41
41
|
export interface Toolkit {
|
|
42
42
|
readonly name: string;
|
|
43
43
|
readonly description?: string;
|
|
44
|
+
/**
|
|
45
|
+
* The validated config this scope came from. Kept as immutable data so
|
|
46
|
+
* deployment-wide operator surfaces can explain the running configuration
|
|
47
|
+
* without inventing a second source of truth.
|
|
48
|
+
*/
|
|
49
|
+
readonly connectors: readonly string[];
|
|
50
|
+
readonly includeTools: readonly string[];
|
|
51
|
+
readonly excludeTools: readonly string[];
|
|
44
52
|
/** True when `connectorId` is inside this toolkit's scope. */
|
|
45
53
|
hasConnector(connectorId: string): boolean;
|
|
46
54
|
/** True when `<connectorId>.<toolName>` is inside this toolkit's scope. */
|
|
@@ -178,6 +186,9 @@ function resolveToolkit(
|
|
|
178
186
|
return {
|
|
179
187
|
name,
|
|
180
188
|
...(definition.description ? { description: definition.description } : {}),
|
|
189
|
+
connectors: Object.freeze([...connectorIds]),
|
|
190
|
+
includeTools: Object.freeze([...(definition.includeTools ?? [])]),
|
|
191
|
+
excludeTools: Object.freeze([...(definition.excludeTools ?? [])]),
|
|
181
192
|
hasConnector: (connectorId) => connectorIds.has(connectorId),
|
|
182
193
|
hasTool: (connectorId, toolName) => {
|
|
183
194
|
if (!connectorIds.has(connectorId)) return false;
|
package/src/types.ts
CHANGED
|
@@ -113,9 +113,9 @@ export interface ConnectorContext {
|
|
|
113
113
|
* when omitted.
|
|
114
114
|
*/
|
|
115
115
|
requestScope?: object;
|
|
116
|
-
/** Best-effort cancellation signal for this
|
|
116
|
+
/** Best-effort cancellation signal for this connector operation. */
|
|
117
117
|
signal?: AbortSignal;
|
|
118
|
-
/** Requested
|
|
118
|
+
/** Requested connector-operation deadline in milliseconds. */
|
|
119
119
|
timeoutMs?: number;
|
|
120
120
|
}
|
|
121
121
|
|