@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 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 | 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` | Require a valid bundle signature (RS256/Ed25519 JWT-of-hashes). Opt out only with `dev: true`. |
101
- | `trustedKeys` | `SignatureKey[]` | `[]` | Public keys trusted to sign bundles. |
102
- | `dev` | `boolean` | `false` | Local-dev escape hatch: bypasses signing 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` | Keep operations reachable via `callTool` inside workflows. |
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. Setting `dev: true`
396
- * disables the requirement and emits a startup warning. Never set false
397
- * (unsigned mode) silently in production.
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().default(true),
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 and relax some defaults
407
- * (e.g. allow http:). Loud startup warning. Never true in production.
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 ctx;
1094
+ return { context: {} };
1035
1095
  case "bearer": {
1036
- let token;
1037
1096
  if (binding.passthroughCallerToken) {
1038
- token = callerToken;
1039
- if (!token) throw new Error("passthrough caller token requested but not supplied");
1040
- } else {
1041
- token = await resolver.resolve(binding.vaultRef, { bundleId });
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
- ctx.jwt = token;
1045
- return ctx;
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
- ctx.apiKeys = { [binding.name]: value };
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
- ctx.oauth2Token = token;
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 securityContext;
1177
+ let auth;
1122
1178
  try {
1123
- securityContext = await buildSecurityContext({
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, securityContext);
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: req.body !== void 0 ? JSON.stringify(req.body) : void 0,
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
- console.warn(
1885
- "[skilled-openapi] dev=true: signature verification BYPASSED and http:// URLs allowed. NEVER use this in production."
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 = new Proxy({}, {
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
- try {
1952
- const toolRegistry = scope.tools;
1953
- if (toolRegistry) {
1954
- opToolFactory = new OperationToolFactory({
1955
- toolRegistry,
1956
- // ScopeEntry exposes a ProviderRegistryInterface; the factory
1957
- // needs the concrete ProviderRegistry to construct ToolInstance.
1958
- // The runtime is the same class — the cast is safe by construction.
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.8.7",
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.8.7",
52
- "@frontmcp/auth": "1.8.7",
53
- "@frontmcp/sdk": "1.8.7",
54
- "@frontmcp/utils": "1.8.7"
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.8.7"
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;AAC7F,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,CAsH7B"}
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;AAChE,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;AA2HD,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,CA+H3B"}
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. Setting `dev: true`
411
- * disables the requirement and emits a startup warning. Never set false
412
- * (unsigned mode) silently in production.
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().default(true),
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 and relax some defaults
422
- * (e.g. allow http:). Loud startup warning. Never true in production.
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 import_utils2 = require("@frontmcp/utils");
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 import_utils = require("@frontmcp/utils");
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, import_utils.classifyIpAddress)(address);
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, import_utils.parseIpv4)(address) !== void 0 || address.includes(":"), address };
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 ctx;
1095
+ return { context: {} };
1036
1096
  case "bearer": {
1037
- let token;
1038
1097
  if (binding.passthroughCallerToken) {
1039
- token = callerToken;
1040
- if (!token) throw new Error("passthrough caller token requested but not supplied");
1041
- } else {
1042
- token = await resolver.resolve(binding.vaultRef, { bundleId });
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
- ctx.jwt = token;
1046
- return ctx;
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
- ctx.apiKeys = { [binding.name]: value };
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
- ctx.oauth2Token = token;
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 securityContext;
1178
+ let auth;
1123
1179
  try {
1124
- securityContext = await buildSecurityContext({
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, securityContext);
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: req.body !== void 0 ? JSON.stringify(req.body) : void 0,
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, import_utils2.isRedirectResponse)(response)) {
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
- console.warn(
1886
- "[skilled-openapi] dev=true: signature verification BYPASSED and http:// URLs allowed. NEVER use this in production."
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 = new Proxy({}, {
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
- try {
1953
- const toolRegistry = scope.tools;
1954
- if (toolRegistry) {
1955
- opToolFactory = new OperationToolFactory({
1956
- toolRegistry,
1957
- // ScopeEntry exposes a ProviderRegistryInterface; the factory
1958
- // needs the concrete ProviderRegistry to construct ToolInstance.
1959
- // The runtime is the same class — the cast is safe by construction.
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.8.7",
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.8.7",
52
- "@frontmcp/auth": "1.8.7",
53
- "@frontmcp/sdk": "1.8.7",
54
- "@frontmcp/utils": "1.8.7"
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.8.7"
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,EAClB,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;AAqDD;;;;;;;;;;;;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;WAaZ,gBAAgB,CAAC,OAAO,EAAE,gCAAgC,GAAG,YAAY,EAAE;CAoK5F"}
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").ZodDefault<import("@frontmcp/lazy-zod").ZodBoolean>;
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").ZodDefault<import("@frontmcp/lazy-zod").ZodBoolean>;
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gCA4E3C,CAAC;AAUH,eAAO,MAAM,iCAAiC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiB5C,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
+ {"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;AAIvB,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;CA+ErB"}
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"}