@frontmcp/plugin-skilled-openapi 1.8.7 → 1.9.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/README.md +13 -13
- package/esm/index.mjs +123 -60
- package/esm/package.json +6 -6
- package/executor/caller-token.d.ts +15 -0
- package/executor/caller-token.d.ts.map +1 -0
- package/executor/execute-skill-action.d.ts.map +1 -1
- package/executor/openapi-runtime.d.ts.map +1 -1
- package/index.js +128 -65
- package/package.json +6 -6
- package/skilled-openapi.plugin.d.ts.map +1 -1
- package/skilled-openapi.types.d.ts +4 -4
- package/skilled-openapi.types.d.ts.map +1 -1
- package/tools/operation-tool.factory.d.ts.map +1 -1
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ npm install @enclave-vm/core @enclave-vm/ast
|
|
|
20
20
|
|
|
21
21
|
## Usage
|
|
22
22
|
|
|
23
|
-
Register the plugin with `SkilledOpenApiPlugin.init(...)` and point it at a bundle source. The `dev: true` flag bypasses signature verification and allows `http://` upstreams for local iteration — see [Security](https://docs.agentfront.dev/frontmcp/plugins/skilled-openapi/security) before going to production.
|
|
23
|
+
Register the plugin with `SkilledOpenApiPlugin.init(...)` and point it at a bundle source. The `dev: true` flag bypasses signature verification (unless `requireSignature` is set explicitly) and allows `http://` upstreams for local iteration — see [Security](https://docs.agentfront.dev/frontmcp/plugins/skilled-openapi/security) before going to production.
|
|
24
24
|
|
|
25
25
|
```typescript
|
|
26
26
|
import * as path from 'node:path';
|
|
@@ -94,18 +94,18 @@ OpenAPI spec --(analyzer + optional signing)--> bundle (spec + overlay)
|
|
|
94
94
|
|
|
95
95
|
All options are validated by a strict Zod schema (`skilledOpenApiPluginOptionsSchema`).
|
|
96
96
|
|
|
97
|
-
| Option | Type | Default
|
|
98
|
-
| --------------------------------- | ------------------------------------------ |
|
|
99
|
-
| `source` | `static \| npm \| saas \| inline` | — (required)
|
|
100
|
-
| `requireSignature` | `boolean` | `true`
|
|
101
|
-
| `trustedKeys` | `SignatureKey[]` | `[]`
|
|
102
|
-
| `dev` | `boolean` | `false`
|
|
103
|
-
| `outbound` | `OutboundOptions` | see below
|
|
104
|
-
| `unprotectedOps` | `'allow' \| 'deny'` | `'allow'`
|
|
105
|
-
| `sourceConflictPolicy` | `'static-wins' \| 'last-wins' \| 'reject'` | `'static-wins'`
|
|
106
|
-
| `bundleCacheDir` | `string` | —
|
|
107
|
-
| `credentials` | `Record<vaultRef, secret>` | —
|
|
108
|
-
| `exposeOperationsAsInternalTools` | `boolean` | `true`
|
|
97
|
+
| Option | Type | Default | Description |
|
|
98
|
+
| --------------------------------- | ------------------------------------------ | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
99
|
+
| `source` | `static \| npm \| saas \| inline` | — (required) | Where bundles come from. `{ type: 'static', path, watch? }`, `{ type: 'npm', package }`, `{ type: 'saas', endpoint, ... }`, or `{ type: 'inline', ... }`. |
|
|
100
|
+
| `requireSignature` | `boolean` | `true` (`false` with `dev: true`) | Require a valid bundle signature (RS256/Ed25519 JWT-of-hashes). Opt out with `dev: true` or `requireSignature: false`; an explicit value wins over `dev`. |
|
|
101
|
+
| `trustedKeys` | `SignatureKey[]` | `[]` | Public keys trusted to sign bundles. |
|
|
102
|
+
| `dev` | `boolean` | `false` | Local-dev escape hatch: turns the signature requirement off (unless `requireSignature` is set) and widens `outbound` to allow `http://`. **Never enable in production.** |
|
|
103
|
+
| `outbound` | `OutboundOptions` | see below | SSRF / egress controls. |
|
|
104
|
+
| `unprotectedOps` | `'allow' \| 'deny'` | `'allow'` | Default-deny policy for operations that declare no required authorities. |
|
|
105
|
+
| `sourceConflictPolicy` | `'static-wins' \| 'last-wins' \| 'reject'` | `'static-wins'` | How to resolve two sources registering the same skill id. |
|
|
106
|
+
| `bundleCacheDir` | `string` | — | Last-good cache directory (only for `source.type === 'saas'`). |
|
|
107
|
+
| `credentials` | `Record<vaultRef, secret>` | — | In-memory credential map for dev / single-tenant. In production resolve via `@frontmcp/auth`'s vault. |
|
|
108
|
+
| `exposeOperationsAsInternalTools` | `boolean` | `true` | Register each operation as an internal tool, callable in-process as `this.callTool('<bundleId>.<operationId>', input)` (hidden from `tools/list`). |
|
|
109
109
|
|
|
110
110
|
`outbound` (SSRF + egress):
|
|
111
111
|
|
package/esm/index.mjs
CHANGED
|
@@ -392,19 +392,20 @@ var skilledOpenApiPluginOptionsObjectSchema = z.object({
|
|
|
392
392
|
/** Bundle source — static, npm, or saas. */
|
|
393
393
|
source: bundleSourceSchema,
|
|
394
394
|
/**
|
|
395
|
-
* Require a valid bundle signature. Defaults to true
|
|
396
|
-
*
|
|
397
|
-
*
|
|
395
|
+
* Require a valid bundle signature. Defaults to true, or to false with
|
|
396
|
+
* `dev: true`; an explicit value always wins. Never set false (unsigned mode)
|
|
397
|
+
* in production.
|
|
398
398
|
*/
|
|
399
|
-
requireSignature: z.boolean().
|
|
399
|
+
requireSignature: z.boolean().optional(),
|
|
400
400
|
/**
|
|
401
401
|
* Trusted public keys for bundle signature verification. At least one must
|
|
402
402
|
* match the bundle's signed `kid` for the bundle to load.
|
|
403
403
|
*/
|
|
404
404
|
trustedKeys: z.array(signatureKeySchema).default([]),
|
|
405
405
|
/**
|
|
406
|
-
* Development mode: bypass signature verification
|
|
407
|
-
*
|
|
406
|
+
* Development mode: bypass signature verification (unless `requireSignature`
|
|
407
|
+
* is set explicitly) and allow http: upstreams. Loud startup warning. Never
|
|
408
|
+
* true in production.
|
|
408
409
|
*/
|
|
409
410
|
dev: z.boolean().default(false),
|
|
410
411
|
/** Outbound HTTP / SSRF defenses for the executor. */
|
|
@@ -473,6 +474,9 @@ var skilledOpenApiPluginOptionsSchema = skilledOpenApiPluginOptionsObjectSchema.
|
|
|
473
474
|
}
|
|
474
475
|
return {
|
|
475
476
|
...opts,
|
|
477
|
+
// `dev: true` turns the signature requirement off, as it advertises; an explicit
|
|
478
|
+
// `requireSignature` (either way) is kept.
|
|
479
|
+
requireSignature: opts.requireSignature ?? !opts.dev,
|
|
476
480
|
outbound,
|
|
477
481
|
bundleCacheDir: opts.bundleCacheDir ?? ".frontmcp/skilled-openapi/"
|
|
478
482
|
};
|
|
@@ -907,6 +911,63 @@ import {
|
|
|
907
911
|
ToolKind
|
|
908
912
|
} from "@frontmcp/sdk";
|
|
909
913
|
|
|
914
|
+
// plugins/plugin-skilled-openapi/src/executor/caller-token.ts
|
|
915
|
+
import { decodeJwtPayloadSafe } from "@frontmcp/auth";
|
|
916
|
+
import { trimTrailing } from "@frontmcp/utils";
|
|
917
|
+
function callerTokenOf(authInfo) {
|
|
918
|
+
if (typeof authInfo !== "object" || authInfo === null) return void 0;
|
|
919
|
+
const token = authInfo.token;
|
|
920
|
+
return typeof token === "string" && token.length > 0 ? token : void 0;
|
|
921
|
+
}
|
|
922
|
+
function callerTokenRefusal(token, url) {
|
|
923
|
+
const claims = decodeJwtPayloadSafe(token);
|
|
924
|
+
if (!claims) {
|
|
925
|
+
return "the caller token is not a JWT, so the API it was issued for cannot be checked";
|
|
926
|
+
}
|
|
927
|
+
let target;
|
|
928
|
+
try {
|
|
929
|
+
target = new URL(url);
|
|
930
|
+
} catch {
|
|
931
|
+
return `the caller token was not issued for ${url} (it is not a URL)`;
|
|
932
|
+
}
|
|
933
|
+
const destination = `${target.origin}${target.pathname}`;
|
|
934
|
+
const issuedFor = [...claimValues(claims["resource"]), ...claimValues(claims["aud"])];
|
|
935
|
+
if (!issuedFor.some((resource) => resourceCovers(resource, target))) {
|
|
936
|
+
return `the caller token was not issued for ${destination} (no resource or aud claim names it)`;
|
|
937
|
+
}
|
|
938
|
+
if (hasDecodedDotDotSegment(target.pathname)) {
|
|
939
|
+
return `the request path ${target.pathname} has a ".." segment once percent-decoded, which could take the caller token outside the API it was issued for`;
|
|
940
|
+
}
|
|
941
|
+
return void 0;
|
|
942
|
+
}
|
|
943
|
+
function claimValues(claim) {
|
|
944
|
+
if (typeof claim === "string") return [claim];
|
|
945
|
+
if (Array.isArray(claim)) return claim.filter((value) => typeof value === "string");
|
|
946
|
+
return [];
|
|
947
|
+
}
|
|
948
|
+
function resourceCovers(resource, target) {
|
|
949
|
+
let issued;
|
|
950
|
+
try {
|
|
951
|
+
issued = new URL(resource);
|
|
952
|
+
} catch {
|
|
953
|
+
return false;
|
|
954
|
+
}
|
|
955
|
+
if (issued.search || issued.hash || issued.origin !== target.origin) return false;
|
|
956
|
+
const issuedPath = trimTrailing(issued.pathname, "/");
|
|
957
|
+
const targetPath = trimTrailing(target.pathname, "/");
|
|
958
|
+
return issuedPath === "" || targetPath === issuedPath || targetPath.startsWith(`${issuedPath}/`);
|
|
959
|
+
}
|
|
960
|
+
var DOT_SEGMENT_ESCAPE = /%(?:2e|2f|5c|3b|25)/gi;
|
|
961
|
+
function hasDecodedDotDotSegment(pathname) {
|
|
962
|
+
let decoded = pathname;
|
|
963
|
+
let previous;
|
|
964
|
+
do {
|
|
965
|
+
previous = decoded;
|
|
966
|
+
decoded = previous.replace(DOT_SEGMENT_ESCAPE, (escape) => String.fromCharCode(parseInt(escape.slice(1), 16)));
|
|
967
|
+
} while (decoded !== previous);
|
|
968
|
+
return decoded.split(/[/\\]/).some((segment) => segment.split(";")[0] === "..");
|
|
969
|
+
}
|
|
970
|
+
|
|
910
971
|
// plugins/plugin-skilled-openapi/src/executor/openapi-runtime.ts
|
|
911
972
|
import {
|
|
912
973
|
buildRequest,
|
|
@@ -1027,35 +1088,30 @@ async function checkOutboundUrl(target, allowedHosts, outbound) {
|
|
|
1027
1088
|
|
|
1028
1089
|
// plugins/plugin-skilled-openapi/src/executor/openapi-runtime.ts
|
|
1029
1090
|
async function buildSecurityContext(args) {
|
|
1030
|
-
const { binding, bundleId, resolver, callerToken } = args;
|
|
1031
|
-
const ctx = {};
|
|
1091
|
+
const { binding, bundleId, resolver, serviceBaseUrl, callerToken } = args;
|
|
1032
1092
|
switch (binding.kind) {
|
|
1033
1093
|
case "none":
|
|
1034
|
-
return
|
|
1094
|
+
return { context: {} };
|
|
1035
1095
|
case "bearer": {
|
|
1036
|
-
let token;
|
|
1037
1096
|
if (binding.passthroughCallerToken) {
|
|
1038
|
-
token
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
if (!token) throw new Error(`bearer vaultRef "${binding.vaultRef}" did not resolve`);
|
|
1097
|
+
if (!callerToken) throw new Error("passthrough caller token requested but the caller presented none");
|
|
1098
|
+
const refusal = callerTokenRefusal(callerToken, serviceBaseUrl);
|
|
1099
|
+
if (refusal) throw new Error(`passthrough caller token refused: ${refusal}`);
|
|
1100
|
+
return { context: { jwt: callerToken }, forwardedCallerToken: callerToken };
|
|
1043
1101
|
}
|
|
1044
|
-
|
|
1045
|
-
|
|
1102
|
+
const token = await resolver.resolve(binding.vaultRef, { bundleId });
|
|
1103
|
+
if (!token) throw new Error(`bearer vaultRef "${binding.vaultRef}" did not resolve`);
|
|
1104
|
+
return { context: { jwt: token } };
|
|
1046
1105
|
}
|
|
1047
1106
|
case "apiKey": {
|
|
1048
1107
|
const value = await resolver.resolve(binding.vaultRef, { bundleId });
|
|
1049
1108
|
if (!value) throw new Error(`apiKey vaultRef "${binding.vaultRef}" did not resolve`);
|
|
1050
|
-
|
|
1051
|
-
ctx.apiKey = value;
|
|
1052
|
-
return ctx;
|
|
1109
|
+
return { context: { apiKeys: { [binding.name]: value }, apiKey: value } };
|
|
1053
1110
|
}
|
|
1054
1111
|
case "oauth2": {
|
|
1055
1112
|
const token = await resolver.resolve(binding.vaultRef, { bundleId });
|
|
1056
1113
|
if (!token) throw new Error(`oauth2 vaultRef "${binding.vaultRef}" did not resolve`);
|
|
1057
|
-
|
|
1058
|
-
return ctx;
|
|
1114
|
+
return { context: { oauth2Token: token } };
|
|
1059
1115
|
}
|
|
1060
1116
|
}
|
|
1061
1117
|
}
|
|
@@ -1118,12 +1174,13 @@ async function executeOperation(args) {
|
|
|
1118
1174
|
} catch (e) {
|
|
1119
1175
|
return failure(0, `tool projection failed: ${e.message}`);
|
|
1120
1176
|
}
|
|
1121
|
-
let
|
|
1177
|
+
let auth;
|
|
1122
1178
|
try {
|
|
1123
|
-
|
|
1179
|
+
auth = await buildSecurityContext({
|
|
1124
1180
|
binding: entry.authBinding,
|
|
1125
1181
|
bundleId,
|
|
1126
1182
|
resolver,
|
|
1183
|
+
serviceBaseUrl: entry.service.baseUrl,
|
|
1127
1184
|
callerToken
|
|
1128
1185
|
});
|
|
1129
1186
|
} catch (e) {
|
|
@@ -1131,7 +1188,7 @@ async function executeOperation(args) {
|
|
|
1131
1188
|
}
|
|
1132
1189
|
let security;
|
|
1133
1190
|
try {
|
|
1134
|
-
security = await resolveSecurity(mcpTool,
|
|
1191
|
+
security = await resolveSecurity(mcpTool, auth.context);
|
|
1135
1192
|
} catch (e) {
|
|
1136
1193
|
return failure(0, `security resolve failed: ${e.message}`);
|
|
1137
1194
|
}
|
|
@@ -1141,10 +1198,23 @@ async function executeOperation(args) {
|
|
|
1141
1198
|
} catch (e) {
|
|
1142
1199
|
return failure(0, `request build failed: ${e.message}`);
|
|
1143
1200
|
}
|
|
1201
|
+
if (auth.forwardedCallerToken) {
|
|
1202
|
+
const refusal = callerTokenRefusal(auth.forwardedCallerToken, req.url);
|
|
1203
|
+
if (refusal) return failure(0, `auth resolution failed: passthrough caller token refused: ${refusal}`);
|
|
1204
|
+
}
|
|
1144
1205
|
const ssrf = await checkOutboundUrl(req.url, allowedHosts, outbound);
|
|
1145
1206
|
if (!ssrf.ok) {
|
|
1146
1207
|
return failure(0, `ssrf check rejected request: ${ssrf.reason}`);
|
|
1147
1208
|
}
|
|
1209
|
+
let body;
|
|
1210
|
+
try {
|
|
1211
|
+
body = req.body !== void 0 ? JSON.stringify(req.body) : void 0;
|
|
1212
|
+
} catch (e) {
|
|
1213
|
+
return failure(0, `request body serialization failed: ${e.message}`);
|
|
1214
|
+
}
|
|
1215
|
+
if (body !== void 0 && !req.headers.has("content-type")) {
|
|
1216
|
+
req.headers.set("content-type", "application/json");
|
|
1217
|
+
}
|
|
1148
1218
|
const timeoutMs = entry.op.timeoutMs ?? outbound.defaultTimeoutMs;
|
|
1149
1219
|
const maxBytes = entry.op.maxResponseBytes ?? outbound.defaultMaxResponseBytes;
|
|
1150
1220
|
const host = (() => {
|
|
@@ -1162,7 +1232,7 @@ async function executeOperation(args) {
|
|
|
1162
1232
|
const response = await fetchImpl(req.url, {
|
|
1163
1233
|
method: entry.op.httpMethod,
|
|
1164
1234
|
headers: req.headers,
|
|
1165
|
-
body
|
|
1235
|
+
body,
|
|
1166
1236
|
signal: ac.signal,
|
|
1167
1237
|
// SECURITY: never auto-follow redirects. `fetch` defaults to
|
|
1168
1238
|
// `redirect: 'follow'`, which re-sends the request — INCLUDING the
|
|
@@ -1431,6 +1501,8 @@ var OperationToolFactory = class {
|
|
|
1431
1501
|
entry,
|
|
1432
1502
|
bundleId: entry.bundleId,
|
|
1433
1503
|
input: inputParse.data,
|
|
1504
|
+
// For a bearer binding with `passthroughCallerToken: true`.
|
|
1505
|
+
callerToken: callerTokenOf(ctx.authInfo),
|
|
1434
1506
|
deps
|
|
1435
1507
|
});
|
|
1436
1508
|
return {
|
|
@@ -1510,6 +1582,8 @@ async function executeSkillAction(args) {
|
|
|
1510
1582
|
entry,
|
|
1511
1583
|
bundleId: entry.bundleId,
|
|
1512
1584
|
input: inputParse.data,
|
|
1585
|
+
// For a bearer binding with `passthroughCallerToken: true`.
|
|
1586
|
+
callerToken: callerTokenOf(authInfo),
|
|
1513
1587
|
deps: runtimeDeps
|
|
1514
1588
|
});
|
|
1515
1589
|
} catch (e) {
|
|
@@ -1784,6 +1858,18 @@ function resolveRuntimeDeps(scope) {
|
|
|
1784
1858
|
return void 0;
|
|
1785
1859
|
}
|
|
1786
1860
|
}
|
|
1861
|
+
function lazyScopeRegistry(scope, name) {
|
|
1862
|
+
return new Proxy({}, {
|
|
1863
|
+
get(_target, prop) {
|
|
1864
|
+
const registry = scope[name];
|
|
1865
|
+
if (!registry) {
|
|
1866
|
+
throw new Error(`[skilled-openapi] scope.${name} not available when accessing "${String(prop)}"`);
|
|
1867
|
+
}
|
|
1868
|
+
const value = registry[prop];
|
|
1869
|
+
return typeof value === "function" ? value.bind(registry) : value;
|
|
1870
|
+
}
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1787
1873
|
function resolveBundleTelemetry(scope) {
|
|
1788
1874
|
const providers = scope?.providers;
|
|
1789
1875
|
if (!providers || typeof providers.get !== "function") return void 0;
|
|
@@ -1881,9 +1967,8 @@ ${catalog}` : searchSkillDescription;
|
|
|
1881
1967
|
}
|
|
1882
1968
|
warnIfInsecureConfig() {
|
|
1883
1969
|
if (this.options.dev) {
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
);
|
|
1970
|
+
const signing = this.options.requireSignature ? "signature verification kept (requireSignature=true)" : "signature verification BYPASSED";
|
|
1971
|
+
console.warn(`[skilled-openapi] dev=true: ${signing} and http:// URLs allowed. NEVER use this in production.`);
|
|
1887
1972
|
}
|
|
1888
1973
|
if (!this.options.requireSignature && !this.options.dev) {
|
|
1889
1974
|
console.warn(
|
|
@@ -1936,39 +2021,17 @@ ${catalog}` : searchSkillDescription;
|
|
|
1936
2021
|
inject: () => [ScopeEntry3, HiddenOpRegistry, BundleStore3],
|
|
1937
2022
|
useFactory: async (scope, hiddenOps, bundleStore) => {
|
|
1938
2023
|
const logger = scope.logger.child("skilled-openapi:sync");
|
|
1939
|
-
const lazySkillRegistry =
|
|
1940
|
-
get(_t, prop) {
|
|
1941
|
-
const reg = scope.skills;
|
|
1942
|
-
if (!reg) {
|
|
1943
|
-
throw new Error(`[skilled-openapi] scope.skills not available when accessing "${String(prop)}"`);
|
|
1944
|
-
}
|
|
1945
|
-
const v = reg[prop];
|
|
1946
|
-
return typeof v === "function" ? v.bind(reg) : v;
|
|
1947
|
-
}
|
|
1948
|
-
});
|
|
2024
|
+
const lazySkillRegistry = lazyScopeRegistry(scope, "skills");
|
|
1949
2025
|
let opToolFactory;
|
|
1950
2026
|
if (parsed.exposeOperationsAsInternalTools) {
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
providers: scope.providers,
|
|
1960
|
-
logger: logger.child("op-tool")
|
|
1961
|
-
});
|
|
1962
|
-
} else {
|
|
1963
|
-
logger.warn(
|
|
1964
|
-
"exposeOperationsAsInternalTools=true but scope.tools is unavailable; per-op internal tools disabled"
|
|
1965
|
-
);
|
|
1966
|
-
}
|
|
1967
|
-
} catch (e) {
|
|
1968
|
-
logger.warn(
|
|
1969
|
-
`failed to build OperationToolFactory: ${e.message}; per-op internal tools disabled`
|
|
1970
|
-
);
|
|
1971
|
-
}
|
|
2027
|
+
opToolFactory = new OperationToolFactory({
|
|
2028
|
+
toolRegistry: lazyScopeRegistry(scope, "tools"),
|
|
2029
|
+
// ScopeEntry exposes a ProviderRegistryInterface; the factory
|
|
2030
|
+
// needs the concrete ProviderRegistry to construct ToolInstance.
|
|
2031
|
+
// The runtime is the same class — the cast is safe by construction.
|
|
2032
|
+
providers: scope.providers,
|
|
2033
|
+
logger: logger.child("op-tool")
|
|
2034
|
+
});
|
|
1972
2035
|
}
|
|
1973
2036
|
const sync = new BundleSyncService(
|
|
1974
2037
|
lazySkillRegistry,
|
package/esm/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontmcp/plugin-skilled-openapi",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "FrontMCP plugin: serve a customer's OpenAPI spec as skill bundles (signed, OpenAPI-Overlay-based) with hidden per-operation tools mediated by three meta-tools (search_skill / load_skill / run_workflow — the last runs an enclave-sandboxed AgentScript that orchestrates the loaded skills' operations).",
|
|
5
5
|
"author": "AgentFront <info@agentfront.dev>",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -48,15 +48,15 @@
|
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@frontmcp/adapters": "1.
|
|
52
|
-
"@frontmcp/auth": "1.
|
|
53
|
-
"@frontmcp/sdk": "1.
|
|
54
|
-
"@frontmcp/utils": "1.
|
|
51
|
+
"@frontmcp/adapters": "1.9.0",
|
|
52
|
+
"@frontmcp/auth": "1.9.0",
|
|
53
|
+
"@frontmcp/sdk": "1.9.0",
|
|
54
|
+
"@frontmcp/utils": "1.9.0"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"@enclave-vm/ast": "*",
|
|
58
58
|
"@enclave-vm/core": "*",
|
|
59
|
-
"@frontmcp/lazy-zod": "1.
|
|
59
|
+
"@frontmcp/lazy-zod": "1.9.0"
|
|
60
60
|
},
|
|
61
61
|
"peerDependenciesMeta": {
|
|
62
62
|
"@enclave-vm/ast": {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** The bearer token the MCP client presented to this server (`authInfo.token`), when it presented one. */
|
|
2
|
+
export declare function callerTokenOf(authInfo: unknown): string | undefined;
|
|
3
|
+
/**
|
|
4
|
+
* Why the caller's token may not be sent to `url`, or `undefined` when it may. It may when it is a
|
|
5
|
+
* JWT whose `resource` claim (RFC 8707) or `aud` claim (where RFC 8707 / RFC 9068 put the resource
|
|
6
|
+
* indicator) names the API `url` belongs to: that URL, or a URL above it on the same origin.
|
|
7
|
+
*
|
|
8
|
+
* `url` is checked as `fetch` sends it, after URL parsing resolves `.` / `..` segments (`%2e`
|
|
9
|
+
* spellings and `\` separators included), so pass the URL a request actually goes to, not only the
|
|
10
|
+
* service's base URL: `/v1/{id}/me` with an `id` of `..` is `/me`, outside a token issued for `/v1`.
|
|
11
|
+
* A path that has a `..` segment once percent-decoded (`..%2F`, `%252e%252e`, `..;`) is refused too:
|
|
12
|
+
* URL parsing keeps it inside the API, but an upstream that decodes its path before resolving it does not.
|
|
13
|
+
*/
|
|
14
|
+
export declare function callerTokenRefusal(token: string, url: string): string | undefined;
|
|
15
|
+
//# sourceMappingURL=caller-token.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"caller-token.d.ts","sourceRoot":"","sources":["../../src/executor/caller-token.ts"],"names":[],"mappings":"AAWA,0GAA0G;AAC1G,wBAAgB,aAAa,CAAC,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAInE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAoBjF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"execute-skill-action.d.ts","sourceRoot":"","sources":["../../src/executor/execute-skill-action.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAEpD,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,gCAAgC,CAAC;AACpE,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"execute-skill-action.d.ts","sourceRoot":"","sources":["../../src/executor/execute-skill-action.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAEpD,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,gCAAgC,CAAC;AACpE,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAE7F,OAAO,EAAoB,KAAK,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAG9E,8FAA8F;AAC9F,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,mFAAmF;AACnF,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE;QACN,QAAQ,EAAE,kBAAkB,CAAC,UAAU,CAAC,CAAC;QACzC,wEAAwE;QACxE,cAAc,EAAE,oBAAoB,CAAC;KACtC,CAAC;IACF,QAAQ,EAAE;QAAE,OAAO,EAAE,kBAAkB,CAAC,UAAU,CAAC,CAAC,SAAS,CAAC,CAAA;KAAE,CAAC;IACjE,KAAK,EAAE,cAAc,CAAC;IACtB,MAAM,EAAE,cAAc,CAAC;IACvB,uEAAuE;IACvE,KAAK,CAAC,EAAE;QAAE,MAAM,EAAE,gBAAgB,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CACvD;AAWD;;;;;GAKG;AACH,wBAAsB,kBAAkB,CAAC,IAAI,EAAE;IAC7C,KAAK,EAAE,aAAa,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,QAAQ,EAAE,OAAO,CAAC;IAClB,IAAI,EAAE,eAAe,CAAC;CACvB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAwH7B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openapi-runtime.d.ts","sourceRoot":"","sources":["../../src/executor/openapi-runtime.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAGpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC;AACpE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"openapi-runtime.d.ts","sourceRoot":"","sources":["../../src/executor/openapi-runtime.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAGpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC;AACpE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAEhE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAIhE,kEAAkE;AAClE,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAErD,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;CACvB;AAiID,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,eAAe,CAAC;IAC1B,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAClC,MAAM,EAAE,cAAc,CAAC;IACvB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE;IAC3C,KAAK,EAAE,aAAa,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,cAAc,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,kBAAkB,CAAC;CAC1B,GAAG,OAAO,CAAC,eAAe,CAAC,CAqJ3B"}
|
package/index.js
CHANGED
|
@@ -407,19 +407,20 @@ var skilledOpenApiPluginOptionsObjectSchema = import_lazy_zod.z.object({
|
|
|
407
407
|
/** Bundle source — static, npm, or saas. */
|
|
408
408
|
source: import_skills.bundleSourceSchema,
|
|
409
409
|
/**
|
|
410
|
-
* Require a valid bundle signature. Defaults to true
|
|
411
|
-
*
|
|
412
|
-
*
|
|
410
|
+
* Require a valid bundle signature. Defaults to true, or to false with
|
|
411
|
+
* `dev: true`; an explicit value always wins. Never set false (unsigned mode)
|
|
412
|
+
* in production.
|
|
413
413
|
*/
|
|
414
|
-
requireSignature: import_lazy_zod.z.boolean().
|
|
414
|
+
requireSignature: import_lazy_zod.z.boolean().optional(),
|
|
415
415
|
/**
|
|
416
416
|
* Trusted public keys for bundle signature verification. At least one must
|
|
417
417
|
* match the bundle's signed `kid` for the bundle to load.
|
|
418
418
|
*/
|
|
419
419
|
trustedKeys: import_lazy_zod.z.array(import_skills.signatureKeySchema).default([]),
|
|
420
420
|
/**
|
|
421
|
-
* Development mode: bypass signature verification
|
|
422
|
-
*
|
|
421
|
+
* Development mode: bypass signature verification (unless `requireSignature`
|
|
422
|
+
* is set explicitly) and allow http: upstreams. Loud startup warning. Never
|
|
423
|
+
* true in production.
|
|
423
424
|
*/
|
|
424
425
|
dev: import_lazy_zod.z.boolean().default(false),
|
|
425
426
|
/** Outbound HTTP / SSRF defenses for the executor. */
|
|
@@ -488,6 +489,9 @@ var skilledOpenApiPluginOptionsSchema = skilledOpenApiPluginOptionsObjectSchema.
|
|
|
488
489
|
}
|
|
489
490
|
return {
|
|
490
491
|
...opts,
|
|
492
|
+
// `dev: true` turns the signature requirement off, as it advertises; an explicit
|
|
493
|
+
// `requireSignature` (either way) is kept.
|
|
494
|
+
requireSignature: opts.requireSignature ?? !opts.dev,
|
|
491
495
|
outbound,
|
|
492
496
|
bundleCacheDir: opts.bundleCacheDir ?? ".frontmcp/skilled-openapi/"
|
|
493
497
|
};
|
|
@@ -911,9 +915,66 @@ LoadSkillTool = __decorateClass([
|
|
|
911
915
|
var import_skills5 = require("@frontmcp/adapters/skills");
|
|
912
916
|
var import_sdk3 = require("@frontmcp/sdk");
|
|
913
917
|
|
|
918
|
+
// plugins/plugin-skilled-openapi/src/executor/caller-token.ts
|
|
919
|
+
var import_auth2 = require("@frontmcp/auth");
|
|
920
|
+
var import_utils = require("@frontmcp/utils");
|
|
921
|
+
function callerTokenOf(authInfo) {
|
|
922
|
+
if (typeof authInfo !== "object" || authInfo === null) return void 0;
|
|
923
|
+
const token = authInfo.token;
|
|
924
|
+
return typeof token === "string" && token.length > 0 ? token : void 0;
|
|
925
|
+
}
|
|
926
|
+
function callerTokenRefusal(token, url) {
|
|
927
|
+
const claims = (0, import_auth2.decodeJwtPayloadSafe)(token);
|
|
928
|
+
if (!claims) {
|
|
929
|
+
return "the caller token is not a JWT, so the API it was issued for cannot be checked";
|
|
930
|
+
}
|
|
931
|
+
let target;
|
|
932
|
+
try {
|
|
933
|
+
target = new URL(url);
|
|
934
|
+
} catch {
|
|
935
|
+
return `the caller token was not issued for ${url} (it is not a URL)`;
|
|
936
|
+
}
|
|
937
|
+
const destination = `${target.origin}${target.pathname}`;
|
|
938
|
+
const issuedFor = [...claimValues(claims["resource"]), ...claimValues(claims["aud"])];
|
|
939
|
+
if (!issuedFor.some((resource) => resourceCovers(resource, target))) {
|
|
940
|
+
return `the caller token was not issued for ${destination} (no resource or aud claim names it)`;
|
|
941
|
+
}
|
|
942
|
+
if (hasDecodedDotDotSegment(target.pathname)) {
|
|
943
|
+
return `the request path ${target.pathname} has a ".." segment once percent-decoded, which could take the caller token outside the API it was issued for`;
|
|
944
|
+
}
|
|
945
|
+
return void 0;
|
|
946
|
+
}
|
|
947
|
+
function claimValues(claim) {
|
|
948
|
+
if (typeof claim === "string") return [claim];
|
|
949
|
+
if (Array.isArray(claim)) return claim.filter((value) => typeof value === "string");
|
|
950
|
+
return [];
|
|
951
|
+
}
|
|
952
|
+
function resourceCovers(resource, target) {
|
|
953
|
+
let issued;
|
|
954
|
+
try {
|
|
955
|
+
issued = new URL(resource);
|
|
956
|
+
} catch {
|
|
957
|
+
return false;
|
|
958
|
+
}
|
|
959
|
+
if (issued.search || issued.hash || issued.origin !== target.origin) return false;
|
|
960
|
+
const issuedPath = (0, import_utils.trimTrailing)(issued.pathname, "/");
|
|
961
|
+
const targetPath = (0, import_utils.trimTrailing)(target.pathname, "/");
|
|
962
|
+
return issuedPath === "" || targetPath === issuedPath || targetPath.startsWith(`${issuedPath}/`);
|
|
963
|
+
}
|
|
964
|
+
var DOT_SEGMENT_ESCAPE = /%(?:2e|2f|5c|3b|25)/gi;
|
|
965
|
+
function hasDecodedDotDotSegment(pathname) {
|
|
966
|
+
let decoded = pathname;
|
|
967
|
+
let previous;
|
|
968
|
+
do {
|
|
969
|
+
previous = decoded;
|
|
970
|
+
decoded = previous.replace(DOT_SEGMENT_ESCAPE, (escape) => String.fromCharCode(parseInt(escape.slice(1), 16)));
|
|
971
|
+
} while (decoded !== previous);
|
|
972
|
+
return decoded.split(/[/\\]/).some((segment) => segment.split(";")[0] === "..");
|
|
973
|
+
}
|
|
974
|
+
|
|
914
975
|
// plugins/plugin-skilled-openapi/src/executor/openapi-runtime.ts
|
|
915
976
|
var import_openapi = require("@frontmcp/adapters/openapi");
|
|
916
|
-
var
|
|
977
|
+
var import_utils3 = require("@frontmcp/utils");
|
|
917
978
|
|
|
918
979
|
// plugins/plugin-skilled-openapi/src/executor/host-concurrency.ts
|
|
919
980
|
var gates = /* @__PURE__ */ new Map();
|
|
@@ -947,7 +1008,7 @@ async function withHostConcurrency(host, limit, fn) {
|
|
|
947
1008
|
}
|
|
948
1009
|
|
|
949
1010
|
// plugins/plugin-skilled-openapi/src/executor/ssrf-guard.ts
|
|
950
|
-
var
|
|
1011
|
+
var import_utils2 = require("@frontmcp/utils");
|
|
951
1012
|
var ALWAYS_FORBIDDEN_RANGES = /* @__PURE__ */ new Set([
|
|
952
1013
|
"link-local",
|
|
953
1014
|
"cloud-metadata",
|
|
@@ -956,7 +1017,7 @@ var ALWAYS_FORBIDDEN_RANGES = /* @__PURE__ */ new Set([
|
|
|
956
1017
|
"local-use-nat64"
|
|
957
1018
|
]);
|
|
958
1019
|
function findAddressViolation(address, outbound) {
|
|
959
|
-
const classification = (0,
|
|
1020
|
+
const classification = (0, import_utils2.classifyIpAddress)(address);
|
|
960
1021
|
if (!classification) {
|
|
961
1022
|
return { description: `malformed IP address ${address}`, alwaysForbidden: true };
|
|
962
1023
|
}
|
|
@@ -980,7 +1041,7 @@ var FORBIDDEN_METADATA_HOSTS = /* @__PURE__ */ new Set([
|
|
|
980
1041
|
]);
|
|
981
1042
|
function classifyHostLiteral(hostname) {
|
|
982
1043
|
const address = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
|
|
983
|
-
return { isIpLiteral: (0,
|
|
1044
|
+
return { isIpLiteral: (0, import_utils2.parseIpv4)(address) !== void 0 || address.includes(":"), address };
|
|
984
1045
|
}
|
|
985
1046
|
async function checkOutboundUrl(target, allowedHosts, outbound) {
|
|
986
1047
|
let url;
|
|
@@ -1028,35 +1089,30 @@ async function checkOutboundUrl(target, allowedHosts, outbound) {
|
|
|
1028
1089
|
|
|
1029
1090
|
// plugins/plugin-skilled-openapi/src/executor/openapi-runtime.ts
|
|
1030
1091
|
async function buildSecurityContext(args) {
|
|
1031
|
-
const { binding, bundleId, resolver, callerToken } = args;
|
|
1032
|
-
const ctx = {};
|
|
1092
|
+
const { binding, bundleId, resolver, serviceBaseUrl, callerToken } = args;
|
|
1033
1093
|
switch (binding.kind) {
|
|
1034
1094
|
case "none":
|
|
1035
|
-
return
|
|
1095
|
+
return { context: {} };
|
|
1036
1096
|
case "bearer": {
|
|
1037
|
-
let token;
|
|
1038
1097
|
if (binding.passthroughCallerToken) {
|
|
1039
|
-
token
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
if (!token) throw new Error(`bearer vaultRef "${binding.vaultRef}" did not resolve`);
|
|
1098
|
+
if (!callerToken) throw new Error("passthrough caller token requested but the caller presented none");
|
|
1099
|
+
const refusal = callerTokenRefusal(callerToken, serviceBaseUrl);
|
|
1100
|
+
if (refusal) throw new Error(`passthrough caller token refused: ${refusal}`);
|
|
1101
|
+
return { context: { jwt: callerToken }, forwardedCallerToken: callerToken };
|
|
1044
1102
|
}
|
|
1045
|
-
|
|
1046
|
-
|
|
1103
|
+
const token = await resolver.resolve(binding.vaultRef, { bundleId });
|
|
1104
|
+
if (!token) throw new Error(`bearer vaultRef "${binding.vaultRef}" did not resolve`);
|
|
1105
|
+
return { context: { jwt: token } };
|
|
1047
1106
|
}
|
|
1048
1107
|
case "apiKey": {
|
|
1049
1108
|
const value = await resolver.resolve(binding.vaultRef, { bundleId });
|
|
1050
1109
|
if (!value) throw new Error(`apiKey vaultRef "${binding.vaultRef}" did not resolve`);
|
|
1051
|
-
|
|
1052
|
-
ctx.apiKey = value;
|
|
1053
|
-
return ctx;
|
|
1110
|
+
return { context: { apiKeys: { [binding.name]: value }, apiKey: value } };
|
|
1054
1111
|
}
|
|
1055
1112
|
case "oauth2": {
|
|
1056
1113
|
const token = await resolver.resolve(binding.vaultRef, { bundleId });
|
|
1057
1114
|
if (!token) throw new Error(`oauth2 vaultRef "${binding.vaultRef}" did not resolve`);
|
|
1058
|
-
|
|
1059
|
-
return ctx;
|
|
1115
|
+
return { context: { oauth2Token: token } };
|
|
1060
1116
|
}
|
|
1061
1117
|
}
|
|
1062
1118
|
}
|
|
@@ -1119,12 +1175,13 @@ async function executeOperation(args) {
|
|
|
1119
1175
|
} catch (e) {
|
|
1120
1176
|
return failure(0, `tool projection failed: ${e.message}`);
|
|
1121
1177
|
}
|
|
1122
|
-
let
|
|
1178
|
+
let auth;
|
|
1123
1179
|
try {
|
|
1124
|
-
|
|
1180
|
+
auth = await buildSecurityContext({
|
|
1125
1181
|
binding: entry.authBinding,
|
|
1126
1182
|
bundleId,
|
|
1127
1183
|
resolver,
|
|
1184
|
+
serviceBaseUrl: entry.service.baseUrl,
|
|
1128
1185
|
callerToken
|
|
1129
1186
|
});
|
|
1130
1187
|
} catch (e) {
|
|
@@ -1132,7 +1189,7 @@ async function executeOperation(args) {
|
|
|
1132
1189
|
}
|
|
1133
1190
|
let security;
|
|
1134
1191
|
try {
|
|
1135
|
-
security = await resolveSecurity(mcpTool,
|
|
1192
|
+
security = await resolveSecurity(mcpTool, auth.context);
|
|
1136
1193
|
} catch (e) {
|
|
1137
1194
|
return failure(0, `security resolve failed: ${e.message}`);
|
|
1138
1195
|
}
|
|
@@ -1142,10 +1199,23 @@ async function executeOperation(args) {
|
|
|
1142
1199
|
} catch (e) {
|
|
1143
1200
|
return failure(0, `request build failed: ${e.message}`);
|
|
1144
1201
|
}
|
|
1202
|
+
if (auth.forwardedCallerToken) {
|
|
1203
|
+
const refusal = callerTokenRefusal(auth.forwardedCallerToken, req.url);
|
|
1204
|
+
if (refusal) return failure(0, `auth resolution failed: passthrough caller token refused: ${refusal}`);
|
|
1205
|
+
}
|
|
1145
1206
|
const ssrf = await checkOutboundUrl(req.url, allowedHosts, outbound);
|
|
1146
1207
|
if (!ssrf.ok) {
|
|
1147
1208
|
return failure(0, `ssrf check rejected request: ${ssrf.reason}`);
|
|
1148
1209
|
}
|
|
1210
|
+
let body;
|
|
1211
|
+
try {
|
|
1212
|
+
body = req.body !== void 0 ? JSON.stringify(req.body) : void 0;
|
|
1213
|
+
} catch (e) {
|
|
1214
|
+
return failure(0, `request body serialization failed: ${e.message}`);
|
|
1215
|
+
}
|
|
1216
|
+
if (body !== void 0 && !req.headers.has("content-type")) {
|
|
1217
|
+
req.headers.set("content-type", "application/json");
|
|
1218
|
+
}
|
|
1149
1219
|
const timeoutMs = entry.op.timeoutMs ?? outbound.defaultTimeoutMs;
|
|
1150
1220
|
const maxBytes = entry.op.maxResponseBytes ?? outbound.defaultMaxResponseBytes;
|
|
1151
1221
|
const host = (() => {
|
|
@@ -1163,7 +1233,7 @@ async function executeOperation(args) {
|
|
|
1163
1233
|
const response = await fetchImpl(req.url, {
|
|
1164
1234
|
method: entry.op.httpMethod,
|
|
1165
1235
|
headers: req.headers,
|
|
1166
|
-
body
|
|
1236
|
+
body,
|
|
1167
1237
|
signal: ac.signal,
|
|
1168
1238
|
// SECURITY: never auto-follow redirects. `fetch` defaults to
|
|
1169
1239
|
// `redirect: 'follow'`, which re-sends the request — INCLUDING the
|
|
@@ -1175,7 +1245,7 @@ async function executeOperation(args) {
|
|
|
1175
1245
|
// here so we can refuse it instead of following blindly.
|
|
1176
1246
|
redirect: "manual"
|
|
1177
1247
|
});
|
|
1178
|
-
if ((0,
|
|
1248
|
+
if ((0, import_utils3.isRedirectResponse)(response)) {
|
|
1179
1249
|
return failure(
|
|
1180
1250
|
response.status,
|
|
1181
1251
|
`upstream returned a redirect (${response.status}); not followed to protect injected credentials`
|
|
@@ -1432,6 +1502,8 @@ var OperationToolFactory = class {
|
|
|
1432
1502
|
entry,
|
|
1433
1503
|
bundleId: entry.bundleId,
|
|
1434
1504
|
input: inputParse.data,
|
|
1505
|
+
// For a bearer binding with `passthroughCallerToken: true`.
|
|
1506
|
+
callerToken: callerTokenOf(ctx.authInfo),
|
|
1435
1507
|
deps
|
|
1436
1508
|
});
|
|
1437
1509
|
return {
|
|
@@ -1511,6 +1583,8 @@ async function executeSkillAction(args) {
|
|
|
1511
1583
|
entry,
|
|
1512
1584
|
bundleId: entry.bundleId,
|
|
1513
1585
|
input: inputParse.data,
|
|
1586
|
+
// For a bearer binding with `passthroughCallerToken: true`.
|
|
1587
|
+
callerToken: callerTokenOf(authInfo),
|
|
1514
1588
|
deps: runtimeDeps
|
|
1515
1589
|
});
|
|
1516
1590
|
} catch (e) {
|
|
@@ -1785,6 +1859,18 @@ function resolveRuntimeDeps(scope) {
|
|
|
1785
1859
|
return void 0;
|
|
1786
1860
|
}
|
|
1787
1861
|
}
|
|
1862
|
+
function lazyScopeRegistry(scope, name) {
|
|
1863
|
+
return new Proxy({}, {
|
|
1864
|
+
get(_target, prop) {
|
|
1865
|
+
const registry = scope[name];
|
|
1866
|
+
if (!registry) {
|
|
1867
|
+
throw new Error(`[skilled-openapi] scope.${name} not available when accessing "${String(prop)}"`);
|
|
1868
|
+
}
|
|
1869
|
+
const value = registry[prop];
|
|
1870
|
+
return typeof value === "function" ? value.bind(registry) : value;
|
|
1871
|
+
}
|
|
1872
|
+
});
|
|
1873
|
+
}
|
|
1788
1874
|
function resolveBundleTelemetry(scope) {
|
|
1789
1875
|
const providers = scope?.providers;
|
|
1790
1876
|
if (!providers || typeof providers.get !== "function") return void 0;
|
|
@@ -1882,9 +1968,8 @@ ${catalog}` : searchSkillDescription;
|
|
|
1882
1968
|
}
|
|
1883
1969
|
warnIfInsecureConfig() {
|
|
1884
1970
|
if (this.options.dev) {
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
);
|
|
1971
|
+
const signing = this.options.requireSignature ? "signature verification kept (requireSignature=true)" : "signature verification BYPASSED";
|
|
1972
|
+
console.warn(`[skilled-openapi] dev=true: ${signing} and http:// URLs allowed. NEVER use this in production.`);
|
|
1888
1973
|
}
|
|
1889
1974
|
if (!this.options.requireSignature && !this.options.dev) {
|
|
1890
1975
|
console.warn(
|
|
@@ -1937,39 +2022,17 @@ ${catalog}` : searchSkillDescription;
|
|
|
1937
2022
|
inject: () => [import_sdk6.ScopeEntry, HiddenOpRegistry, import_skills8.BundleStore],
|
|
1938
2023
|
useFactory: async (scope, hiddenOps, bundleStore) => {
|
|
1939
2024
|
const logger = scope.logger.child("skilled-openapi:sync");
|
|
1940
|
-
const lazySkillRegistry =
|
|
1941
|
-
get(_t, prop) {
|
|
1942
|
-
const reg = scope.skills;
|
|
1943
|
-
if (!reg) {
|
|
1944
|
-
throw new Error(`[skilled-openapi] scope.skills not available when accessing "${String(prop)}"`);
|
|
1945
|
-
}
|
|
1946
|
-
const v = reg[prop];
|
|
1947
|
-
return typeof v === "function" ? v.bind(reg) : v;
|
|
1948
|
-
}
|
|
1949
|
-
});
|
|
2025
|
+
const lazySkillRegistry = lazyScopeRegistry(scope, "skills");
|
|
1950
2026
|
let opToolFactory;
|
|
1951
2027
|
if (parsed.exposeOperationsAsInternalTools) {
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
providers: scope.providers,
|
|
1961
|
-
logger: logger.child("op-tool")
|
|
1962
|
-
});
|
|
1963
|
-
} else {
|
|
1964
|
-
logger.warn(
|
|
1965
|
-
"exposeOperationsAsInternalTools=true but scope.tools is unavailable; per-op internal tools disabled"
|
|
1966
|
-
);
|
|
1967
|
-
}
|
|
1968
|
-
} catch (e) {
|
|
1969
|
-
logger.warn(
|
|
1970
|
-
`failed to build OperationToolFactory: ${e.message}; per-op internal tools disabled`
|
|
1971
|
-
);
|
|
1972
|
-
}
|
|
2028
|
+
opToolFactory = new OperationToolFactory({
|
|
2029
|
+
toolRegistry: lazyScopeRegistry(scope, "tools"),
|
|
2030
|
+
// ScopeEntry exposes a ProviderRegistryInterface; the factory
|
|
2031
|
+
// needs the concrete ProviderRegistry to construct ToolInstance.
|
|
2032
|
+
// The runtime is the same class — the cast is safe by construction.
|
|
2033
|
+
providers: scope.providers,
|
|
2034
|
+
logger: logger.child("op-tool")
|
|
2035
|
+
});
|
|
1973
2036
|
}
|
|
1974
2037
|
const sync = new BundleSyncService(
|
|
1975
2038
|
lazySkillRegistry,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontmcp/plugin-skilled-openapi",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "FrontMCP plugin: serve a customer's OpenAPI spec as skill bundles (signed, OpenAPI-Overlay-based) with hidden per-operation tools mediated by three meta-tools (search_skill / load_skill / run_workflow — the last runs an enclave-sandboxed AgentScript that orchestrates the loaded skills' operations).",
|
|
5
5
|
"author": "AgentFront <info@agentfront.dev>",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -48,15 +48,15 @@
|
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@frontmcp/adapters": "1.
|
|
52
|
-
"@frontmcp/auth": "1.
|
|
53
|
-
"@frontmcp/sdk": "1.
|
|
54
|
-
"@frontmcp/utils": "1.
|
|
51
|
+
"@frontmcp/adapters": "1.9.0",
|
|
52
|
+
"@frontmcp/auth": "1.9.0",
|
|
53
|
+
"@frontmcp/sdk": "1.9.0",
|
|
54
|
+
"@frontmcp/utils": "1.9.0"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"@enclave-vm/ast": "*",
|
|
58
58
|
"@enclave-vm/core": "*",
|
|
59
|
-
"@frontmcp/lazy-zod": "1.
|
|
59
|
+
"@frontmcp/lazy-zod": "1.9.0"
|
|
60
60
|
},
|
|
61
61
|
"peerDependenciesMeta": {
|
|
62
62
|
"@enclave-vm/ast": {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skilled-openapi.plugin.d.ts","sourceRoot":"","sources":["../src/skilled-openapi.plugin.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,gBAAgB,EAIrB,KAAK,iBAAiB,EACvB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAEL,aAAa,EAOb,KAAK,SAAS,EAEd,KAAK,YAAY,
|
|
1
|
+
{"version":3,"file":"skilled-openapi.plugin.d.ts","sourceRoot":"","sources":["../src/skilled-openapi.plugin.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,gBAAgB,EAIrB,KAAK,iBAAiB,EACvB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAEL,aAAa,EAOb,KAAK,SAAS,EAEd,KAAK,YAAY,EAGlB,MAAM,eAAe,CAAC;AAOvB,OAAO,EAEL,KAAK,2BAA2B,EAChC,KAAK,gCAAgC,EACtC,MAAM,yBAAyB,CAAC;AAsBjC;;;;;;;;;;GAUG;AACH,eAAO,MAAM,kCAAkC,eAAsD,CAAC;AAEtG;;;;;GAKG;AACH,MAAM,WAAW,yBAA0B,SAAQ,gBAAgB;IACjE;;;;OAIG;IACH,MAAM,CAAC,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAAC;CAC1C;AAuED;;;;;;;;;;;;GAYG;AAWH,MAAM,CAAC,OAAO,OAAO,oBAAqB,SAAQ,aAAa,CAC7D,2BAA2B,EAC3B,gCAAgC,CACjC;IACC,OAAO,EAAE,2BAA2B,CAAC;IACrC,OAAO,CAAC,YAAY,CAAC,CAAiB;gBAE1B,OAAO,EAAE,gCAAgC;IAMrD,OAAO,CAAC,SAAS;IAOjB;;;;;;;;;;OAUG;IAEG,gCAAgC,CAAC,OAAO,EAAE,SAAS,CAAC,kBAAkB,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAqC7F;;;;;OAKG;IAEG,0BAA0B,CAAC,OAAO,EAAE,SAAS,CAAC,eAAe,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAqBpF,OAAO,CAAC,eAAe;IAUvB,wFAAwF;IACxF,OAAO,CAAC,eAAe;IAQvB,OAAO,CAAC,oBAAoB;WAcZ,gBAAgB,CAAC,OAAO,EAAE,gCAAgC,GAAG,YAAY,EAAE;CA8I5F"}
|
|
@@ -33,7 +33,7 @@ declare const skilledOpenApiPluginOptionsObjectSchema: import("@frontmcp/lazy-zo
|
|
|
33
33
|
type: import("@frontmcp/lazy-zod").ZodLiteral<"inline">;
|
|
34
34
|
content: import("@frontmcp/lazy-zod").ZodUnknown;
|
|
35
35
|
}, import("zod/v4/core").$strip>], "type">;
|
|
36
|
-
requireSignature: import("@frontmcp/lazy-zod").
|
|
36
|
+
requireSignature: import("@frontmcp/lazy-zod").ZodOptional<import("@frontmcp/lazy-zod").ZodBoolean>;
|
|
37
37
|
trustedKeys: import("@frontmcp/lazy-zod").ZodDefault<import("@frontmcp/lazy-zod").ZodArray<import("@frontmcp/lazy-zod").ZodObject<{
|
|
38
38
|
keyId: import("@frontmcp/lazy-zod").ZodString;
|
|
39
39
|
alg: import("@frontmcp/lazy-zod").ZodEnum<{
|
|
@@ -87,7 +87,7 @@ export declare const skilledOpenApiPluginOptionsSchema: import("zod").ZodPipe<im
|
|
|
87
87
|
type: import("@frontmcp/lazy-zod").ZodLiteral<"inline">;
|
|
88
88
|
content: import("@frontmcp/lazy-zod").ZodUnknown;
|
|
89
89
|
}, import("zod/v4/core").$strip>], "type">;
|
|
90
|
-
requireSignature: import("@frontmcp/lazy-zod").
|
|
90
|
+
requireSignature: import("@frontmcp/lazy-zod").ZodOptional<import("@frontmcp/lazy-zod").ZodBoolean>;
|
|
91
91
|
trustedKeys: import("@frontmcp/lazy-zod").ZodDefault<import("@frontmcp/lazy-zod").ZodArray<import("@frontmcp/lazy-zod").ZodObject<{
|
|
92
92
|
keyId: import("@frontmcp/lazy-zod").ZodString;
|
|
93
93
|
alg: import("@frontmcp/lazy-zod").ZodEnum<{
|
|
@@ -118,6 +118,7 @@ export declare const skilledOpenApiPluginOptionsSchema: import("zod").ZodPipe<im
|
|
|
118
118
|
credentials: import("@frontmcp/lazy-zod").ZodOptional<import("@frontmcp/lazy-zod").ZodRecord<import("@frontmcp/lazy-zod").ZodString, import("@frontmcp/lazy-zod").ZodString>>;
|
|
119
119
|
exposeOperationsAsInternalTools: import("@frontmcp/lazy-zod").ZodDefault<import("@frontmcp/lazy-zod").ZodBoolean>;
|
|
120
120
|
}, import("zod/v4/core").$strip>, import("zod").ZodTransform<{
|
|
121
|
+
requireSignature: boolean;
|
|
121
122
|
outbound: {
|
|
122
123
|
allowPrivateNetworks: boolean;
|
|
123
124
|
maxConcurrencyPerHost: number;
|
|
@@ -149,7 +150,6 @@ export declare const skilledOpenApiPluginOptionsSchema: import("zod").ZodPipe<im
|
|
|
149
150
|
type: "inline";
|
|
150
151
|
content: unknown;
|
|
151
152
|
};
|
|
152
|
-
requireSignature: boolean;
|
|
153
153
|
trustedKeys: {
|
|
154
154
|
keyId: string;
|
|
155
155
|
alg: "RS256" | "EdDSA";
|
|
@@ -183,7 +183,6 @@ export declare const skilledOpenApiPluginOptionsSchema: import("zod").ZodPipe<im
|
|
|
183
183
|
type: "inline";
|
|
184
184
|
content: unknown;
|
|
185
185
|
};
|
|
186
|
-
requireSignature: boolean;
|
|
187
186
|
trustedKeys: {
|
|
188
187
|
keyId: string;
|
|
189
188
|
alg: "RS256" | "EdDSA";
|
|
@@ -201,6 +200,7 @@ export declare const skilledOpenApiPluginOptionsSchema: import("zod").ZodPipe<im
|
|
|
201
200
|
unprotectedOps: "allow" | "deny";
|
|
202
201
|
sourceConflictPolicy: "static-wins" | "last-wins" | "reject";
|
|
203
202
|
exposeOperationsAsInternalTools: boolean;
|
|
203
|
+
requireSignature?: boolean | undefined;
|
|
204
204
|
bundleCacheDir?: string | undefined;
|
|
205
205
|
credentials?: Record<string, string> | undefined;
|
|
206
206
|
}>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skilled-openapi.types.d.ts","sourceRoot":"","sources":["../src/skilled-openapi.types.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,oBAAoB,CAAC;AAKvC,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,gBAAgB,EAChB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,2BAA2B,CAAC;AACnC,YAAY,EACV,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,YAAY,EACZ,mBAAmB,GACpB,MAAM,2BAA2B,CAAC;AAInC,eAAO,MAAM,qBAAqB;;;;;;;iCAgC9B,CAAC;AAEL,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAIpE,QAAA,MAAM,uCAAuC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"skilled-openapi.types.d.ts","sourceRoot":"","sources":["../src/skilled-openapi.types.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,oBAAoB,CAAC;AAKvC,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,gBAAgB,EAChB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,2BAA2B,CAAC;AACnC,YAAY,EACV,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,YAAY,EACZ,mBAAmB,GACpB,MAAM,2BAA2B,CAAC;AAInC,eAAO,MAAM,qBAAqB;;;;;;;iCAgC9B,CAAC;AAEL,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAIpE,QAAA,MAAM,uCAAuC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gCA6E3C,CAAC;AAUH,eAAO,MAAM,iCAAiC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoB5C,CAAC;AAEH,kEAAkE;AAClE,MAAM,MAAM,2BAA2B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iCAAiC,CAAC,CAAC;AAE5F,uDAAuD;AACvD,MAAM,MAAM,gCAAgC,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uCAAuC,CAAC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"operation-tool.factory.d.ts","sourceRoot":"","sources":["../../src/tools/operation-tool.factory.ts"],"names":[],"mappings":"AAaA,OAAO,EAAe,KAAK,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAErF,OAAO,EAIL,KAAK,cAAc,EACnB,KAAK,gBAAgB,EAGrB,KAAK,YAAY,EAClB,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"operation-tool.factory.d.ts","sourceRoot":"","sources":["../../src/tools/operation-tool.factory.ts"],"names":[],"mappings":"AAaA,OAAO,EAAe,KAAK,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAErF,OAAO,EAIL,KAAK,cAAc,EACnB,KAAK,gBAAgB,EAGrB,KAAK,YAAY,EAClB,MAAM,eAAe,CAAC;AAKvB,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAWpE;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAS/E;AA2BD,MAAM,WAAW,wBAAwB;IACvC,sEAAsE;IACtE,YAAY,EAAE,YAAY,CAAC;IAC3B,8DAA8D;IAC9D,SAAS,EAAE,gBAAgB,CAAC;IAC5B,8BAA8B;IAC9B,MAAM,EAAE,cAAc,CAAC;IACvB;;;;;;;OAOG;IACH,sBAAsB,CAAC,EAAE,sBAAsB,CAAC;CACjD;AAED;;;;;GAKG;AACH,MAAM,WAAW,wBAAwB;IACvC,mEAAmE;IACnE,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CACnC;AAED;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAUD;;;GAGG;AACH,qBAAa,oBAAoB;IAMnB,OAAO,CAAC,QAAQ,CAAC,IAAI;IALjC,kFAAkF;IAClF,OAAO,CAAC,UAAU,CAAwC;IAC1D,2EAA2E;IAC3E,OAAO,CAAC,kBAAkB,CAAqB;gBAElB,IAAI,EAAE,wBAAwB;IAE3D;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,KAAK,EAAE,aAAa,EAAE,GAAG,CAAC,EAAE,wBAAwB,GAAG,IAAI;IA2CpE;;;;OAIG;IACH,aAAa,IAAI,IAAI;IAqBrB,iFAAiF;IACjF,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;;;;;OAMG;IACH,OAAO,CAAC,YAAY;CAiFrB"}
|